feat: implement templating system based on tstrings#409
Conversation
|
@evertlammerts I would love to get a "going in the right direction" confirmation before I commit much more time on this. If you want, I could do a google meet to chat about this live, if that would make it easier for you to digest what this does, and we can chat about assumptions, tradeoffs, etc. Thank you! |
Catalog::GetCatalog/GetSystemCatalog were only available transitively before the DuckDB submodule bump in duckdb#532. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
CI keeps failing because duckdb/duckdb is changing underneath us. This passes locally for me. See #543 as a fix for this. |
|
Hey Nick, thanks for the work. Right now we're preparing DuckDB 2.0, and as part of that we're doing a large rework of duckdb's python bindings. The Relational API will be overhauled and parameter binding as well. I'm hoping to merge it in August, well in time before to prep for the 2.0 release. Until then I probably won't be merging contributions that add new features. |
|
Thanks for the realistic timeline. Will the Relational and parameter API changes be breaking? If so, and there are opportunities to align it for this future direction, I would love to be tagged into those relevant PRs so I can advise. This PR doesn't really depend on the relational API very much, but this DOES depend on the parameter API. Eg this PR adds a whole Parameter dataclass and a Other side comment if you are changing APIs, consider this PR's proposal of |
This PR implements a SQL templating system for duckdb-python based on Python t-strings (PEP 750). Discussion started in #370
It is still a WIP, but it is now good enough that I think the shape of the API is 90% there. I wanted to open up this PR so @evertlammerts can take a look at it and give some high level comments. Once we iron the large scale things out then I can fix those up and do more polishing before I ask you for a more detailed review.
Simple Example
This is one of the most basic use cases, but I imagine also would be one of the more popular:
More complete example
Types from the duckdb package, such as DuckdbPyRelation, datatype constants, expressions, etc, are converted to SQL and params correctly. This allows you to easily build up analysis chains, for easily commenting out individual lines or re-ordering, common to analysis:
When passed to conn.execute(), this will be compiled into the SQL
and the params
{"min_age": config.min_age}A template is just sequence of interleaving str's and param-ish things
Using the t-string literal syntax of
t"foo{bar}"is simply just syntactic sugar for python 3.14+.For older versions of python, or for programmatic construction, or for any other reason, you can create a SqlTemplate object with the
duckdb.template()function:There is a well-defined lifecycle of IntoSqlTemplate -> SqlTemplate -> ResolvedSqlTemplate -> CompiledSql
IntoSqlTemplateis a type alias that represents anything that can be interpeted as a SqlTemplate byduckdb.template(). Concretely:Most users will just go straight from
IntoSqlTemplatestraight to executing, but the intermediate processing is well defined, and a public API, and the user can get in the middle of the process and view/modify the intermediate data structures.A
SqlTemplateis what you get back from theduckdb.template()function. It is quite similar to the actualstring.templatelib.Templatebuilt in to python 3.14. It contains a sequence of str's and Interpolation objects, just like string.templatelib.Template. This is potentially nested, with the Interpolation objects containing other SqlTemplates, Params, str, or anything else. It contains an additional.resolve()method that recursively resolves all the inner Interpolations into str's and Params, resulting in aResolvedSqlTemplate.A
ResolvedSqlTemplateis semantically aSequence[str | Param]. The actual final param IDs haven't been resolved yet, but any nesting has been flattened. You can call theSqlTemplate.compile()method to combine adjacent str's and to resolve the param IDs to their final form, resulting in aCompiledSqlobject.The final object is a
CompiledSqlobject, which is just a simple dataclass likeCompiledSql(sql: str, params: dict[str, object]). This gets used asconn.execute(compiled.sql, compiled.params).Param details
By default, we generate IDs for the anonymous params.
If you want more control over the naming, you can explicitly create a Param object.
which is just a simple dataclass:
You can also create this with the
duckdb.param()factory function:Build Higher-Order components using
__duckdb_template__()We define a Protocol that makes an implementor become compatible with
duckdb.tempate():Here is an example usage where someone can define
Behavior notes:
__duckdb_template__()protocol accept unused kwarg args for future-proofing. If in the future we decide that we need to pass config or other metadata during the compilation step, we can start passing that in, and all the handlers won't break.Query API integration
The main SQL entry points (
sql,query,from_query,execute,executemany) now accept template-ish inputs (SqlTemplate/CompiledSql) in addition to existing string/statement inputs. This behavior could use some careful thought, as this is one of the biggest ways we are painting ourselves into a corner for future API changes, or we cause footguns from unexpected behavior. Everyone already uses these APIs, and always will..sql/.params, those are consumed directly..compile(), it is compiled before statement parsing.This allows passing compiled/template objects directly into execution/query paths without requiring manual
.sql/.paramsunpacking by users.I'm not sure if we should be EVEN more coercive, and support accepting any of the things that
template()accepts. Eg should we acceptconn.sql(["SELECT * FROM users WHERE id = ", 123])?Testing
Adds broad coverage across: