Skip to content

Add EXQLITE_EXTRA_SRC: statically compile SQLite extensions into the NIF - #355

Open
AlanMcCann wants to merge 1 commit into
elixir-sqlite:mainfrom
AlanMcCann:extra-src-static-extensions
Open

Add EXQLITE_EXTRA_SRC: statically compile SQLite extensions into the NIF#355
AlanMcCann wants to merge 1 commit into
elixir-sqlite:mainfrom
AlanMcCann:extra-src-static-extensions

Conversation

@AlanMcCann

Copy link
Copy Markdown

Why

Run-time loadable extensions don't cover every target: iOS forbids dynamic code loading outright, and some hardened server deployments disable load_extension. Shipping one pre-compiled .so per architecture is also a real maintenance burden. For those cases the natural answer is compiling the extension into the NIF next to the bundled sqlite3.c — SQLite supports this officially via SQLITE_EXTRA_INIT — but the Makefile's source list is currently fixed, so today this requires forking exqlite.

We hit this migrating a production Elixir app (SQLite + sqlite-vec for vector search, including an iOS target) and carried exactly this patch as a vendored fork; upstreaming the generic hook so nobody else has to.

What

EXQLITE_EXTRA_SRC — a space-separated list of C files compiled and linked into the NIF. Combined with the existing EXQLITE_SYSTEM_CFLAGS, an extension registers on every connection with no load_extension anywhere:

config :exqlite,
  force_build: true,
  make_env: %{
    "EXQLITE_EXTRA_SRC" => "/path/to/sqlite-vec.c /path/to/vec_init_shim.c",
    "EXQLITE_SYSTEM_CFLAGS" => "-DSQLITE_CORE=1 -DSQLITE_EXTRA_INIT=exqlite_vec_init"
  }

with an 8-line shim calling sqlite3_auto_extension(sqlite3_vec_init). README section and CHANGELOG entry included.

Design notes

  • No-op when unsetOBJ and the existing pattern rules are byte-identical without the variable.
  • Ignored under EXQLITE_USE_SYSTEM (linking is the system library's business there).
  • Extra objects are namespaced extra_<basename>.o with sources resolved via vpath, so out-of-tree paths (deps, vendored dirs) work. Basename collisions are documented.
  • POSIX make only; Makefile.win untouched (happy to add if you want it).

Verified by execution

Against this branch, consumed as a plain path dep through make_env only:

UPSTREAM EXTRA_SRC PROOF: PASS - sqlite=3.53.4 vec=v0.1.9 cos_orthogonal=1.0 cos_45deg=0.2928932309150696
NO-HOOK REGRESSION CHECK: PASS - sqlite=3.53.4

(orthogonal vectors → cosine distance 1.0; 45° → 1 − 1/√2 — known-answer checks, not just "it loaded"). The same static-link approach (same flags + shim) was additionally verified on iOS: device-SDK static archive builds clean and the query executes correctly in the simulator, which is the platform where this hook is the only option.

Some targets cannot use run-time loadable extensions at all: iOS forbids
dynamic code loading, and hardened deployments often disable load_extension.
Shipping per-architecture .so files is also a maintenance burden that static
compilation removes.

EXQLITE_EXTRA_SRC takes a space-separated list of C files compiled and linked
into the NIF alongside the bundled sqlite3.c. Paired with the existing
EXQLITE_SYSTEM_CFLAGS (-DSQLITE_CORE=1 -DSQLITE_EXTRA_INIT=...), an extension
such as sqlite-vec registers itself on every connection with no load_extension
call anywhere.

- Only applies when compiling the bundled SQLite; ignored under
  EXQLITE_USE_SYSTEM (linking is the system library's business there).
- No-op when the variable is unset: OBJ and the pattern rules are unchanged,
  verified by building and running with and without the hook.
- Extra objects are namespaced as extra_<basename>.o in BUILD, with sources
  resolved via vpath, so out-of-tree paths (deps, vendored dirs) work.
- POSIX make only for now; Makefile.win untouched.

Verified end-to-end against this branch via a path dep and make_env only
(no fork consumer patches): sqlite-vec v0.1.9 statically linked into the
bundled 3.53.4, SELECT vec_distance_cosine(vec_f32('[1,0]'), vec_f32('[0,1]'))
returning 1.0 and the 45-degree case returning 1 - 1/sqrt(2). Also verified on
iOS (device-SDK static archive + execution in the simulator) in the migration
that motivated this, where load_extension was not an option.
@warmwaffles

Copy link
Copy Markdown
Member

At some point I am going to package https://sqlite.org/vec1/doc/trunk/doc/vec1.md with this project once it becomes "offical" and stable.

@warmwaffles warmwaffles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem too insane to me. Tools for people who know the risks they are taking.

I was contemplating bundling sqlite-vec with this project, but I don't know if I can get it to cross compile for all supported precompiled architectures.

@warmwaffles

Copy link
Copy Markdown
Member

Question:

Run-time loadable extensions don't cover every target: iOS forbids
dynamic code loading outright
, and some hardened server deployments
disable load_extension.

How is this possible? This is a precompiled binary or compiled straight from source, how is the "hardened" server deployments disabling load extension @AlanMcCann?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants