5 ms·
Show HN: SQL-tString a t-string SQL builder in Python
SQL-tString is a SQL builder that utilises the recently accepted PEP-750, https://peps.python.org/pep-0750/ https://peps.python.org/pep-0750/, t-strings to build SQL queries, for example,
from sql_tstring import sql
val = 2
query, values = sql(t"SELECT x FROM y WHERE x = {val}")
assert query == "SELECT x FROM y WHERE x = ?"
assert values == [2]
db.execute(query, values) # Most DB engines support this
The placeholder ? protects against SQL injection, but cannot be used everywhere. For example, a column name cannot be a placeholder. If you try this SQL-tString will raise an error,
col = "x"
sql(t"SELECT {col} FROM y") # Raises ValueError
To proceed you'll need to declare what the valid values of col can be,
from sql_tstring import sql_context
with sql_context(columns="x"):
query, values = sql(t"SELECT {col} FROM y")
assert query == "SELECT x FROM y"
assert values == []
Thus allowing you to protect against SQL injection.
As t-strings are format strings you can safely format the literals you'd like to pass as variables,
text = "world"
query, values = sql(t"SELECT x FROM y WHERE x LIKE '%{text}'")
assert query == "SELECT x FROM y WHERE x LIKE ?"
assert values == ["%world"]
This is especially useful when used with the Absent rewriting value.
SQL-tString is a SQL builder and as such you can use special RewritingValues to alter and build the query you want at runtime. This is best shown by considering a query you sometimes want to search by one column a, sometimes by b, and sometimes both,
def search(
*,
a: str | AbsentType = Absent,
b: str | AbsentType = Absent
) -> tuple[str, list[str]]:
return sql(t"SELECT x FROM y WHERE a = {a} AND b = {b}")
assert search() == "SELECT x FROM y", []
assert search(a="hello") == "SELECT x FROM y WHERE a = ?", ["hello"]
assert search(b="world") == "SELECT x FROM y WHERE b = ?", ["world"]
assert search(a="hello", b="world") == (
"SELECT x FROM y WHERE a = ? AND b = ?", ["hello", "world"]
)
Specifically Absent (which is an alias of RewritingValue.ABSENT) will remove the expression it is present in, and if there an no expressions left after the removal it will also remove the clause.
The other rewriting values I've included are handle the frustrating case of comparing to NULL, for example the following is valid but won't work as you'd likely expect,
optional = None
sql(t"SELECT x FROM y WHERE x = {optional}")
Instead you can use IsNull to achieve the right result,
from sql_tstring import IsNull
optional = IsNull
query, values = sql(t"SELECT x FROM y WHERE x = {optional}")
assert query == "SELECT x FROM y WHERE x IS NULL"
assert values == []
There is also a IsNotNull for the negated comparison.
The final feature allows for complex query building by nesting a t-string within the existing,
inner = t"x = 'a'"
query, _ = sql(t"SELECT x FROM y WHERE {inner}")
assert query == "SELECT x FROM y WHERE x = 'a'"
This library can be used today without Python3.14's t-strings with some limitations, https://github.com/pgjones/sql-tstring?tab=readme-ov-file#pre-python-314-usage https://github.com/pgjones/sql-tstring?tab=readme-ov-file#pr..., and I've been doing so this year. Thoughts and feedback very welcome.
- zenethian 1y ago[flagged]
- deleted 1y ago[deleted]
- schultzer 1y agoNot really sure what a t string is or if it’s a macro, but feel similar to https://github.com/elixir-dbvisor/sql https://github.com/elixir-dbvisor/sql but less elegant and ergonomic.
- 1_08iu 1y agot-strings (or template strings) are an upcoming Python 3.14 feature. They have similar syntax to f-strings (which were introduced in 3.6) except that they provide access to the string and the interpolated values (the bits inside the curly brackets) before they have been combined. Previously, something like db.query(f"SELECT * FROM table WHERE id={id};") would have been vulnerable to the classic "bobby tables" SQL injection but t-strings allow for almost the same syntax (which is quite natural for Python programmers) without incurring a security risk. If you are curious, t-strings have previously been discussed here (https://news.ycombinator.com/item?id=43748512 https://news.ycombinator.com/item?id=43748512 and https://news.ycombinator.com/item?id=43647716 https://news.ycombinator.com/item?id=43647716) and you can read the PEP that proposed their addition to the language (https://peps.python.org/pep-0750/ https://peps.python.org/pep-0750/).
- deleted 1y ago[deleted]
- sirfz 1y agoTechnically this enforces parameterized queries since all it does is basically return the parameterized query in the given db client dialect and the list of parameters. It could be useful for building a unified interface for all sql db clients for example (instead of having to remember whether parameters are %s or ? or {param}, etc). On the other hand, db clients can utilize t-strings to directly allow you to safely execute queries such as t"select * from table where id = {id}" without passing any extra parameters.