The Nightly Build.
Expert Tools

Moving a date from Spark to Snowflake takes two casts, and SQLGlot writes them

A no-dependency library for more than 30 SQL dialects, honest that its lenient parser will accept queries a real engine would reject.

Where find-and-replace stops working

You have a working Spark query and a Snowflake warehouse that has to run it. Most of the syntax is shared, so the quick fix is to patch the few functions that differ with a pass of string substitution. That holds until the first function whose replacement is not just another function. Spark's DATE_ADD(a, 1) adds a day to a date. Snowflake writes the same operation as DATEADD(DAY, 1, ...), and it will not accept the bare column Spark accepted: the argument has to be cast to a date first. Character-level substitution cannot know that, because it never parsed the query.

SQLGlot parses SQL into a real syntax tree and generates SQL back from it, so it preserves what a query means even when the exact text changes.1 It is written in pure Python, carries no dependencies, and reads more than 30 dialects.1 The release I tested, 30.17.0, registers 33 of them and imports nothing outside the standard library.2 Once a query is a tree instead of a string, a translation can rewrite structure and not only spelling, which is what the Spark-to-Snowflake case needs. For raw throughput there is an optional compiled build, sqlglot[c], that parses roughly three to five times faster than the pure-Python default.1

0runtime dependencies, base install
33dialects registered in 30.17.0
3–5×faster parsing with the compiled build

What one Spark date call becomes in Snowflake

Here is the translation, run against 30.17.0.

three cross-dialect translationsPYTHON
import sqlglot

# Spark's DATE_ADD adds days to a date. Snowflake's DATEADD needs the unit
# named and the argument cast to a date before it will add to it.
sqlglot.transpile("SELECT DATE_ADD(a, 1) FROM t",
                  read="spark", write="snowflake")[0]
# SELECT DATEADD(DAY, 1, CAST(CAST(a AS TIMESTAMP) AS DATE)) FROM t

# DuckDB epoch-milliseconds to Hive: the divide-by-1000 is inserted, not renamed.
sqlglot.transpile("SELECT EPOCH_MS(1618088028295)",
                  read="duckdb", write="hive")[0]
# SELECT FROM_UNIXTIME(1618088028295 / POW(10, 3))

# MySQL's strftime-style format becomes Postgres's TO_DATE pattern.
sqlglot.transpile("SELECT STR_TO_DATE(x, '%Y-%m-%d') FROM t",
                  read="mysql", write="postgres")[0]
# SELECT TO_DATE(x, 'YYYY-MM-DD') FROM t
Fig. 1 · transpile parses each query in its read dialect and regenerates it in the write dialect; the outputs shown are what 30.17.0 prints.32

None of the three outputs is a rename. Spark's single DATE_ADD becomes a DATEADD with the DAY unit spelled out and the column wrapped in two casts, because Snowflake will not add days to an untyped value. The DuckDB-to-Hive line turns EPOCH_MS into FROM_UNIXTIME and inserts the division by a thousand that converts milliseconds to seconds, arithmetic no target function carried on its own. The MySQL-to-Postgres line swaps STR_TO_DATE for TO_DATE and rewrites the format string '%Y-%m-%d' into 'YYYY-MM-DD'. Each rewrite is a decision about what the source meant, made by walking the parsed tree and emitting the target dialect's equivalent.

The translation is best-effort by design. When a feature has no equivalent in the target dialect, SQLGlot emits a warning and produces the closest SQL it can rather than failing, and you can raise the error level with error_level to make it stop instead.1

Asking the tree which tables a query touches

The same parse that made the translation possible also makes the query inspectable. A tree is a data structure, so you can walk it and pull out every node of a given kind.

reading a parsed queryPYTHON
from sqlglot import parse_one, exp

tree = parse_one("SELECT a, b FROM x JOIN y USING (id)")

[t.name for t in tree.find_all(exp.Table)]   # ['x', 'y']
[c.name for c in tree.find_all(exp.Column)]  # ['a', 'b']
Fig. 2 · find_all collects every node of a type from the parsed tree, here the tables the query joins and the columns it selects.2

find_all(exp.Table) returns the two tables the query joins and find_all(exp.Column) returns the selected columns, with no database connection and no catalog.2 This is the operation a linter or an access check needs: given a query, which objects does it read? The same tree also supports column-level lineage. Asked which source produced an output column, lineage traces it back through subquery aliases; for SELECT y.c AS c FROM (SELECT t.a AS c FROM t) AS y it follows the output c through the alias y.c to the base column t.a.2 And given a schema, an optimizer over the same tree expands SELECT * into fully qualified columns and folds a constant predicate like 1 = 1 out of a WHERE clause.12 Each capability is one more walk over the same parse tree.

The parser accepts SQL a database rejects

The permissiveness that lets SQLGlot parse so many dialects is also its sharpest limit, and the project states it outright.

The project's own caveat
The parser is intentionally lenient, so it can accept queries that a real engine would reject. SQLGlot is a transpiler, not a validator. A query that parses successfully may still fail at execution time.1 SQLGlot README, FAQ

In practice, SQLGlot will parse SELECT SOMEUDF(x) OVER () FROM t with an unknown function, and a query that groups by column positions that do not exist, and hand you a tree for either.2 It is not a rubber stamp: genuinely malformed input like SELECT FROM WHERE still raises a ParseError.2 Lenient means it tolerates SQL that is well-formed but engine-invalid, not arbitrary text. This is where SQLGlot is the wrong tool. If the job is to decide whether a query is valid for a specific engine, SQLGlot cannot answer it. That check belongs to the engine, which owns the semantics SQLGlot declines to enforce.1

Coverage also varies by dialect. They fall into tiers: 18 are officially maintained by the core team with priority on fixes and features, 14 are community-maintained at lower priority, and a couple more live as third-party plugins with no core-team support.1 For a mainstream engine the fidelity is high. For a niche one, a translation is close without a guarantee of exactness, and the honest expectation is best-effort.

The speed you trade for pure Python

Pure Python is the parser's headline feature and its main runtime cost. The base install runs entirely in the interpreter, which is slower than a native tokenizer.24 The remedy is the optional compiled build, sqlglot[c], produced by compiling the same Python with mypyc. Installing it overlays compiled extensions onto SQLGlot's own modules.45 On the project's own parse benchmark it runs each query in a fifth to a quarter of the pure-Python time.

Parse time in seconds, pure-Python sqlglot against the compiled sqlglot[c]; the factor is sqlglot[c] relative to pure Python at 1.00, so lower is faster. Python 3.14.3, parse-only.1
Query sqlglot sqlglot[c] Factor
tpch 0.002709 0.000740 0.27
values 0.466734 0.113762 0.24
complex_where 0.032710 0.006602 0.20

Earlier, the accelerator was a tokenizer rewritten in Rust and shipped as sqlglotrs. It landed in SQLGlot 20.3.0 in early 2024 with a claimed 30 to 40 percent improvement in overall parsing speed.6 As of 30.17.0 the source says sqlglotrs "is deprecated and no longer compatible with sqlglot" and points users to sqlglotc instead.47 The maintainers' account is that compiling the existing Python with mypyc matched the Rust tokenizer while removing a separate build, an independent release cycle, and any need for Rust expertise.8

Two caveats sit under the speed. Peak performance is no longer dependency-free: sqlglot[c] pulls a compiled wheel, so the no-dependency claim holds for the base parser and not for the fast path.5 And the FAQ notes that a subclassed custom dialect "may not work properly" with sqlglot[c] installed, so a team that writes its own dialect and also wants native speed may have to pick one.1

Whether the project is here to stay

Whether to depend on SQLGlot comes down to whether it will be maintained. It ships on the order of a new minor release every week or two: 30.17.0 came out on 12 August 2026, after 30.16.0, 30.15.0, and 30.14.0 across the preceding weeks.9 It is MIT-licensed and requires Python 3.9 or newer.9 The cadence is not a volunteer's spare hours. SQLGlot is the open-source core of Tobiko Data, a company that raised $21.8 million and builds the SQLMesh data platform on top of it.10 The maintainers' own product depends on the parser, which is the strongest evidence that it will keep being fixed.110

Verdict

Reach for SQLGlot when you need to read, translate, or reason about SQL as a data structure across dialects and can accept a parse that is deliberately more permissive than any single engine. Reach for the database itself when the question is whether a query is valid or exactly how it will run. The trust question rests on the funded backer and the weekly cadence. A stall in releases, or the flagship product moving off it, is what would change the assessment.910

Sources

  1. GitHub · SQLGlot README (v30.17.0)
  2. Local run against sqlglot 30.17.0 (pip install sqlglot==30.17.0)
  3. SQLGlot · API documentation
  4. GitHub · SQLGlot tokenizer backend selection (tokens.py, v30.17.0)
  5. PyPI · sqlglotc (mypyc-compiled build)
  6. Tobiko Data · SQLGlot jumps on the Rust bandwagon
  7. PyPI · sqlglotrs (deprecated Rust tokenizer)
  8. Fivetran · Accelerating transpilation by compiling SQLGlot with mypyc
  9. PyPI · sqlglot (release history)
  10. TechCrunch · Tobiko raises $21.8M to build a modern data platform