Dragon

SQL and URL Templates

The markup content types escape text so it can't break out of a document. The last two content types do something more interesting: SQL turns interpolations into bound parameters rather than text, and URL percent-encodes for the query string. Both close an injection class by construction.

SQL - interpolations become bound parameters

This is the one that surprises people. In a template[SQL], a !{} interpolation does not paste the value into the query text. It adds a bound parameter - a placeholder in the SQL, with the value carried alongside, separately:

from database import SQL

id: int = 5
q: SQL = template[SQL] {
  select name from users where id = !{id}
}
print(q.to_str())
# select name from users where id = $$0

Notice the output: !{id} became $$0, a parameter placeholder - not the literal 5. The value 5 is held by the SQL object, bound to that slot. When you execute the query through the database module, the driver sends the SQL text and the parameter values as two separate things, so a value can never be interpreted as SQL. Each !{} becomes the next placeholder, in order:

from database import SQL

name: str = "Ada"
active: bool = true
q: SQL = template[SQL] {
  select * from users where name = !{name} and active = !{active}
}
# → ... where name = $$0 and active = $$1, with name and active bound

This is structurally stronger than escaping. A classic injection payload - a name of '; DROP TABLE users; -- - can't do anything, because it's never part of the query text; it's a bound value compared against the name column. You get parameterized queries for free, with the readability of an inline literal:

# This looks like string interpolation, but it is a safe, parameterized query:
q: SQL = template[SQL] { select * from orders where customer = !{user_id} }

You build SQL values with template[SQL]; you execute them - db.all(q), db.one(q), db.run(q) - in the Databases chapter, which covers connections, result handling, and transactions.

Two limits to know inside template[SQL]: each !{} must be a single bound expression - the block forms from the previous chapter (!{ for ... }, !{ if ... }) are not supported here - and you cannot splice one SQL value into another (nesting a SQL interpolation is a compile error). Both are rejected at build time, not silently miscompiled.

URL - percent-encoding for query strings

URL escapes interpolated values for a URL query string: spaces, &, =, ?, #, and + are percent-encoded so a value can't add a second parameter or otherwise alter the query's structure:

from urllib.parse import URL

query: str = "a b&c"
link: URL = template[URL] {
  https://example.com/search?q=!{query}
}
print(link.to_str())
# https://example.com/search?q=a%20b%26c

The space becomes %20 and the & becomes %26, so the interpolated query stays a single query-string value instead of injecting a second &c parameter. The literal structure of the URL - the scheme, host, path, and the ?q= you wrote - passes through untouched.

URL's built-in escape targets the characters that matter most in a query string; it does not percent-encode every RFC 3986 reserved character (/, :, ;, ,, and @ pass through). When you need to encode a value fully - everything outside the unreserved set - use the | url filter (!{value | url}) or urllib.parse.quote(value, ""), both of which encode /, :, ;, ,, and the rest:

from urllib.parse import quote

print(quote("a/b;c", ""))   # a%2Fb%3Bc

At a glance

Content typeImportWhat !{x} doesFor
SQLfrom database import SQLbecomes a bound parameter ($$0, $$1, …)safe, parameterized queries
URLfrom urllib.parse import URLpercent-encodedsafe query-string values

Together with HTML/CSS/XML and the generic template system, these give you typed, injection-safe text generation for every layer of a web service - page, query, and link. That closes Part 12. Executing the SQL values you just built is the subject of the next part: Databases.