AST compilers
Compilers convert an AST into a DBMS-specific SQL syntax.
Each database has its own compiler, available as the Compiler class.
from ending.db import postgres
compiler = postgres.Compiler(quote=quoting.singlequote)
Compiling a node
To compile an AST Node, use compile().
>>> query = Query("users").columns("username", "password")
>>> compiler.compile(query)
'SELECT username,password FROM users'
In order to be used with f-strings or str(), nodes can be wrapped with a compiler:
>>> compiler.wrap(query)
>>> f"A' UNION {query} -- -"
"A' UNION SELECT username,password FROM users -- -"
String encoding
The quote argument defines how strings should be encoded by the compiler:
>>> condition = Identifier("role") == 'admin'
>>> compiler = mysql.Compiler(quote=quoting.singlequote)
>>> compiler.compile(condition)
"role='admin'"
>>> compiler = mysql.Compiler(quote=quoting.hexadecimal)
>>> compiler.compile(condition)
"role=0x61646d696e"
Notes
Refer to ending.util.quoting to check out every string quoting function.
Development
Node compilation
Compilers have a compile_X() method to compile each node type X. Simply override the method to change the resulting SQL syntax.
To convert the standard SUBSTRING(a, b, c) to SUBSTRING(a FROM b FOR c) (supported by MySQL, for instance), override compile_Substring():
class NoCommaCompiler(mysql.Compiler):
def compile_Substring(self, substring: Substring, s):
if substring.length is not None:
return f"SUBSTRING({substring.string} FROM {substring.start+1} FOR {substring.length})"
else:
return f"SUBSTRING({substring.string} FROM {substring.start+1})"
>>> compiler = NoCommaCompiler(quote=quoting.singlequote)
>>> compiler.compile(Substring(Identifier("password"), 10, 1))
'SUBSTRING(password FROM 11 FOR 1)'
Recursivity
In addition to returning a string directly, compile_X() methods can return another node, which will in its turn get compiled.
As an example, if a target automatically encodes < characters, we can get rid of them by reversing the comparison: a < b becomes b > a. This is handled by the Comparison node. Instead of returning a string directly, we can return another Comparison, which will in turn get compiled.
class NoLessThanCompiler(mysql.Compiler):
def compile_Comparison(self, comparison: Comparison, s: str) -> str:
# Avoid the use of "<" by reversing the comparison
# a < b --> b > a
if comparison.operator == "<":
return Comparison(comparison.right, ">", comparison.left)
return super().compile_Comparison(comparison, s)
>>> compiler = NoLessThanCompiler(quote=quoting.singlequote)
>>> compiler.compile(Identifier("id") < 100)
'100>id'