Skip to main content

sql-sage

Parse, check, translate and review SQL for PostgreSQL, SQLite and Oracle. No dependencies. Every dialect parses into one shared AST, and everything else works on that AST.

SQL (any dialect) --parse--> shared AST --+--> SQL for another dialect   (transpile)
                                          +--> compatibility report      (--compat)
                                          +--> readable outline          (ordered / tight)
                                          +--> query fingerprint         (--fingerprint)

Install

pipx install sql-sage          # or: uv tool install sql-sage / pip install sql-sage

The PyPI package ships the sql-sage command as a prebuilt wheel (Linux, macOS, Windows); it has no Python API. Prebuilt binaries are also attached to each GitHub release, or build from source with cargo install --git https://github.com/vivainio/sql-sage. As a Rust library:

sql-sage = { git = "https://github.com/vivainio/sql-sage" }

CLI

sql-sage -d oracle --check "SELECT * FROM t LIMIT 5"        # syntax check, exit 1 on error
sql-sage -d oracle --to postgres -f query.sql                # translate
sql-sage -d oracle --compat postgres -f query.sql            # what needs changing?
sql-sage -d postgres --to ordered "SELECT ..."               # readable outline
sql-sage -d postgres --to tight "SELECT ..."                 # symbolic outline
sql-sage -d oracle --fingerprint -f queries.sql              # tables, operations, functions
sql-sage -d sqlite --ast "SELECT 1"                          # AST as YAML
sql-sage -d sqlite --ast-compact "SELECT 1"                  # shorter YAML-like text (not valid YAML)
sql-sage -d sqlite --ast-debug "SELECT 1"                    # verbose Rust Debug output

Dialects: postgres, sqlite, oracle. SQL comes from -f FILE, an argument, or stdin.

Library

use sql_sage::{check_syntax, check_compatibility, transpile, outline_sql};
use sql_sage::dialect::DialectKind::*;
use sql_sage::outline::Syntax;

check_syntax(Postgres, "SELECT a::int FROM t")?;                          // syntax check
let sql = transpile(Oracle, Postgres, "SELECT 1 FROM dual MINUS SELECT 2 FROM dual")?;
let report = check_compatibility(Oracle, Postgres, "SELECT NVL(a, 0) FROM t")?;
assert!(!report.is_compatible());
let text = outline_sql(Postgres, Syntax::Ordered, "SELECT a FROM t WHERE b = 1")?;

parse_sql(&dyn Dialect, sql) returns the AST (sql_sage::ast). Dialects implement the Dialect trait, which only describes what differs between engines.

Translation

transpile rewrites what can be rewritten safely and returns an error for what cannot: pagination (LIMIT/OFFSET vs FETCH FIRST), MINUS/EXCEPT, FROM dual, bind parameters ($1, ?, :name), data types (VARCHAR2/VARCHAR, NUMBER/NUMERIC, ...), ILIKE, % vs MOD, booleans, INSERT OR IGNORE, and more. RETURNING, IF EXISTS, GLOB, array types and similar constructs error on engines that lack them.

Compatibility report

oracle -> postgres: 1 statement
  2 incompatible, 1 warning, 1 rewritten automatically

statement 1 (line 1)
  [incompatible] function NVL does not exist in postgres: use COALESCE
      at: NVL(e.bonus, 0)
  [warning] '' is NULL in oracle but an empty string in postgres
  [rewritten] set difference spelled EXCEPT on the target
  • incompatible: needs a manual change
  • warning: translates, but behaves differently ('' is NULL, || with NULL, integer division, identifier case folding)
  • rewritten: handled automatically by transpile

The report and transpile share one set of rules.

Readable outlines

Neither outline is SQL and neither can be parsed back. Both read in execution order.

ordered (words):

from orders as o
left join customers as c on o.customer_id = c.id
filter o.status = 'open'
   and not exists (
     from blocked as b
     filter b.customer_id = c.id
     select 1
   )
group c.name
select c.name, sum(o.total) as revenue
sort revenue desc
take 10

tight (symbols: @ from, + <+ +> <+> x joins, ? filter, & and, by group, ?? having, > select, ~ sort, [offset:limit], { } subqueries):

@ orders o
<+ customers c on o.customer_id = c.id
? o.status = 'open'
& !exists {
  @ blocked b
  ? b.customer_id = c.id
  > 1
}
by c.name
> c.name, sum(o.total) as revenue
~ revenue desc
[:10]

Fingerprint

A fingerprint says which tables a statement touches (and how), which operations it applies and which functions it calls. Literals, columns, aliases, bind parameters, CTE names and dialect spelling are ignored, so the same query shape gets the same fingerprint on every dialect and with any parameter values.

$ sql-sage -d oracle --fingerprint "SELECT NVL(a,0), count(*) FROM emp GROUP BY a"
b0f00150db4166d4 [ora] select | emp:read | group aggregate | count() nvl()

Sections are kind | tables | operations | functions (- when empty). The leading id is a 64-bit hash of that text, handy for grouping queries in logs or finding every query that writes to a table.

The bracketed code lists the dialects the statement runs on without manual changes: ora, pg, lite (for example [ora,pg,lite] for portable SQL, [pg,lite] for RETURNING, [ora] for NVL, - when none fit). A ? means nothing is known to be wrong but the statement calls functions that cannot be verified there ([ora?,pg,lite?] for a user-defined function written for Postgres). It uses the same analysis as --compat against every dialect, the source one included. Warnings and automatic rewrites do not reduce compatibility. The code is not part of the id, so the id depends only on query shape.

From Rust: fingerprint_sql(dialect, sql) or stmt.fingerprint_for(dialect) (use stmt.fingerprint() for shape only), with reads(), writes(), is_read_only(), id() and compat_code() on the result.

Function catalogs

Function availability is judged against catalogs of the built-in functions of the latest release of each engine, generated from the engines' own sources by tools/gen_function_catalogs.py into data/functions/:

engine source names
PostgreSQL 18 pg_proc.dat + system_functions.sql (REL_18_STABLE) 2793
SQLite 3.53.4 PRAGMA function_list of the official amalgamation, built with the CLI options 137
Oracle AI Database 26ai function pages of the SQL Language Reference (plus conditions and byte/char variants) 342

For each call and target dialect: in the target's catalog means fine; missing from the target but present in another dialect is incompatible (with a replacement hint such as NVL -> COALESCE where known); in no catalog is unverified (probably user-defined or from an extension) and is trusted for the dialect the SQL was written in. A few names exist everywhere but mean different things (DECODE, CONCAT with 3+ arguments, MAX(a, b)); those are flagged as collisions. Schema- or package-qualified calls are ignored.

Re-run the script to move to newer releases: tools/gen_function_catalogs.py --pg 19 --sqlite <path> --oracle 26 (needs network, gcc, unzip).

AST dump

--ast prints the shared AST as YAML that any YAML parser loads. The node type is a key; empty fields (None, [], false, zero counts) are left out, wrapper nodes collapsed, and small nodes inlined. Several statements become a multi-document stream (---).

$ sql-sage -d sqlite --ast "SELECT a FROM t WHERE b > 1 LIMIT 5, 10"
Query:
  body:
    Select:
      projection: [{Identifier: a}]
      from: [{TableWithJoins: {relation: {Table: {name: t}}}}]
      selection: {BinaryOp: {left: {Identifier: b}, op: Gt, right: {Number: 1}}}
  limit: {Number: 10}
  offset: {Number: 5}

Strings stay strings ('true', '1', '2020-01-01' are quoted), numbers and booleans are typed, quoted identifiers appear as {Quoted: name}, and keys YAML 1.1 would read as booleans (On) are quoted. --ast-compact is a shorter form for reading (body: Select with fields nested under it) and is not valid YAML; --ast-debug is the verbose Rust Debug output.

From Rust: sql_sage::ast_dump::{yaml, compact}. The shape follows the AST and is not a stable interchange format.

Scope and limits

  • Syntax only: no check that tables, columns or types exist.
  • Covered: SELECT (CTEs, set operations, joins, subqueries, window functions), INSERT (incl. ON CONFLICT), UPDATE, DELETE, CREATE TABLE, CREATE INDEX, DROP.
  • Not covered: PL/SQL and procedural code, CONNECT BY, Oracle (+) joins, window frames, IS DISTINCT FROM, CREATE VIEW, ALTER.
  • The parser is lenient in places (for example AUTOINCREMENT is accepted in every dialect).
  • Function catalogs contain names only (no argument types or per-version availability) and cover core built-ins; extension functions such as pg_trgm's are unverified. The Postgres catalog is every callable pg_proc function, including internal ones. The Oracle catalog is scraped from the docs, so a function missing from those pages would be reported as missing. Replacement hints (Dialect::function_hint) and collisions are short curated lists. transpile does not translate functions.
  • The AST has no source spans, so findings point at a statement and the expression text.

Development

cargo test
cargo clippy

The library has no dependencies; yaml-rust2 is a dev-dependency used to check that --ast output is valid YAML.

Releases

The version in Cargo.toml is a placeholder (0.0.0-dev); the release workflow takes the real one from the git tag. To cut a release, let gh create the tag:

gh release create vX.Y.Z --title "vX.Y.Z" --notes "..."

That triggers .github/workflows/release.yml, which builds binaries for Linux, macOS (Intel and Apple Silicon) and Windows with checksums, builds wheels and an sdist with maturin, attaches the binaries to the release and publishes the wheels to PyPI via Trusted Publishing (no token stored). One-time setup on PyPI: add a (pending) publisher for project sql-sage with owner vivainio, repository sql-sage, workflow release.yml and environment pypi.

CI (.github/workflows/ci.yml) runs cargo fmt --check, clippy -D warnings and the tests on Linux, macOS and Windows, and smoke-tests a built wheel.

License

MIT

Metadata

Release files for sql-sage 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sql-sage 0.1.0
File Size Uploaded
sql_sage-0.1.0.tar.gz 83.8 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for sql-sage 0.1.0
File
sql_sage-0.1.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
sql_sage-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl Python 3 none Linux glibc 2.17+ x86-64 Details
sql_sage-0.1.0-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details
sql_sage-0.1.0-py3-none-macosx_10_12_x86_64.whl Python 3 none macOS 10.12+ x86-64 Details

Total release size: 2.0 MB

Release files / sql_sage-0.1.0.tar.gz

Download URL sql_sage-0.1.0.tar.gz
Size 83.8 kB
Tags Source
SHA-256 checksum
How to use checksums
ab1badb6679ebf0860a3f33da1608e8a8b28e42aa696588d0cc6bb5c4282dac7
BLAKE2b-256 checksum
How to use checksums
915ec86064d3401bd48c8e4f4fc6f25b3758b5586040c815c67d446a9b9b3f37
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release files / sql_sage-0.1.0-py3-none-win_amd64.whl

Download URL sql_sage-0.1.0-py3-none-win_amd64.whl
Size 404.9 kB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
e98212f6d6a5c8b6342272e406e7853b7488292b221501906fe9620267adf6b9
BLAKE2b-256 checksum
How to use checksums
c5c4d32cd1a112954a501ae62e849c5d12a6b311e568c6b81790f7aa9160c57b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release files / sql_sage-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL sql_sage-0.1.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 535.6 kB
Tags Linux glibc 2.17+ x86-64 Python 3
SHA-256 checksum
How to use checksums
5bc7c57db704d48905bba27e69272ad8c94c932caf5399c87b6a6a6492d81d15
BLAKE2b-256 checksum
How to use checksums
6afe994a927df54fc11754de1c9150be4eecc2ed825375471ab56142a2f73f3c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release files / sql_sage-0.1.0-py3-none-macosx_11_0_arm64.whl

Download URL sql_sage-0.1.0-py3-none-macosx_11_0_arm64.whl
Size 499.1 kB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
0f1033507c2801e6ef1acc2f26244358a892e0d7995f2cba6046f2e85f229368
BLAKE2b-256 checksum
How to use checksums
160f7197a1e19b3f90d981f1b08d726b2958b40a5dfcc22b1a2d8ff875589b08
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release files / sql_sage-0.1.0-py3-none-macosx_10_12_x86_64.whl

Download URL sql_sage-0.1.0-py3-none-macosx_10_12_x86_64.whl
Size 509.5 kB
Tags Python 3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
ac79e6232a2dafa9c5cbe201773c6826caca913459ff64e84a9f233f4cb9e75d
BLAKE2b-256 checksum
How to use checksums
337c758399f21e1778daae1752e93153af6336ac7b0cdc42ac5ab4a5d2060a11
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

5 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page