Chapter 14 · Full-Text Search, R-Tree, Virtual Tables, and Extensions
Loadable Extensions, Built-In Extensions, and Supply-Chain Safety
Treat SQLite extensions as executable capabilities with explicit trust boundaries, detect built-in modules before depending on them, and design a safe, portable extension policy for applications and deployments.
Learning outcomes
SQLite can be extended with functions, collations, virtual-table modules, and even VFS implementations. That power means extension loading belongs in the software supply-chain and security model. The final lesson separates capabilities compiled into your SQLite library from native code loaded at runtime, then builds a repeatable capability checklist before Chapter 15 moves into application drivers.
Distinguish built-in, statically linked, registered, and runtime-loadable SQLite capabilities.
Explain why native extension loading is disabled by default in the core application API.
Use .load and application-level loading only as explicitly trusted deployment operations.
Inventory FTS5, RTree, dbstat, session, CSV/other extension capabilities without promising they exist everywhere.
Identify portability and packaging costs introduced by non-standard extensions.
Produce a capability-detection and supply-chain checklist suitable for an application startup/deployment review.
“Extension” can mean several deployment shapes
| Capability shape | How code reaches the process | Example | Operational implication |
|---|---|---|---|
| Core/built into SQLite library | Compiled into the library | JSON support in modern SQLite | Travels with that library build. |
| Optional feature compiled in | Compile-time option or source integration | FTS5/RTree/dbstat/session depending on build | Must verify the actual host build. |
| Statically linked extension | Application/library link step | Official extension source integrated into product | Versioned and reviewed with the application binary. |
| Runtime loadable extension | Shared library/DLL loaded after connection opens | Approved custom module | Native code executes in-process; packaging/signing/provenance matter. |
Runtime extension loading is code execution
A SQLite loadable extension is a native shared library: .dll on Windows, usually .so on Linux/Unix, and .dylib on macOS. Its initialization routine can register SQL functions, collations, virtual tables, or VFSes. If the binary is malicious or compromised, SQL sandboxing cannot make its native machine code harmless.
Never teach or automate “download whatever DLL fixes no such module.” Use an approved source/build pipeline, pin versions/hashes or signatures where your organization supports them, match CPU/OS/runtime architecture, and treat extension updates like application dependencies.
Why application APIs disable loading by default
SQLite's core documentation says extension loading is turned off by default for security. An application must explicitly enable it before using the C loading interface or SQL load_extension() function. This default reduces the chance that untrusted SQL can cause the process to execute arbitrary native libraries.
/* Preferred application decision point: enable extension loading only when the deployment policy explicitly requires it. */sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 1, NULL);/* Then load only an approved extension path/entry point. */sqlite3_load_extension(db, approved_path, NULL, &errmsg);/* Disable again when the loading window is over. */sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, 0, NULL);Higher-level drivers expose different wrappers—or none at all. Chapter 15 will treat driver behavior as its own contract rather than assuming the C API surface is available unchanged.
The sqlite3 CLI intentionally behaves differently
The official command-line shell enables extension loading as part of its setup, so its .load dot-command can load a trusted extension without an extra enable step. That convenience is for an interactive tool; do not infer that an application connection should enable loading globally.
-- Dot-command, not SQL:.load ./approved_extension-- Optional non-default entry point:.load ./approved_extension custom_entry_pointThe course intentionally does not provide an arbitrary extension download URL. The safe lesson is the loading mechanism and trust process, not “run this unknown binary.”
Capability detection comes before loading
Many useful features may already be available in the exact SQLite library. Loading duplicate or unnecessary native code expands deployment risk without benefit.
SELECT sqlite_version() AS sqlite_version;SELECT nameFROM pragma_module_listWHERE name IN ( 'fts5','fts5vocab','rtree','rtree_i32','dbstat', 'json_each','json_tree')ORDER BY name;SELECT compile_optionsFROM pragma_compile_optionsWHERE compile_options LIKE '%FTS%' OR compile_options LIKE '%RTREE%' OR compile_options LIKE '%DBSTAT%' OR compile_options LIKE '%SESSION%' OR compile_options LIKE '%OMIT_LOAD_EXTENSION%'ORDER BY compile_options;Survey official/bundled capabilities without promising universality
| Capability | How it may be delivered | What it is for | Detection idea |
|---|---|---|---|
| FTS5 | Built/compiled into many SQLite libraries; can also be built as extension | Full-text search | module_list + controlled CREATE VIRTUAL TABLE probe |
| RTree | Compile-time extension in many builds | Multidimensional range/bounding boxes | module_list for rtree/rtree_i32 |
| dbstat | Requires SQLITE_ENABLE_DBSTAT_VTAB | Read-only page/storage introspection | module_list / compile_options |
| session | Optional extension/compile capability | Changesets/session tracking | compile_options/API capability; not exposed as a normal SQL table by default |
| CSV | Official extension source exists, often not built into generic host libraries | Expose CSV as virtual table | Do not assume present; package explicitly if your application needs it |
Optional local lab: prove loading is disabled before trusting anything
Python's standard sqlite3 wrapper may expose enable_load_extension() when its underlying library supports loadable extensions. This lab does not load a binary. It only records runtime capabilities and demonstrates the application-side control surface.
import sqlite3con = sqlite3.connect(":memory:")print("SQLite:", sqlite3.sqlite_version)print("modules:", [r[0] for r in con.execute("PRAGMA module_list")])print("has enable_load_extension:", hasattr(con, "enable_load_extension"))# Do not call load_extension() with an unreviewed path.con.close()On the generation environment, Python is linked to SQLite 3.46.1 and exposes enable_load_extension(); its module list already includes FTS5, RTree, dbstat, JSON traversal modules, and others, so no external binary is needed for this chapter's labs.
Portability cost: the database file can outlive the extension environment
An ordinary SQLite database file is famously portable, but a schema can contain virtual tables whose modules are unavailable on another host. Opening the file may still be possible, yet statements touching those objects can fail because the module is missing. A deployment package therefore needs both the database schema and the executable capabilities it depends on.
| Dependency question | What to record |
|---|---|
| Which SQLite library actually runs? | Runtime version/source id and driver package version. |
| Which modules must exist? | FTS5/RTree/custom module names and feature options. |
| How are they supplied? | Built-in, statically linked, or approved runtime shared library. |
| Which platforms are supported? | OS, CPU architecture, ABI/toolchain constraints. |
| How is integrity/provenance verified? | Controlled build source, package signature/hash policy, release process. |
| What is the fallback? | Disable optional feature, migrate schema, or fail startup clearly—never silently corrupt semantics. |
Do not let SQL choose arbitrary filesystem paths
If application users can submit SQL, leaving the SQL load_extension() function enabled can turn SQL input into native-library loading. SQLite's explicit enablement model exists for a reason. Keep extension policy in trusted application/deployment code, not in an arbitrary user query channel.
Enable runtime loading only for the narrowest trusted initialization window, use allowlisted paths/artifacts, load what is required, then disable loading again where the driver/API permits. If runtime loading is unnecessary, leave it disabled.
Failure cases and diagnosis
| Symptom | Likely class of cause | Safe next step |
|---|---|---|
| no such module: fts5 | Host library lacks/has not registered FTS5 | Inspect sqlite_version, module_list, compile options, and driver bundling. |
| not authorized / extension loading disabled | API deliberately blocks loading | Decide whether product policy actually permits native extensions; do not bypass casually. |
| cannot open shared object / DLL | Path, packaging, architecture, dependency issue | Verify approved artifact path, CPU/OS ABI, dependent libraries, and permissions. |
| undefined symbol / entry point | Extension/runtime ABI or entry-point mismatch | Use the extension's documented build/entry point for the target SQLite/platform. |
| Works in CLI, fails in app | Different SQLite library/build or loading policy | Compare runtime versions/modules inside each process, not installed file names. |
Production capability checklist
Extension readiness review
Use this as a deployment gate before Chapter 15 application integration.
- Have we recorded the SQLite library version that the application process actually uses?
- Are every required virtual-table/function capabilities detected at startup or during compatibility tests?
- Is runtime extension loading actually necessary, or can approved capabilities be built/static-linked instead?
- If loading is necessary, are artifacts provenance-controlled, versioned, architecture-matched, and allowlisted?
- Does untrusted SQL remain unable to select arbitrary extension paths?
- Have CI/release tests opened representative databases on every supported runtime and exercised extension-dependent features?
Review the answers
A production-ready design knows the exact runtime, required capabilities, delivery method, trust policy, and compatibility test matrix. Runtime loading should be a deliberate deployment decision, not a query-time convenience. If a feature is optional, failure behavior should be explicit; if it is required, startup should fail clearly rather than operating with changed semantics.
Chapter recap: SQLite's extension model has three responsibilities
Semantic responsibility: know what the module actually indexes or exposes. Operational responsibility: maintain structures such as external-content FTS indexes and benchmark their costs. Security responsibility: treat native extensions as executable dependencies with a supply chain. With those boundaries established, Chapter 15 can safely move from shell labs into application drivers, connections, prepared statements, and binding.