mrkeyoor.com_
Sun 20 Sept 11:42 UTC
PyPIDataupdated 20 Sept 2026

sqlglot review

Our sqlglot 30.17.0 install provided a typed, pure-Python SQL parser that produces an editable expression tree, prints that tree in more than 30 database dialects, qualifies names against schemas, traces column lineage, and runs optimizer rewrites. It can also execute a limited subset of SQL over Python data, though that executor is for tests and small examples. Version 30.17.0 expands Trino routine parsing with WHILE, LOOP, REPEAT, ITERATE, and LEAVE. It also fixes projection pruning around GROUP BY, HAVING, QUALIFY, set operations, and OR-conditioned joins, while its test executor gains correct LIKE, NOT LIKE, ILIKE, and LENGTH behavior.

Verdict

SQLGlot is the strongest Python choice when SQL must be parsed into something you can inspect, edit, qualify, or emit for another engine. Pin 30.17.x, always name the dialects, and install a real validator or database check when acceptance by the target engine matters.

We installed it

Lab card: what happened when we installed sqlglotScreenshot of sqlglot documentation
Install✓ · 0.5s1 package on disk · 4 MB
Importimport sqlglot in 0.54s · pure Python · py.typed · requires Python >=3.9
Known vulns0(pip-audit)

Answers from our run

Does sqlglot install cleanly?

Yes. In a fresh container with an empty cache, pip install sqlglot finished in 0.5s, leaving 1 package and 4 MB on disk. pip-audit reported no known vulnerabilities.

What does sqlglot need to run?

Python >=3.9, and nothing compiled: it is pure Python. In our run import sqlglot succeeded in 0.54s, and the package ships py.typed for type checkers.

sqlglot or sqlparse: which should you use?

sqlparse: Choose it for statement splitting and lightweight formatting when dialect-aware AST analysis and transpilation are unnecessary. SQLGlot is the strongest Python choice when SQL must be parsed into something you can inspect, edit, qualify, or emit for another engine.

When should you not use sqlglot?

You need a SQL validator. The FAQ says the parser is intentionally lenient, so a successfully parsed query can still be rejected by its database

API stability2/5The README explicitly increments MINOR for backward-incompatible changes and MAJOR for significant backward-incompatible changes. That makes 30.16 to 30.17 a boundary where optimizer output, dialect parsing, expression classes, or helper behavior may change. The 30.17.0 commit set contains several changes marked breaking in optimizer and dialect work. Exact pins plus query snapshots are appropriate for production rewrite pipelines.
Docs4/5The README documents dialect selection, lenient parsing, unsupported translations, expression traversal, AST transforms, optimizer use, semantic diffing, custom dialects, execution, benchmarks, and compiled extras with runnable code. Separate AST and diff primers explain the representation. The API site is extensive but largely generated reference material, so advanced lineage and optimizer composition still requires reading source, tests, or repository posts.
Maintenance5/5Version 30.17.0 reached PyPI on 12 August 2026, the repository pushed on 23 August, and GitHub showed only four open issues and pull requests with 9,560 stars. The change set includes dialect features, optimizer correctness fixes, type annotations, execution fixes, and integration-test routing rather than release-only churn. Frequent releases reduce the time to a fix, though they also reinforce the need to pin because minor versions can break behavior.
Ecosystem5/5Weekly use is recorded at 21,075,985 downloads. The README lists projects such as Apache Superset, Ibis, Dagster, dlt, SQLMesh, SQLFrame, and others that depend on its parsing or generation. More than 30 built-in dialects cover major warehouses and query engines, while plugin entry points and custom subclasses make smaller engines possible. Its position inside other data tools matters more than direct end-user adoption alone.

Use it if

  • You need to translate SQL between named engines and can review unsupported constructs instead of treating output as automatically executable
  • A lineage, policy, formatting, query-comparison, or cost-analysis tool needs an AST rather than string matching
  • You want to rewrite identifiers, projections, predicates, or functions and then regenerate valid SQL for a target dialect
  • Schema-aware qualification and optimizer passes are useful for making queries comparable before caching, diffing, or analysis
Skip it if

Setup reality

We installed sqlglot 30.17.0 in a fresh Python 3.12 Bookworm container with no cache. uv finished in 0.5 seconds and left one package using 4 MB on disk. The measurement reported 18 direct dependencies. The package is pure Python, requires Python 3.9 or newer, and ships py.typed. pip-audit found zero known vulnerabilities, and import sqlglot worked in 0.54 seconds. The installed package's license metadata was unknown in the measurement.

The first configuration decision appears in almost every call: state the input dialect and output dialect. parse_one without dialect reads the project's superset grammar, and Expression.sql() without dialect emits its default SQL. That can accept vendor syntax under the wrong assumptions or generate the wrong identifier quotes and function names. transpile returns a list because input may contain several statements. Set unsupported_level=RAISE when a lossy translation must stop a job rather than log a warning and continue.

The default install needs no compiler. The sqlglot[c] extra adds a mypyc-compiled build and may compile from source when a wheel is unavailable. The README reports a speed gain for that route, but custom Dialect subclasses may stop working with compiled classes. The rs extra adds sqlglotrs as well. Choose speed or runtime subclassing deliberately, then benchmark the actual queries. Type-sensitive rewrites need a schema passed into qualification or annotation; those passes stay off by default because they add cost and can only infer what the supplied schema describes.

Parsing changes the representation even when no semantic rewrite is requested. Formatting, casing, delimiter choices, and comment positions can move when sql() regenerates text. AST transforms also require care: a callback must return unchanged nodes on its default path, and replacing a node can discard aliases or metadata unless copied. Optimizer output is suitable for analysis and normalization, but pin the exact minor release and regression-test important queries because optimizer behavior is explicitly allowed to break between minor versions.

Patterns

Translate between named SQL dialects transpile-sql

import sqlglot

queries = sqlglot.transpile(
    "SELECT EPOCH_MS(1618088028295)",
    read="duckdb",
    write="hive",
)
print(queries[0])

transpile returns one output string per input statement. Always set read and write when vendor syntax is involved.

Raise when a target lacks a feature reject-lossy-translation

import sqlglot
from sqlglot import ErrorLevel

sqlglot.transpile(
    "SELECT APPROX_DISTINCT(value, 0.1) FROM events",
    read="presto",
    write="hive",
    unsupported_level=ErrorLevel.RAISE,
)

The default can warn and emit a best-effort query. RAISE makes dropped arguments or unsupported constructs visible to a batch job.

Parse one statement into an expression parse-with-dialect

from sqlglot import parse_one

expression = parse_one(
    "SELECT TOP 10 [name] FROM [users]",
    dialect="tsql",
)
print(expression.sql(dialect="postgres"))

A missing source dialect selects SQLGlot's superset grammar. That is convenient for exploration and risky for engine-specific production SQL.

List referenced tables extract-table-references

from sqlglot import exp, parse_one

tree = parse_one(query, dialect="snowflake")
tables = [table.sql(dialect="snowflake") for table in tree.find_all(exp.Table)]

The walk includes tables inside CTEs and subqueries. Decide whether CTE aliases belong in lineage output before storing the result.

Inspect columns and projections extract-column-references

from sqlglot import exp, parse_one

tree = parse_one("SELECT a, b + 1 AS total FROM sales")
columns = [column.name for column in tree.find_all(exp.Column)]
projections = [item.alias_or_name for item in tree.expressions]

An unqualified column name does not identify its source table. Run qualification with a schema for joins or star expansion.

Pretty-print with quoted identifiers format-generated-sql

from sqlglot import parse_one

formatted = parse_one("select a from foo").sql(
    dialect="spark",
    pretty=True,
    identify=True,
)
print(formatted)

Generation can alter case, whitespace, quotes, and comment positions. It preserves query meaning rather than source-text fidelity.

Compose a select expression build-query

from sqlglot import condition, select

query = (
    select("customer_id", "sum(total) AS spend")
    .from_("orders")
    .where(condition("status = 'paid'"))
    .group_by("customer_id")
)
print(query.sql(dialect="postgres"))

Builder methods return expressions suitable for chaining. Parse untrusted values as data in your execution layer instead of concatenating them into SQL fragments.

Transform matching AST nodes rewrite-columns

from sqlglot import exp, parse_one

def mask_email(node):
    if isinstance(node, exp.Column) and node.name == "email":
        return exp.func("SHA256", node.copy())
    return node

rewritten = parse_one("SELECT email, id FROM users").transform(mask_email)
print(rewritten.sql())

Return each unmatched node. A replacement may need copied aliases, comments, or metadata when those belong to the original expression.

Resolve names against a schema qualify-columns

from sqlglot import parse_one
from sqlglot.optimizer.qualify import qualify

schema = {"orders": {"id": "INT", "total": "DECIMAL"}}
tree = qualify(
    parse_one("SELECT id, total FROM orders"),
    schema=schema,
    dialect="postgres",
)
print(tree.sql(dialect="postgres"))

Qualification adds table references and output aliases. Incorrect or incomplete schema data produces incorrect resolution, especially around stars and joins.

Normalize a query with schema types optimize-query

from sqlglot import parse_one
from sqlglot.optimizer import optimize

schema = {"events": {"active": "BOOLEAN", "id": "INT"}}
normalized = optimize(
    parse_one("SELECT id FROM events WHERE 1 = 1 AND active"),
    schema=schema,
    dialect="postgres",
)
print(normalized.sql())

Optimizer output targets machines and may be noisy for people. Snapshot important results because minor releases may change rewrites.

Follow an output column to its source trace-column-lineage

from sqlglot.lineage import lineage

root = lineage(
    "total",
    "SELECT amount * quantity AS total FROM order_items",
    dialect="postgres",
)
for node in root.walk():
    print(node.name)

Provide schema information for stars, ambiguous joins, and type-sensitive cases. A lineage graph is only as accurate as name resolution.

Read structured parser errors handle-parse-error

from sqlglot import parse_one
from sqlglot.errors import ParseError

try:
    parse_one("SELECT * FROM (SELECT id FROM users")
except ParseError as error:
    for item in error.errors:
        print(item["line"], item["col"], item["description"])

Structured positions work well in editors. A clean parse is not proof that a database will accept or execute the statement.

Alternatives

PackageRegistryPick it when
sqlparsePyPIChoose it for statement splitting and lightweight formatting when dialect-aware AST analysis and transpilation are unnecessary
sqlfluffPyPIChoose it for configurable lint rules and automatic style fixes in CI rather than programmatic query rewriting
sqloxidePyPIChoose it when a Rust-backed parser and dictionary-shaped AST matter more than cross-dialect generation and optimizer utilities
ibis-frameworkPyPIChoose it when users should construct analytical queries through a Python dataframe expression API and execute them on supported backends

More data guides

numpy · fsspec · pandas · sqlalchemy · pyarrow · lxml · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.