Skip to content

C ​

From C you can either load the extension at runtime or compile it in (static) — the latter is the most robust option for a service, since there's no loader to enable and nothing on disk to point at.

Dynamic load ​

For an app linking -lsqlite3:

c
#include <sqlite3.h>
#include <stdio.h>

int main(void) {
  sqlite3 *db;
  char *err = 0;
  sqlite3_open("app.db", &db);

  /* Enable the C-API loader just long enough to load ours, then disable it. */
  sqlite3_enable_load_extension(db, 1);
  if (sqlite3_load_extension(db, "dist/rls.so", 0, &err)) {
    fprintf(stderr, "load failed: %s\n", err);
    return 1;
  }
  sqlite3_enable_load_extension(db, 0);

  sqlite3_exec(db,
    "CREATE TABLE documents(id INTEGER PRIMARY KEY, owner_id INT, title TEXT);"
    "INSERT INTO documents(owner_id,title) VALUES (1,'Alice'),(2,'Bob');",
    0, 0, &err);

  /* '' is an escaped single quote inside a C string literal. */
  sqlite3_exec(db,
    "SELECT rls_protect('{\"table\":\"documents\","
    "\"select\":\"owner_id = rls_ctx(''user_id'')\"}')", 0, 0, &err);
  sqlite3_exec(db, "SELECT rls_seal('CHANGE_ME'); SELECT rls_guard();", 0, 0, &err);

  /* Per query — use a prepared statement + sqlite3_bind_text in real code so
     the JSON is a bound parameter, not concatenated into the SQL. */
  sqlite3_exec(db, "SELECT rls_set_context('{\"user_id\":1}')", 0, 0, &err);
  sqlite3_exec(db, "SELECT id, title FROM documents", /* callback */ 0, 0, &err);

  sqlite3_close(db);
  return 0;
}
sh
cc app.c -lsqlite3 -o app

If you compile SQLite into your binary (the amalgamation), skip load_extension entirely and register the init function before opening connections. It then applies to every new connection automatically:

c
extern int sqlite3_rls_init(sqlite3 *, char **, const sqlite3_api_routines *);

int main(void) {
  sqlite3_auto_extension((void (*)(void)) sqlite3_rls_init);
  /* every sqlite3_open() from here on has rls_* available */
  ...
}

Build the extension's src/rls.c into your program alongside the SQLite amalgamation. There's no loader to enable and no .so to ship or locate.

Binding parameters (do this in real code) ​

The examples inline JSON for brevity. In production, bind it:

c
sqlite3_stmt *st;
sqlite3_prepare_v2(db, "SELECT rls_set_context(?1)", -1, &st, 0);
sqlite3_bind_text(st, 1, "{\"user_id\":42}", -1, SQLITE_TRANSIENT);
sqlite3_step(st);
sqlite3_finalize(st);

Notes ​

  • Entry point. SQLite derives the init symbol from the filename: rls.so → sqlite3_rls_init. If you rename the file, pass the entry point as the third argument to sqlite3_load_extension.
  • Return codes. A denied base-table read returns SQLITE_AUTH (23); a policy violation on write returns SQLITE_CONSTRAINT via RAISE(ABORT). Check sqlite3_exec/sqlite3_step return codes.
  • Re-disabling sqlite3_enable_load_extension after loading closes the C-API path; the authorizer additionally blocks the load_extension() SQL function once guarded.

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