Quill uses a whitebox macro to encode type-level information about the original AST of a quotation. This mechanism allows Quill to generate queries at compile time, providing quick feedback to the user about the final SQL query and almost zero runtime overhead. It also opens the path for more advanced features like compile-time query probing. Example:
When testDB.run
is called, the macro only knows that a term q
is being used. The macro system doesn’t provide a way to allow inspection of the original AST of q
. To workaround this limitation, Quill encodes the original AST information as a type annotation of the type refinement generated by the quote
macro.
To exemplify, this quotation:
val q = quote(1)
is expanded to:
val q = new Quoted[Int] {
@QuotedAst(Constant(1))
def ast = Constant(1)
}
When q
is used within another quotation, Quill obtains the QuotedAst
annotation from the term type and is able to expand the original AST locally.
Note that this approach has an important limitation. If the user uses type widening:
val q: Quoted[Int] = quote(1)
the type refinement information is lost and Quill has to fall back to runtime query generation using the ast
method.
I’d say that this usage of whitebox macros is a workaround and could be better handled by the inline
keyword initially proposed with the new macros system. Regardless of type widening, the user could declare quotations as inline values:
inline val q: Quoted[Int] = quoted(1)
and wherever this value is used, the tree quoted(1)
is expanded locally, giving access to the original AST.
Is inline
still being considered? I’ve heard different answers from different people about this feature.