Using python 3.14 template strings as an easier way to provide parameter bindings #593
Replies: 11 comments 15 replies
-
|
String templates are a really bad match for SQL, and even worse for SQLite. A completely safe Pythonic equivalent is: Traditional networked databases usually had the client driver send a SQL string, so even if the developer used bindings, the driver made a SQL string. SQLite doesn't have a layer like that and has to parse out the stringized values back to their originals anyway, so the templates are extra work to compose and decompose the SQL string. And it defeats the statement cache so will be less efficient again. The above example also doesn't handle strings or blobs correctly, and it means you can't use APSW's JSONB and convert binding support. And just for completeness, it won't handle strings with embedded nulls which SQLite and APSW handle just fine. You can only use bindings for SQL values, and cannot use them for identifiers like table and column names. That is a situation where you do have to use string interpolation or templating, but usually at that point you are doing something similar to an ORM and so can handle it correctly. |
Beta Was this translation helpful? Give feedback.
-
Depends on the usage. I think string template is perfect for writing parameter binding.
I don't understand how it defeats the statement cache since it is one layer above apsw.
It does actually. conn = Connection(':memory:')
r = conn.execute(t'select null is {None}').fetchone()
print(r)
r = conn.execute(t"select 'a' = {'a'}").fetchone()
print(r)
r = conn.execute(t'select null is not {None}').fetchone()
print(r)
r = conn.execute(t"select 'a' <> {'a'}").fetchone()
print(r)
conn.convert_binding = lambda c, n, v: apsw.jsonb_encode(v)
conn.convert_jsonb = lambda c, l, v: apsw.jsonb_decode(v)
conn.execute('create table t (j jsonb)')
conn.execute(t'insert into t values ({[1, 2, 3]})')
r = conn.execute('select * from t').fetchone()
print(r, type(r[0]))I feel that you might be mixing string template with python string format. |
Beta Was this translation helpful? Give feedback.
-
|
Also for blob conn.execute('create table t2 (b blob)')
conn.execute(t'insert into t2 values ({b"\0\1\2"})')
r = conn.execute('select * from t2').fetchone()
print(r) |
Beta Was this translation helpful? Give feedback.
-
Because the SQL strings after templates have been expanded is what the statement cache sees. So these would be two different entries: Whereas this would not be: As for strings and blobs, show the results of: s = "Hello "world", how's y'all doing?" |
Beta Was this translation helpful? Give feedback.
-
|
String template does not expand the parameter. >>> a = 1
>>> t'{a} + 2'
Template(strings=('', ' + 2'), interpolations=(Interpolation(1, 'a', None, ''),))>>> a = 1
>>> b = None
>>> c = [4, 5, 6]
>>> t = t'a = {a}, b = {b}, c = {c}'
>>> t.strings
('a = ', ', b = ', ', c = ', '')
>>> t.values
(1, None, [4, 5, 6]) |
Beta Was this translation helpful? Give feedback.
-
|
Let me explain a bit what python 3.14 string template is. It transforms t'prefix {variable} suffix' into a list of strings and values from locals() strings: ['prefix ', ' suffix''] The values keep their original type, so this is why string template is made perfectly for writing sql with parameters, we can simply concat the strings with |
Beta Was this translation helpful? Give feedback.
-
|
My mistake - I rewrote the code above to clarify what it really happening: Result And it will generate invalid SQL if used for identifiers so can't be unsafe. |
Beta Was this translation helpful? Give feedback.
-
Well, my post title is using template strings for parameter bindings, and table name is not a binding. |
Beta Was this translation helpful? Give feedback.
-
|
I might also note that there is The first one is string template while the later is called string template literal. I might have wrongly used the wrong term, missing the literal suffix. |
Beta Was this translation helpful? Give feedback.
-
|
For things like table name, we can use the format spec to tag the value as a sql fragment class Connection(apsw.Connection):
def execute(self, sql, params=None, *args, **kwargs):
if isinstance(sql, Template):
_sql = ''
params = []
for v in sql:
if isinstance(v, str):
_sql += v
else:
if v.format_spec == 'sql':
_sql += v.value
else:
_sql += '?'
params.append(v.value)
sql = _sql
return super().execute(sql, params, *args, **kwargs)
conn.execute('create table t (x int)')
table = 't'
conn.execute(t'select * from {table:sql}') |
Beta Was this translation helpful? Give feedback.
-
|
A bigger picture question is how should raw SQL be handled in Python applications. In my view the majority of devs avoid it completely and use an ORM like SQLAlchemy. FastAPI and Pydantic have been doing well in the Python ecosystem recently and have a corresponding SQLModel. In both cases they map Python objects to SQL tables, and have a LINQ like query scheme, and then do the corresponding SQL behind the scenes. What I do like is the approach from yesql which has you put the queries in a separate text file with a comment to name each one. aiosql provides an implementation for Python. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I really enjoyed writing sql with parameter bindings using javascript tagged template, and now we can also use the same syntax in python 3.14.
Beta Was this translation helpful? Give feedback.
All reactions