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.sosqlite3_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.