SQLGlot est un analyseur, un transpileur, un optimiseur et un moteur SQL sans dépendance. Il peut être utilisé pour formater SQL ou traduire entre 24 dialectes différents comme DuckDB, Presto/Trino, Spark/Databricks, Snowflake et BigQuery. Il vise à lire une grande variété d’entrées SQL et à produire du SQL syntaxiquement et sémantiquement correct dans les dialectes ciblés.
Il s'agit d'un analyseur SQL générique très complet avec une suite de tests robuste. Il est également assez performant, tout en étant écrit uniquement en Python.
Vous pouvez facilement personnaliser l'analyseur, analyser les requêtes, parcourir les arborescences d'expressions et créer SQL par programme.
Les erreurs de syntaxe sont mises en évidence et les incompatibilités dialectales peuvent avertir ou se déclencher selon les configurations. Cependant, SQLGlot n'a pas pour objectif d'être un validateur SQL, il peut donc ne pas détecter certaines erreurs de syntaxe.
Apprenez-en davantage sur SQLGlot dans la documentation de l’API et dans l’introduction à l’arborescence d’expression.
Les contributions sont les bienvenues dans SQLGlot ; lisez le guide de contribution et le document d’intégration pour commencer !
Depuis PyPI :
pip3 install " sqlglot[rs] "
# Without Rust tokenizer (slower):
# pip3 install sqlglot
Ou avec une caisse locale :
make install
Conditions requises pour le développement (facultatif) :
make install-dev
Étant donné un numéro de version MAJOR
. MINOR
. PATCH
, SQLGlot utilise la stratégie de versioning suivante :
PATCH
est incrémentée lorsqu'il existe des correctifs rétrocompatibles ou des ajouts de fonctionnalités.MINOR
est incrémentée en cas de correctifs rétrocompatibles ou d'ajouts de fonctionnalités.MAJOR
est incrémentée lorsqu'il existe des correctifs importants incompatibles avec les versions antérieures ou des ajouts de fonctionnalités. Nous aimerions avoir de vos nouvelles. Rejoignez notre chaîne communautaire Slack !
J'ai essayé d'analyser du SQL qui devrait être valide mais cela a échoué, pourquoi est-ce arrivé ?
parse_one(sql, dialect="spark")
(alternativement : read="spark"
). Si aucun dialecte n'est spécifié, parse_one
tentera d'analyser la requête selon le « dialecte SQLGlot », qui est conçu pour être un surensemble de tous les dialectes pris en charge. Si vous avez essayé de spécifier le dialecte et que cela ne fonctionne toujours pas, veuillez signaler un problème.J'ai essayé de générer du SQL mais ce n'est pas dans le bon dialecte !
parse_one(sql, dialect="spark").sql(dialect="duckdb")
(ou : transpile(sql, read="spark", write="duckdb")
).J'ai essayé d'analyser du SQL invalide et cela a fonctionné, même si cela devrait générer une erreur ! Pourquoi n'a-t-il pas validé mon SQL ?
Qu'est-il arrivé à sqlglot.dataframe ?
Traduisez facilement d'un dialecte à un autre. Par exemple, les fonctions date/heure varient selon les dialectes et peuvent être difficiles à gérer :
import sqlglot
sqlglot . transpile ( "SELECT EPOCH_MS(1618088028295)" , read = "duckdb" , write = "hive" )[ 0 ]
' SELECT FROM_UNIXTIME(1618088028295 / POW(10, 3)) '
SQLGlot peut même traduire des formats d'heure personnalisés :
import sqlglot
sqlglot . transpile ( "SELECT STRFTIME(x, '%y-%-m-%S')" , read = "duckdb" , write = "hive" )[ 0 ]
" SELECT DATE_FORMAT(x, 'yy-M-ss') "
Les délimiteurs d’identifiants et les types de données peuvent également être traduits :
import sqlglot
# Spark SQL requires backticks (`) for delimited identifiers and uses `FLOAT` over `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"""
# Translates the query into Spark SQL, formats it, and delimits all of its identifiers
print ( sqlglot . transpile ( sql , write = "spark" , identify = True , pretty = True )[ 0 ])
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 `
Les commentaires sont également conservés dans la mesure du possible :
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
"""
# Note: MySQL-specific comments (`#`) are converted into standard syntax
print ( sqlglot . transpile ( sql , read = 'mysql' , pretty = True )[ 0 ])
/* 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 */
Vous pouvez explorer SQL avec des assistants d'expression pour effectuer des opérations telles que rechercher des colonnes et des tables dans une requête :
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 )
Lisez la dernière introduction pour en savoir plus sur les composants internes de SQLGlot.
Lorsque l'analyseur détecte une erreur dans la syntaxe, il génère un ParseError
:
import sqlglot
sqlglot . transpile ( "SELECT foo FROM (SELECT baz FROM t" )
sqlglot.errors.ParseError: Expecting ). Line 1, Col: 34.
SELECT foo FROM (SELECT baz FROM t
~
Les erreurs de syntaxe structurée sont accessibles pour une utilisation programmatique :
import sqlglot
try :
sqlglot . transpile ( "SELECT foo FROM (SELECT baz FROM t" )
except sqlglot . errors . ParseError as e :
print ( e . errors )
[{
'description' : 'Expecting )' ,
'line' : 1 ,
'col' : 34 ,
'start_context' : 'SELECT foo FROM (SELECT baz FROM ' ,
'highlight' : 't' ,
'end_context' : '' ,
'into_expression' : None
}]
Il se peut qu'il ne soit pas possible de traduire certaines requêtes entre certains dialectes. Dans ces cas, SQLGlot peut émettre un avertissement et procédera par défaut à une traduction au mieux :
import sqlglot
sqlglot . transpile ( "SELECT APPROX_DISTINCT(a, 0.1) FROM foo" , read = "presto" , write = "hive" )
APPROX_COUNT_DISTINCT does not support accuracy
' SELECT APPROX_COUNT_DISTINCT(a) FROM foo '
Ce comportement peut être modifié en définissant l'attribut unsupported_level
. Par exemple, nous pouvons le définir sur RAISE
ou IMMEDIATE
pour garantir qu'une exception soit déclenchée à la place :
import sqlglot
sqlglot . transpile ( "SELECT APPROX_DISTINCT(a, 0.1) FROM foo" , read = "presto" , write = "hive" , unsupported_level = sqlglot . ErrorLevel . RAISE )
sqlglot.errors.UnsupportedError: APPROX_COUNT_DISTINCT does not support accuracy
Certaines requêtes nécessitent que des informations supplémentaires soient transpilées avec précision, telles que les schémas des tables qui y sont référencées. En effet, certaines transformations sont sensibles au type, ce qui signifie que l'inférence de type est nécessaire pour comprendre leur sémantique. Même si les règles d'optimisation qualify
et annotate_types
peuvent aider à cela, elles ne sont pas utilisées par défaut car elles ajoutent une surcharge et une complexité significatives.
La transpilation est généralement un problème difficile, c'est pourquoi SQLGlot utilise une approche « incrémentale » pour le résoudre. Cela signifie qu'il peut y avoir des paires de dialectes qui ne prennent pas actuellement en charge certaines entrées, mais cela devrait s'améliorer avec le temps. Nous apprécions grandement les problèmes ou PR bien documentés et testés, alors n'hésitez pas à nous contacter si vous avez besoin de conseils !
SQLGlot prend en charge la création incrémentielle d'expressions SQL :
from sqlglot import select , condition
where = condition ( "x=1" ). and_ ( "y=1" )
select ( "*" ). from_ ( "y" ). where ( where ). sql ()
' SELECT * FROM y WHERE x = 1 AND y = 1 '
Il est possible de modifier un arbre analysé :
from sqlglot import parse_one
parse_one ( "SELECT x FROM y" ). from_ ( "z" ). sql ()
' SELECT x FROM z '
Les expressions analysées peuvent également être transformées de manière récursive en appliquant une fonction de mappage à chaque nœud de l'arborescence :
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 )
transformed_tree . sql ()
' SELECT FUN(a) FROM x '
SQLGlot peut réécrire les requêtes sous une forme « optimisée ». Il exécute diverses techniques pour créer un nouvel AST canonique. Cet AST peut être utilisé pour standardiser les requêtes ou fournir les bases de la mise en œuvre d'un véritable moteur. Par exemple:
import sqlglot
from sqlglot . optimizer import optimize
print (
optimize (
sqlglot . 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-02-01 ' AS DATE )
Vous pouvez voir la version AST du SQL analysé en appelant 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 peut calculer la différence sémantique entre deux expressions et afficher les modifications sous la forme d'une séquence d'actions nécessaires pour transformer une expression source en une expression cible :
from sqlglot import diff , parse_one
diff ( parse_one ( "SELECT a + b, c, d" ), parse_one ( "SELECT c, a - b, d" ))
[
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 = Column ( this = Identifier ( this = a , quoted = False )),
target = Column ( this = Identifier ( this = a , quoted = False ))),
...
]
Voir aussi : Diff sémantique pour SQL.
Des dialectes peuvent être ajoutés en sous-classant 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'>
SQLGlot est capable d'interpréter les requêtes SQL, où les tables sont représentées sous forme de dictionnaires Python. Le moteur n'est pas censé être rapide, mais il peut être utile pour les tests unitaires et l'exécution native de SQL sur des objets Python. De plus, la fondation peut être facilement intégrée à des noyaux de calcul rapides, tels que Arrow et Pandas.
L'exemple ci-dessous présente l'exécution d'une requête qui implique des agrégations et des jointures :
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 },
],
}
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
)
user_id price
1 4.0
2 3.0
Voir aussi : Écrire un moteur Python SQL à partir de zéro.
SQLGlot utilise pdoc pour servir sa documentation API.
Une version hébergée se trouve sur le site Web SQLGlot, ou vous pouvez construire localement avec :
make docs-serve
make style # Only linter checks
make unit # Only unit tests (or unit-rs, to use the Rust tokenizer)
make test # Unit and integration tests (or test-rs, to use the Rust tokenizer)
make check # Full test suite & linter checks
Les benchmarks s'exécutent sur Python 3.10.12 en quelques secondes.
Requête | sqlglot | sqlglotrs | sqlfluff | arbre SQL | sqlparse | moz_sql_parser | sqloxyde |
---|---|---|---|---|---|---|---|
tpch | 0,00944 (1,0) | 0,00590 (0,625) | 0,32116 (33,98) | 0,00693 (0,734) | 0,02858 (3,025) | 0,03337 (3,532) | 0,00073 (0,077) |
court | 0,00065 (1,0) | 0,00044 (0,687) | 0,03511 (53,82) | 0,00049 (0,759) | 0,00163 (2,506) | 0,00234 (3,601) | 0,00005 (0,073) |
long | 0,00889 (1,0) | 0,00572 (0,643) | 0,36982 (41,56) | 0,00614 (0,690) | 0,02530 (2,844) | 0,02931 (3,294) | 0,00059 (0,066) |
fou | 0,02918 (1,0) | 0,01991 (0,682) | 1,88695 (64,66) | 0,02003 (0,686) | 7,46894 (255,9) | 0,64994 (22,27) | 0,00327 (0,112) |
SQLGlot utilise dateutil pour simplifier les expressions timedelta littérales. L'optimiseur ne simplifiera pas les expressions comme celles-ci si le module est introuvable :
x + interval ' 1 ' month