Skip to content

Python

Python's standard-library sqlite3 module can load extensions on most builds — no third-party driver needed.

Load and set up

python
import json, os, sqlite3

con = sqlite3.connect("app.db")
con.enable_load_extension(True)
con.load_extension("dist/rls")      # no ".so" — the loader appends it
con.enable_load_extension(False)    # re-disable

con.executescript("""
  CREATE TABLE documents(id INTEGER PRIMARY KEY, owner_id INT NOT NULL, title TEXT);
  INSERT INTO documents(owner_id, title) VALUES (1,'Alice'), (2,'Bob');
""")

con.execute("SELECT rls_protect(?)", (json.dumps({
    "table": "documents",
    "select": "owner_id = rls_ctx('user_id')",
    "insert": "NEW.owner_id = rls_ctx('user_id')",
}),))
con.execute("SELECT rls_config('resolver', ?)",
            ("SELECT id AS user_id, role FROM _rls_users WHERE token = ?1",))
con.execute("SELECT rls_seal(?)", (os.environ["RLS_TOKEN"],))
con.execute("SELECT rls_guard()")

Serve queries

python
def set_user(claims):
    con.execute("SELECT rls_set_context(?)", (json.dumps(claims),))

set_user({"user_id": 1, "role": "user"})
print(con.execute("SELECT title FROM documents").fetchall())   # [('Alice',)]

# or resolve from the store:
con.execute("SELECT rls_auth(?)", ("the-session-token",))
print(con.execute("SELECT title FROM documents").fetchall())

A tiny per-request helper

python
def query_as(claims, sql, params=()):
    con.execute("SELECT rls_set_context(?)", (json.dumps(claims),))
    return con.execute(sql, params).fetchall()

query_as({"user_id": 2}, "SELECT title FROM documents")         # [('Bob',)]

If extension loading is unavailable

Some Python builds bundle a SQLite compiled with SQLITE_OMIT_LOAD_EXTENSION; then enable_load_extension raises AttributeError or OperationalError. The official python.org builds and most Linux distribution packages support it — use one of those. You can check quickly:

python
import sqlite3
print(hasattr(sqlite3.connect(":memory:"), "enable_load_extension"))  # True == OK

Notes

  • Write results: cursor.rowcount is 0 for writes through a policy view. Use RETURNING ... and fetchall(), or SELECT rls_last_rowid() after an insert. See the caveats.
  • With a pool or per-thread connections, run the setup (load + protect + config + seal + guard) once per connection and bind the context per query.
  • sqlite3 autocommit/transaction behavior is unchanged — the context can switch between statements inside a transaction, just like any other driver.

A loadable SQLite extension. No warranty; test against your own schema.