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
What one Spark date call becomes in Snowflake
Here is the translation, run against 30.17.0.
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
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.
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']
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 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.
| 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
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
- GitHub · SQLGlot README (v30.17.0)
- Local run against sqlglot 30.17.0 (pip install sqlglot==30.17.0)
- SQLGlot · API documentation
- GitHub · SQLGlot tokenizer backend selection (tokens.py, v30.17.0)
- PyPI · sqlglotc (mypyc-compiled build)
- Tobiko Data · SQLGlot jumps on the Rust bandwagon
- PyPI · sqlglotrs (deprecated Rust tokenizer)
- Fivetran · Accelerating transpilation by compiling SQLGlot with mypyc
- PyPI · sqlglot (release history)
- TechCrunch · Tobiko raises $21.8M to build a modern data platform