SQLite Basics: Register a Small Python Function for SQL Queries
Import reports sometimes need one normalization rule in both Python and SQL. Register a small scalar function on the connection so a query can apply the same rule to each value without embedding application code in the database file.
Prepare a scratch directory
Tested on Linux with CPython 3.12.14 and SQLite 3.53.1. You need Python 3.11 or newer with SQLite support, Bash, a text editor, and standard mktemp, rm, and rmdir utilities. No third-party packages are required. Run Python without -O so the checks remain enabled.
Keep the same terminal open. Stop if setup fails, before saving the source:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Run the example
Save this as normalize-sql-labels.py:
import sqlite3
def normalized_label(value):
if value is None:
return None
if not isinstance(value, str):
raise TypeError("label must be text or NULL")
return " ".join(value.split()).casefold()
connection = sqlite3.connect(":memory:")
try:
connection.create_function("normalized_label", 1, normalized_label,
deterministic=True)
connection.execute("CREATE TABLE imports (id INTEGER PRIMARY KEY, label TEXT)")
connection.executemany("INSERT INTO imports VALUES (?, ?)",
[(1, " Blue Pen "), (2, "Straße"), (3, None)])
rows = connection.execute(
"SELECT id, normalized_label(label) FROM imports ORDER BY id"
).fetchall()
assert rows == [(1, "blue pen"), (2, "strasse"), (3, None)]
for item_id, label in rows:
print(f"{item_id}: {label!r}")
other = sqlite3.connect(":memory:")
try:
try:
other.execute("SELECT normalized_label('Blue')")
except sqlite3.OperationalError as error:
assert "no such function" in str(error)
print("Other connection: function is not registered")
else:
raise AssertionError("Function unexpectedly exists")
finally:
other.close()
finally:
connection.close()Run it:
python3 -B normalize-sql-labels.pyObserved output:
1: 'blue pen'
2: 'strasse'
3: None
Other connection: function is not registeredApply the result
SQLite calls the Python function once for each value it needs to evaluate. SQL NULL arrives as Python None; returning None produces NULL. The normalization collapses whitespace and uses Unicode case folding, which is stronger than merely making letters lowercase. It does not perform Unicode normalization or establish that two labels identify the same person or product.
The second connection cannot call the function until it registers its own implementation. Registration lives on the connection, not in a database file. Mark a function deterministic only when equal arguments produce equal results. Avoid callbacks that depend on the current time, mutable external state, or network responses.
This example uses trusted SQL and a database it creates itself. Keep callbacks free of filesystem or network side effects, especially when an application opens database files from other parties. A callback exception is reported through SQLite rather than preserving an ordinary Python call stack at the query site.
See the official reference for the API and its constraints. The output above comes from this controlled fixture.
Clean up
Remove the saved source, return to the starting directory, then remove the empty practice directory:
rm -- normalize-sql-labels.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf the directory is not empty, inspect its contents before removing anything else.
Acknowledgements
This post was created with assistance from AI.