Try / SQLGlot in Y minutes
SQLGlot is a no-dependency SQL parser, transpiler, optimizer, and engine. It can be used to format SQL or translate between 20 different dialects like DuckDB, Presto / Trino, Spark / Databricks, Snowflake, and BigQuery.
It aims to read a wide variety of SQL inputs and output syntactically and semantically correct SQL in the targeted dialects.
See the examples below to get started with SQLGlot:
Formatting and Transpiling · Metadata · Parser Errors · Unsupported Errors · Build and Modify SQL · SQL Optimizer · AST Introspection and Diff · Custom Dialects · SQL Execution · Further reading
✨ This is an open source guide. Feel free to improve it!
Formatting and Transpiling
Easily translate from one dialect to another. For example, date/time functions vary between dialects and can be hard to deal with:
import sqlglot
sql = sqlglot.transpile(
"SELECT EPOCH_MS(1618088028295)",
read="duckdb", write="hive"
)[0]
print(sql)
SELECT FROM_UNIXTIME(1618088028295, 'millis')
SQLGlot can even translate custom time formats:
sql = sqlglot.transpile(
"SELECT STRFTIME(x, '%y-%-m-%S')",
read="duckdb", write="hive"
)[0]
print(sql)
SELECT DATE_FORMAT(x, 'yy-M-ss')
As another example, let's suppose that we want to read in a SQL query that contains a CTE and a cast to REAL
, and then transpile it to Spark, which uses backticks for identifiers and FLOAT
instead of REAL
:
sql = """
WITH baz AS (
SELECT a, c FROM foo WHERE a = 1
)
SELECT
f.a, b.b, baz.c,
CAST("b"."a" AS REAL) d
FROM foo f
JOIN bar b ON f.a = b.a
LEFT JOIN baz ON f.a = baz.a
"""
sql = sqlglot.transpile(sql, write="spark", identify=True, pretty=True)[0]
print(sql)
WITH `baz` AS (
SELECT
`a`,
`c`
FROM `foo`
WHERE
`a` = 1
)
SELECT
`f`.`a`,
`b`.`b`,
`baz`.`c`,
CAST(`b`.`a` AS FLOAT) AS `d`
FROM `foo` AS `f`
JOIN `bar` AS `b`
ON `f`.`a` = `b`.`a`
LEFT JOIN `baz`
ON `f`.`a` = `baz`.`a`
Comments are also preserved on a best-effort basis when transpiling SQL code:
sql = """
/* multi
line
comment
*/
SELECT
tbl.cola /* comment 1 */ + tbl.colb /* comment 2 */,
CAST(x AS INT), # comment 3
y -- comment 4
FROM
bar /* comment 5 */,
tbl # comment 6
"""
sql = sqlglot.transpile(sql, read="mysql", pretty=True)[0]
print(sql)
/* multi
line
comment
*/
SELECT
tbl.cola /* comment 1 */ + tbl.colb /* comment 2 */,
CAST(x AS SIGNED), /* comment 3 */
y /* comment 4 */
FROM bar /* comment 5 */, tbl /* comment 6 */
Metadata
You can explore SQL with expression helpers to do things like find columns and tables:
from sqlglot import parse_one, exp
# print all column references (a and b)
for column in parse_one("SELECT a, b + 1 AS c FROM d").find_all(exp.Column):
print(column.alias_or_name)
# find all projections in select statements (a and c)
for select in parse_one("SELECT a, b + 1 AS c FROM d").find_all(exp.Select):
for projection in select.expressions:
print(projection.alias_or_name)
# find all tables (x, y, z)
for table in parse_one("SELECT * FROM x JOIN y JOIN z").find_all(exp.Table):
print(table.name)
a
b
a
c
x
y
z
Read the AST primer to learn more about SQLGlot's internals.
Parser Errors
When the parser detects an error in the syntax, it raises a ParseError
:
sqlglot.transpile("SELECT foo( FROM bar")
Traceback (most recent call last):
File "/sandbox/main.py", line 3, in <module>
sqlglot.transpile("SELECT foo( FROM bar")
Structured syntax errors are accessible for programmatic use:
import pprint
try:
sqlglot.transpile("SELECT foo( FROM bar")
except sqlglot.errors.ParseError as e:
pprint.pprint(e.errors)
[{'col': 20,
'description': 'Expecting )',
'end_context': '',
'highlight': 'bar',
'into_expression': None,
'line': 1,
'start_context': 'SELECT foo( FROM '}]
Unsupported Errors
Presto APPROX_DISTINCT
supports the accuracy argument which is not supported in Hive:
sqlglot.transpile(
"SELECT APPROX_DISTINCT(a, 0.1) FROM foo",
read="presto", write="hive"
)
APPROX_COUNT_DISTINCT does not support accuracy
Build and Modify SQL
SQLGlot supports incrementally building sql expressions:
from sqlglot import select, condition
where = condition("x=1").and_("y=1")
sql = select("*").from_("y").where(where).sql()
print(sql)
SELECT * FROM y WHERE x = 1 AND y = 1
You can also modify a parsed tree:
from sqlglot import parse_one
sql = parse_one("SELECT x FROM y").from_("z").sql()
print(sql)
SELECT x FROM z
There is also a way to recursively transform the parsed tree by applying a mapping function to each tree node:
from sqlglot import exp, parse_one
expression_tree = parse_one("SELECT a FROM x")
def transformer(node):
if isinstance(node, exp.Column) and node.name == "a":
return parse_one("FUN(a)")
return node
transformed_tree = expression_tree.transform(transformer)
sql = transformed_tree.sql()
print(sql)
SELECT FUN(a) FROM x
SQL Optimizer
SQLGlot can rewrite queries into an "optimized" form. It performs a variety of techniques to create a new canonical AST. This AST can be used to standardize queries or provide the foundations for implementing an actual engine. For example:
from sqlglot import parse_one
from sqlglot.optimizer import optimize
print(
optimize(
parse_one("""
SELECT A OR (B OR (C AND D))
FROM x
WHERE Z = date '2021-01-01' + INTERVAL '1' month OR 1 = 0
"""),
schema={"x": {"A": "INT", "B": "INT", "C": "INT", "D": "INT", "Z": "STRING"}}
).sql(pretty=True)
)
SELECT
(
"x"."a" <> 0 OR "x"."b" <> 0 OR "x"."c" <> 0
)
AND (
"x"."a" <> 0 OR "x"."b" <> 0 OR "x"."d" <> 0
) AS "_col_0"
FROM "x" AS "x"
WHERE
CAST("x"."z" AS DATE) = CAST('2021-01-01' AS DATE) + INTERVAL '1' MONTH
AST Introspection and Diff
You can see the AST version of the sql by calling repr
:
from sqlglot import parse_one
print(repr(parse_one("SELECT a + 1 AS z")))
Select(
expressions=[
Alias(
this=Add(
this=Column(
this=Identifier(this=a, quoted=False)),
expression=Literal(this=1, is_string=False)),
alias=Identifier(this=z, quoted=False))])
SQLGlot can calculate the difference between two expressions and output changes in a form of a sequence of actions needed to transform a source expression into a target one:
from sqlglot import diff, parse_one
sqldiff = diff(
parse_one("SELECT a + b, c, d"),
parse_one("SELECT c, a - b, d")
)
print(sqldiff)
[Remove(expression=Add(
this=Column(
this=Identifier(this=a, quoted=False)),
expression=Column(
this=Identifier(this=b, quoted=False)))), Insert(expression=Sub(
this=Column(
this=Identifier(this=a, quoted=False)),
expression=Column(
this=Identifier(this=b, quoted=False)))), Keep(source=Identifier(this=d, quoted=False), target=Identifier(this=d, quoted=False)), Keep(source=Column(
this=Identifier(this=c, quoted=False)), target=Column(
this=Identifier(this=c, quoted=False))), Keep(source=Select(
expressions=[
Add(
this=Column(
this=Identifier(this=a, quoted=False)),
expression=Column(
this=Identifier(this=b, quoted=False))),
Column(
this=Identifier(this=c, quoted=False)),
Column(
this=Identifier(this=d, quoted=False))]), target=Select(
expressions=[
Column(
this=Identifier(this=c, quoted=False)),
Sub(
this=Column(
this=Identifier(this=a, quoted=False)),
expression=Column(
this=Identifier(this=b, quoted=False))),
Column(
this=Identifier(this=d, quoted=False))])), Keep(source=Column(
this=Identifier(this=d, quoted=False)), target=Column(
this=Identifier(this=d, quoted=False))), Keep(source=Identifier(this=c, quoted=False), target=Identifier(this=c, quoted=False)), Keep(source=Identifier(this=b, quoted=False), target=Identifier(this=b, quoted=False)), Keep(source=Identifier(this=a, quoted=False), target=Identifier(this=a, quoted=False)), Keep(source=Column(
this=Identifier(this=a, quoted=False)), target=Column(
this=Identifier(this=a, quoted=False))), Keep(source=Column(
this=Identifier(this=b, quoted=False)), target=Column(
this=Identifier(this=b, quoted=False)))]
See also: Semantic Diff for SQL
Custom Dialects
Dialects can be added by subclassing Dialect
:
from sqlglot import exp
from sqlglot.dialects.dialect import Dialect
from sqlglot.generator import Generator
from sqlglot.tokens import Tokenizer, TokenType
class Custom(Dialect):
class Tokenizer(Tokenizer):
QUOTES = ["'", '"']
IDENTIFIERS = ["`"]
KEYWORDS = {
**Tokenizer.KEYWORDS,
"INT64": TokenType.BIGINT,
"FLOAT64": TokenType.DOUBLE,
}
class Generator(Generator):
TRANSFORMS = {exp.Array: lambda self, e: f"[{self.expressions(e)}]"}
TYPE_MAPPING = {
exp.DataType.Type.TINYINT: "INT64",
exp.DataType.Type.SMALLINT: "INT64",
exp.DataType.Type.INT: "INT64",
exp.DataType.Type.BIGINT: "INT64",
exp.DataType.Type.DECIMAL: "NUMERIC",
exp.DataType.Type.FLOAT: "FLOAT64",
exp.DataType.Type.DOUBLE: "FLOAT64",
exp.DataType.Type.BOOLEAN: "BOOL",
exp.DataType.Type.TEXT: "STRING",
}
print(Dialect["custom"])
<class '__main__.Custom'>
SQL Execution
One can even interpret SQL queries using SQLGlot, where the tables are represented as Python dictionaries. Although the engine is not very fast (it's not supposed to be) and is in a relatively early stage of development, it can be useful for unit testing and running SQL natively across Python objects. Additionally, the foundation can be easily integrated with fast compute kernels (arrow, pandas).
Below is an example showcasing the execution of a SELECT expression that involves aggregations and JOINs:
from sqlglot.executor import execute
tables = {
"sushi": [
{"id": 1, "price": 1.0},
{"id": 2, "price": 2.0},
{"id": 3, "price": 3.0},
],
"order_items": [
{"sushi_id": 1, "order_id": 1},
{"sushi_id": 1, "order_id": 1},
{"sushi_id": 2, "order_id": 1},
{"sushi_id": 3, "order_id": 2},
],
"orders": [
{"id": 1, "user_id": 1},
{"id": 2, "user_id": 2},
],
}
res = execute(
"""
SELECT
o.user_id,
SUM(s.price) AS price
FROM orders o
JOIN order_items i
ON o.id = i.order_id
JOIN sushi s
ON i.sushi_id = s.id
GROUP BY o.user_id
""",
tables=tables
)
print(res)
user_id price
1 4.0
2 3.0
Further reading
See SQLGlot documentation for details.
Toby Mao · original · CC-BY-SA-4.0 · 2024-01-07
Toby Mao