L'auteur de Pony ORM est ici.
Pony traduit le générateur Python en requête SQL en trois étapes:
- Décompilation du bytecode du générateur et reconstruction du générateur AST (arbre de syntaxe abstraite)
- Traduction de Python AST en "SQL abstrait" - représentation universelle basée sur une liste d'une requête SQL
- Conversion d'une représentation SQL abstraite en dialecte SQL spécifique dépendant de la base de données
La partie la plus complexe est la deuxième étape, où Pony doit comprendre la «signification» des expressions Python. Il semble que vous soyez le plus intéressé par la première étape, alors laissez-moi vous expliquer comment fonctionne la décompilation.
Considérons cette requête:
>>> from pony.orm.examples.estore import *
>>> select(c for c in Customer if c.country == 'USA').show()
Ce qui sera traduit dans le SQL suivant:
SELECT "c"."id", "c"."email", "c"."password", "c"."name", "c"."country", "c"."address"
FROM "Customer" "c"
WHERE "c"."country" = 'USA'
Et ci-dessous est le résultat de cette requête qui sera imprimée:
id|email |password|name |country|address
--+-------------------+--------+--------------+-------+---------
1 |john@example.com |*** |John Smith |USA |address 1
2 |matthew@example.com|*** |Matthew Reed |USA |address 2
4 |rebecca@example.com|*** |Rebecca Lawson|USA |address 4
La select()
fonction accepte un générateur python comme argument, puis analyse son bytecode. Nous pouvons obtenir les instructions bytecode de ce générateur en utilisant le dis
module python standard :
>>> gen = (c for c in Customer if c.country == 'USA')
>>> import dis
>>> dis.dis(gen.gi_frame.f_code)
1 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 26 (to 32)
6 STORE_FAST 1 (c)
9 LOAD_FAST 1 (c)
12 LOAD_ATTR 0 (country)
15 LOAD_CONST 0 ('USA')
18 COMPARE_OP 2 (==)
21 POP_JUMP_IF_FALSE 3
24 LOAD_FAST 1 (c)
27 YIELD_VALUE
28 POP_TOP
29 JUMP_ABSOLUTE 3
>> 32 LOAD_CONST 1 (None)
35 RETURN_VALUE
Pony ORM a la fonction decompile()
dans le module pony.orm.decompiling
qui peut restaurer un AST à partir du bytecode:
>>> from pony.orm.decompiling import decompile
>>> ast, external_names = decompile(gen)
Ici, nous pouvons voir la représentation textuelle des nœuds AST:
>>> ast
GenExpr(GenExprInner(Name('c'), [GenExprFor(AssName('c', 'OP_ASSIGN'), Name('.0'),
[GenExprIf(Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))]))])]))
Voyons maintenant comment decompile()
fonctionne la fonction.
La decompile()
fonction crée un Decompiler
objet qui implémente le modèle Visiteur. L'instance du décompilateur reçoit les instructions de bytecode une par une. Pour chaque instruction, l'objet décompilateur appelle sa propre méthode. Le nom de cette méthode est égal au nom de l'instruction de bytecode actuelle.
Lorsque Python calcule une expression, il utilise stack, qui stocke un résultat intermédiaire de calcul. L'objet décompilateur a également sa propre pile, mais cette pile ne stocke pas le résultat du calcul de l'expression, mais le nœud AST pour l'expression.
Lorsque la méthode de décompilation pour l'instruction de bytecode suivante est appelée, elle prend les nœuds AST de la pile, les combine dans un nouveau nœud AST, puis place ce nœud au sommet de la pile.
Par exemple, voyons comment la sous c.country == 'USA'
- expression est calculée. Le fragment de bytecode correspondant est:
9 LOAD_FAST 1 (c)
12 LOAD_ATTR 0 (country)
15 LOAD_CONST 0 ('USA')
18 COMPARE_OP 2 (==)
Ainsi, l'objet décompilateur effectue les opérations suivantes:
- Appels
decompiler.LOAD_FAST('c')
. Cette méthode place le Name('c')
nœud au sommet de la pile du décompilateur.
- Appels
decompiler.LOAD_ATTR('country')
. Cette méthode prend le Name('c')
nœud de la pile, crée le Geattr(Name('c'), 'country')
nœud et le place au sommet de la pile.
- Appels
decompiler.LOAD_CONST('USA')
. Cette méthode place le Const('USA')
nœud au-dessus de la pile.
- Appels
decompiler.COMPARE_OP('==')
. Cette méthode prend deux nœuds (Getattr et Const) de la pile, puis les place Compare(Getattr(Name('c'), 'country'), [('==', Const('USA'))])
en haut de la pile.
Une fois toutes les instructions de bytecode traitées, la pile de décompilateur contient un seul nœud AST qui correspond à l'expression entière du générateur.
Puisque Pony ORM doit décompiler uniquement les générateurs et les lambdas, ce n'est pas si complexe, car le flux d'instructions pour un générateur est relativement simple - il ne s'agit que d'un tas de boucles imbriquées.
Actuellement, Pony ORM couvre l'ensemble des instructions du générateur, à l'exception de deux choses:
- Inline if expressions:
a if b else c
- Comparaisons composées:
a < b < c
Si Pony rencontre une telle expression, il lève l' NotImplementedError
exception. Mais même dans ce cas, vous pouvez le faire fonctionner en passant l'expression du générateur sous forme de chaîne. Lorsque vous passez un générateur sous forme de chaîne, Pony n'utilise pas le module décompilateur. Au lieu de cela, il obtient l'AST en utilisant la compiler.parse
fonction Python standard .
J'espère que ça répond à ta question.
p
objet est un objet d'un type mis en œuvre par Pony qui se penche sur les méthodes / propriétés sont accessibles à ce sujet (par exemplename
,startswith
) et les convertit en SQL.