dirsql 0.3.21__tar.gz → 0.3.22__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dirsql-0.3.21 → dirsql-0.3.22}/Cargo.lock +1 -1
- {dirsql-0.3.21 → dirsql-0.3.22}/Cargo.toml +4 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/PKG-INFO +1 -1
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/cli/config.md +27 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.21/packages/rust → dirsql-0.3.22/packages/python}/docs/cli/config.md +27 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/Cargo.toml +1 -1
- {dirsql-0.3.21/packages/python → dirsql-0.3.22/packages/rust}/docs/cli/config.md +27 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/config.rs +137 -2
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/db.rs +96 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/lib.rs +96 -0
- dirsql-0.3.22/packages/rust/tests/extensions.rs +247 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/README.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/_async.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/_async_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/binary_path_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/dispatch_extract.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/dispatch_extract_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/load_app.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/load_app_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/run.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/run_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/write_message.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/write_message_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/is_windows_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/main_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/py.typed +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/resolve_config.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/resolve_config_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/AGENTS.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/api/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/cli/http-api.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/cli/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/cli/init.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/cli/server.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/getting-started.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/async.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/querying.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/tables.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/watching.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/migrations.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/package.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/README.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/conftest.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/api/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/cli/http-api.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/data/a/meta.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/data/b/meta.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/dirsql.config.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/interpret/data/a/meta.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/interpret/data/b/meta.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_no_app.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_raises.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/interpret_subprocess.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_async_dirsql.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_binding.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_dirsql.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_docs_examples.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_docs_gaps.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_from_config.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_interpret.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_native_config.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_persist.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_serialization.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/README.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/api/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/cli/http-api.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/native_config.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/async_sdk.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/cli_e2e.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/cli_integration.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/code_review_findings.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/docs_examples.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/docs_gaps.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/from_config.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/init_e2e.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/init_integration.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/persist.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/readonly_query.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/sdk.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/serialization.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.22}/pyproject.toml +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
[workspace]
|
|
2
2
|
members = ["packages/python", "packages/rust"]
|
|
3
|
+
# Test-only loadable-extension fixture, built on demand by
|
|
4
|
+
# packages/rust/tests/extensions.rs. Excluded so it isn't built/linted by
|
|
5
|
+
# `cargo build --workspace` / `cargo clippy --workspace`.
|
|
6
|
+
exclude = ["packages/rust/tests/fixtures/testext"]
|
|
3
7
|
resolver = "2"
|
|
4
8
|
|
|
5
9
|
[workspace.package]
|
|
@@ -138,6 +138,33 @@ persist = true
|
|
|
138
138
|
See [Persistence](../guide/persistence.md) for the full reconcile algorithm,
|
|
139
139
|
storage layout, and limitations.
|
|
140
140
|
|
|
141
|
+
## Loading extensions
|
|
142
|
+
|
|
143
|
+
You can load SQLite extensions by specifying them in a config.
|
|
144
|
+
|
|
145
|
+
Declare each extension as a `[[dirsql.extension]]` entry:
|
|
146
|
+
|
|
147
|
+
```toml
|
|
148
|
+
[[dirsql.extension]]
|
|
149
|
+
path = "./ext/myext.dylib"
|
|
150
|
+
entrypoint = "sqlite3_myext_init"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **`path`** — a path to the extension's shared library (`.so` / `.dylib` /
|
|
154
|
+
`.dll`). Relative paths resolve against the config file's parent directory.
|
|
155
|
+
- **`entrypoint`** *(optional)* — the extension's init symbol. When omitted,
|
|
156
|
+
SQLite derives a default from the filename; set it when that default does not
|
|
157
|
+
match (for example, `sqlite-vec`'s entry point is `sqlite3_vec_init`).
|
|
158
|
+
|
|
159
|
+
**Note**: `dirsql` enables extension loading only while loading the configured libraries,
|
|
160
|
+
then disables it again, so `load_extension()` is not exposed via SQL to the user.
|
|
161
|
+
|
|
162
|
+
Extensions add **functions** you can call in queries and in a regular table's
|
|
163
|
+
DDL (defaults, generated columns). An extension-backed **virtual table** cannot
|
|
164
|
+
be declared as a `[[table]]` — `dirsql` tables are per-file row tables — so a
|
|
165
|
+
`CREATE VIRTUAL TABLE` DDL is rejected; call the extension's functions in your
|
|
166
|
+
queries instead.
|
|
167
|
+
|
|
141
168
|
## Strict Mode
|
|
142
169
|
|
|
143
170
|
By default, auto-injected virtuals that aren't in the DDL are silently
|
|
@@ -4,7 +4,7 @@ name = "dirsql-py-ext"
|
|
|
4
4
|
# pypi/maturin handler can rewrite it via `write-version` before
|
|
5
5
|
# `maturin build`. `pyproject.toml` declares `dynamic = ["version"]`
|
|
6
6
|
# and maturin reads this field. Mirrors `packages/rust/Cargo.toml`.
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.22"
|
|
8
8
|
edition.workspace = true
|
|
9
9
|
publish = false
|
|
10
10
|
readme = "README.md"
|
|
@@ -138,6 +138,33 @@ persist = true
|
|
|
138
138
|
See [Persistence](../guide/persistence.md) for the full reconcile algorithm,
|
|
139
139
|
storage layout, and limitations.
|
|
140
140
|
|
|
141
|
+
## Loading extensions
|
|
142
|
+
|
|
143
|
+
You can load SQLite extensions by specifying them in a config.
|
|
144
|
+
|
|
145
|
+
Declare each extension as a `[[dirsql.extension]]` entry:
|
|
146
|
+
|
|
147
|
+
```toml
|
|
148
|
+
[[dirsql.extension]]
|
|
149
|
+
path = "./ext/myext.dylib"
|
|
150
|
+
entrypoint = "sqlite3_myext_init"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **`path`** — a path to the extension's shared library (`.so` / `.dylib` /
|
|
154
|
+
`.dll`). Relative paths resolve against the config file's parent directory.
|
|
155
|
+
- **`entrypoint`** *(optional)* — the extension's init symbol. When omitted,
|
|
156
|
+
SQLite derives a default from the filename; set it when that default does not
|
|
157
|
+
match (for example, `sqlite-vec`'s entry point is `sqlite3_vec_init`).
|
|
158
|
+
|
|
159
|
+
**Note**: `dirsql` enables extension loading only while loading the configured libraries,
|
|
160
|
+
then disables it again, so `load_extension()` is not exposed via SQL to the user.
|
|
161
|
+
|
|
162
|
+
Extensions add **functions** you can call in queries and in a regular table's
|
|
163
|
+
DDL (defaults, generated columns). An extension-backed **virtual table** cannot
|
|
164
|
+
be declared as a `[[table]]` — `dirsql` tables are per-file row tables — so a
|
|
165
|
+
`CREATE VIRTUAL TABLE` DDL is rejected; call the extension's functions in your
|
|
166
|
+
queries instead.
|
|
167
|
+
|
|
141
168
|
## Strict Mode
|
|
142
169
|
|
|
143
170
|
By default, auto-injected virtuals that aren't in the DDL are silently
|
|
@@ -53,7 +53,7 @@ futures-channel.workspace = true
|
|
|
53
53
|
globset = "0.4"
|
|
54
54
|
notify = "7"
|
|
55
55
|
regex.workspace = true
|
|
56
|
-
rusqlite = { version = "0.34", features = ["bundled"] }
|
|
56
|
+
rusqlite = { version = "0.34", features = ["bundled", "load_extension"] }
|
|
57
57
|
serde.workspace = true
|
|
58
58
|
serde_json.workspace = true
|
|
59
59
|
thiserror.workspace = true
|
|
@@ -138,6 +138,33 @@ persist = true
|
|
|
138
138
|
See [Persistence](../guide/persistence.md) for the full reconcile algorithm,
|
|
139
139
|
storage layout, and limitations.
|
|
140
140
|
|
|
141
|
+
## Loading extensions
|
|
142
|
+
|
|
143
|
+
You can load SQLite extensions by specifying them in a config.
|
|
144
|
+
|
|
145
|
+
Declare each extension as a `[[dirsql.extension]]` entry:
|
|
146
|
+
|
|
147
|
+
```toml
|
|
148
|
+
[[dirsql.extension]]
|
|
149
|
+
path = "./ext/myext.dylib"
|
|
150
|
+
entrypoint = "sqlite3_myext_init"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **`path`** — a path to the extension's shared library (`.so` / `.dylib` /
|
|
154
|
+
`.dll`). Relative paths resolve against the config file's parent directory.
|
|
155
|
+
- **`entrypoint`** *(optional)* — the extension's init symbol. When omitted,
|
|
156
|
+
SQLite derives a default from the filename; set it when that default does not
|
|
157
|
+
match (for example, `sqlite-vec`'s entry point is `sqlite3_vec_init`).
|
|
158
|
+
|
|
159
|
+
**Note**: `dirsql` enables extension loading only while loading the configured libraries,
|
|
160
|
+
then disables it again, so `load_extension()` is not exposed via SQL to the user.
|
|
161
|
+
|
|
162
|
+
Extensions add **functions** you can call in queries and in a regular table's
|
|
163
|
+
DDL (defaults, generated columns). An extension-backed **virtual table** cannot
|
|
164
|
+
be declared as a `[[table]]` — `dirsql` tables are per-file row tables — so a
|
|
165
|
+
`CREATE VIRTUAL TABLE` DDL is rejected; call the extension's functions in your
|
|
166
|
+
queries instead.
|
|
167
|
+
|
|
141
168
|
## Strict Mode
|
|
142
169
|
|
|
143
170
|
By default, auto-injected virtuals that aren't in the DDL are silently
|
|
@@ -13,6 +13,9 @@ pub enum ConfigError {
|
|
|
13
13
|
|
|
14
14
|
#[error("Missing required field '{0}' in [[table]] entry")]
|
|
15
15
|
MissingField(&'static str),
|
|
16
|
+
|
|
17
|
+
#[error("Missing required field '{0}' in [[dirsql.extension]] entry")]
|
|
18
|
+
MissingExtensionField(&'static str),
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
pub type Result<T> = std::result::Result<T, ConfigError>;
|
|
@@ -33,6 +36,29 @@ pub struct Config {
|
|
|
33
36
|
/// Optional override for the on-disk cache location. Resolved relative
|
|
34
37
|
/// to the config file's parent directory when relative.
|
|
35
38
|
pub persist_path: Option<PathBuf>,
|
|
39
|
+
/// SQLite extensions to load at startup, declared via
|
|
40
|
+
/// `[[dirsql.extension]]`. Paths are taken verbatim from the file here;
|
|
41
|
+
/// relative paths are resolved against the config file's parent directory
|
|
42
|
+
/// by the caller (`DirSQLBuilder::resolve`).
|
|
43
|
+
pub extensions: Vec<ExtensionSpec>,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// A SQLite extension to load at startup.
|
|
47
|
+
///
|
|
48
|
+
/// Declared as a `[[dirsql.extension]]` array entry. dirsql loads each
|
|
49
|
+
/// extension onto the connection before any `CREATE TABLE` runs, then
|
|
50
|
+
/// disables loading again so the SQL `load_extension()` function is never
|
|
51
|
+
/// left exposed.
|
|
52
|
+
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
|
53
|
+
pub struct ExtensionSpec {
|
|
54
|
+
/// Local path to the extension's shared library (`.so` / `.dylib` /
|
|
55
|
+
/// `.dll`). Relative paths resolve against the config file's parent
|
|
56
|
+
/// directory.
|
|
57
|
+
pub path: PathBuf,
|
|
58
|
+
/// Optional init-symbol override. When `None`, SQLite derives the entry
|
|
59
|
+
/// point from the filename, which often does not match — set this when
|
|
60
|
+
/// the extension's init function isn't `sqlite3_<filename>_init`.
|
|
61
|
+
pub entrypoint: Option<String>,
|
|
36
62
|
}
|
|
37
63
|
|
|
38
64
|
/// Configuration for a single table.
|
|
@@ -65,6 +91,13 @@ struct RawDirsql {
|
|
|
65
91
|
ignore: Option<Vec<String>>,
|
|
66
92
|
persist: Option<bool>,
|
|
67
93
|
persist_path: Option<PathBuf>,
|
|
94
|
+
extension: Option<Vec<RawExtension>>,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[derive(Deserialize)]
|
|
98
|
+
struct RawExtension {
|
|
99
|
+
path: Option<String>,
|
|
100
|
+
entrypoint: Option<String>,
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
#[derive(Deserialize)]
|
|
@@ -84,16 +117,31 @@ pub fn load_config(path: &Path) -> Result<Config> {
|
|
|
84
117
|
pub fn load_config_str(content: &str) -> Result<Config> {
|
|
85
118
|
let raw: RawConfig = toml::from_str(content)?;
|
|
86
119
|
|
|
87
|
-
let (root, ignore, persist, persist_path) = match raw.dirsql {
|
|
120
|
+
let (root, ignore, persist, persist_path, raw_extensions) = match raw.dirsql {
|
|
88
121
|
Some(d) => (
|
|
89
122
|
d.root,
|
|
90
123
|
d.ignore.unwrap_or_default(),
|
|
91
124
|
d.persist.unwrap_or(false),
|
|
92
125
|
d.persist_path,
|
|
126
|
+
d.extension.unwrap_or_default(),
|
|
93
127
|
),
|
|
94
|
-
None => (None, Vec::new(), false, None),
|
|
128
|
+
None => (None, Vec::new(), false, None, Vec::new()),
|
|
95
129
|
};
|
|
96
130
|
|
|
131
|
+
let mut extensions = Vec::with_capacity(raw_extensions.len());
|
|
132
|
+
for raw_ext in raw_extensions {
|
|
133
|
+
// An empty `path = ""` is as unusable as a missing key: reject it at
|
|
134
|
+
// parse time rather than silently resolving it to a directory later.
|
|
135
|
+
let path = raw_ext
|
|
136
|
+
.path
|
|
137
|
+
.filter(|p| !p.is_empty())
|
|
138
|
+
.ok_or(ConfigError::MissingExtensionField("path"))?;
|
|
139
|
+
extensions.push(ExtensionSpec {
|
|
140
|
+
path: PathBuf::from(path),
|
|
141
|
+
entrypoint: raw_ext.entrypoint,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
97
145
|
let raw_tables = raw.table.unwrap_or_default();
|
|
98
146
|
let mut tables = Vec::with_capacity(raw_tables.len());
|
|
99
147
|
|
|
@@ -114,6 +162,7 @@ pub fn load_config_str(content: &str) -> Result<Config> {
|
|
|
114
162
|
tables,
|
|
115
163
|
persist,
|
|
116
164
|
persist_path,
|
|
165
|
+
extensions,
|
|
117
166
|
})
|
|
118
167
|
}
|
|
119
168
|
|
|
@@ -341,4 +390,90 @@ format = "json"
|
|
|
341
390
|
let config = load_config_str(toml).unwrap();
|
|
342
391
|
assert_eq!(config.tables.len(), 1);
|
|
343
392
|
}
|
|
393
|
+
|
|
394
|
+
#[test]
|
|
395
|
+
fn extensions_parse_path_and_entrypoint() {
|
|
396
|
+
let toml = r#"
|
|
397
|
+
[[dirsql.extension]]
|
|
398
|
+
path = "./ext/vec0.dylib"
|
|
399
|
+
entrypoint = "sqlite3_vec_init"
|
|
400
|
+
|
|
401
|
+
[[table]]
|
|
402
|
+
ddl = "CREATE TABLE t (_path TEXT)"
|
|
403
|
+
glob = "*.json"
|
|
404
|
+
"#;
|
|
405
|
+
let config = load_config_str(toml).unwrap();
|
|
406
|
+
assert_eq!(config.extensions.len(), 1);
|
|
407
|
+
assert_eq!(config.extensions[0].path, PathBuf::from("./ext/vec0.dylib"));
|
|
408
|
+
assert_eq!(
|
|
409
|
+
config.extensions[0].entrypoint.as_deref(),
|
|
410
|
+
Some("sqlite3_vec_init")
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#[test]
|
|
415
|
+
fn extension_entrypoint_is_optional() {
|
|
416
|
+
let toml = r#"
|
|
417
|
+
[[dirsql.extension]]
|
|
418
|
+
path = "ext.so"
|
|
419
|
+
"#;
|
|
420
|
+
let config = load_config_str(toml).unwrap();
|
|
421
|
+
assert_eq!(config.extensions.len(), 1);
|
|
422
|
+
assert!(config.extensions[0].entrypoint.is_none());
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
#[test]
|
|
426
|
+
fn extension_missing_path_errors() {
|
|
427
|
+
let toml = r#"
|
|
428
|
+
[[dirsql.extension]]
|
|
429
|
+
entrypoint = "sqlite3_x_init"
|
|
430
|
+
"#;
|
|
431
|
+
let err = load_config_str(toml).unwrap_err();
|
|
432
|
+
assert!(
|
|
433
|
+
matches!(err, ConfigError::MissingExtensionField("path")),
|
|
434
|
+
"got: {err:?}"
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[test]
|
|
439
|
+
fn extensions_default_empty_when_absent() {
|
|
440
|
+
let toml = r#"
|
|
441
|
+
[[table]]
|
|
442
|
+
ddl = "CREATE TABLE t (_path TEXT)"
|
|
443
|
+
glob = "*.json"
|
|
444
|
+
"#;
|
|
445
|
+
let config = load_config_str(toml).unwrap();
|
|
446
|
+
assert!(config.extensions.is_empty());
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#[test]
|
|
450
|
+
fn multiple_extensions_preserve_order() {
|
|
451
|
+
let toml = r#"
|
|
452
|
+
[[dirsql.extension]]
|
|
453
|
+
path = "a.so"
|
|
454
|
+
|
|
455
|
+
[[dirsql.extension]]
|
|
456
|
+
path = "b.so"
|
|
457
|
+
"#;
|
|
458
|
+
let config = load_config_str(toml).unwrap();
|
|
459
|
+
assert_eq!(config.extensions.len(), 2);
|
|
460
|
+
assert_eq!(config.extensions[0].path, PathBuf::from("a.so"));
|
|
461
|
+
assert_eq!(config.extensions[1].path, PathBuf::from("b.so"));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
#[test]
|
|
465
|
+
fn extension_empty_path_errors() {
|
|
466
|
+
// An empty `path = ""` is as unusable as a missing key — it must be
|
|
467
|
+
// rejected at parse time, not silently accepted and later resolved to
|
|
468
|
+
// the config's parent directory. (RED for #225 review finding #4.)
|
|
469
|
+
let toml = r#"
|
|
470
|
+
[[dirsql.extension]]
|
|
471
|
+
path = ""
|
|
472
|
+
"#;
|
|
473
|
+
let err = load_config_str(toml).unwrap_err();
|
|
474
|
+
assert!(
|
|
475
|
+
matches!(err, ConfigError::MissingExtensionField("path")),
|
|
476
|
+
"got: {err:?}"
|
|
477
|
+
);
|
|
478
|
+
}
|
|
344
479
|
}
|
|
@@ -75,6 +75,27 @@ impl Db {
|
|
|
75
75
|
&self.conn
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/// Load a SQLite extension shared library onto this connection.
|
|
79
|
+
///
|
|
80
|
+
/// `entrypoint` overrides the init symbol; when `None`, SQLite derives it
|
|
81
|
+
/// from the filename. Extension loading is enabled for the duration of the
|
|
82
|
+
/// call via [`rusqlite::LoadExtensionGuard`] and disabled again on return,
|
|
83
|
+
/// so the SQL `load_extension()` function is never left exposed to later
|
|
84
|
+
/// queries. A missing or unloadable file surfaces as [`DbError::Sqlite`].
|
|
85
|
+
pub fn load_extension(&self, path: &Path, entrypoint: Option<&str>) -> Result<()> {
|
|
86
|
+
// SAFETY: loading an extension executes native code from `path`. The
|
|
87
|
+
// path is operator-supplied configuration (a `[[dirsql.extension]]`
|
|
88
|
+
// entry), at the same trust level as the DDL the operator already
|
|
89
|
+
// controls. The guard enables loading on construction and disables it
|
|
90
|
+
// on drop at the end of this block — after the load — so the SQL
|
|
91
|
+
// `load_extension()` function is never left exposed to later queries.
|
|
92
|
+
unsafe {
|
|
93
|
+
let _guard = rusqlite::LoadExtensionGuard::new(&self.conn)?;
|
|
94
|
+
self.conn.load_extension(path, entrypoint)?;
|
|
95
|
+
}
|
|
96
|
+
Ok(())
|
|
97
|
+
}
|
|
98
|
+
|
|
78
99
|
/// Create a table from a user-provided DDL statement.
|
|
79
100
|
/// Automatically injects internal tracking columns (_dirsql_file_path, _dirsql_row_index).
|
|
80
101
|
///
|
|
@@ -83,6 +104,20 @@ impl Db {
|
|
|
83
104
|
/// `CREATE TABLE foo;DROP_TABLE_bar--(id TEXT)` would parse to a poisoned
|
|
84
105
|
/// internal table name and break downstream `format!()`-built SQL.
|
|
85
106
|
pub fn create_table(&self, ddl: &str) -> Result<()> {
|
|
107
|
+
// A dirsql table is a per-file row table: create_table injects
|
|
108
|
+
// `_dirsql_` tracking columns and the engine inserts one row per file.
|
|
109
|
+
// That is structurally incompatible with an extension-backed virtual
|
|
110
|
+
// table, so reject `CREATE VIRTUAL TABLE` with a clear message instead
|
|
111
|
+
// of mangling the DDL via column injection. Load the extension and use
|
|
112
|
+
// its functions in queries instead.
|
|
113
|
+
if is_virtual_table_ddl(ddl) {
|
|
114
|
+
return Err(DbError::DdlParse(
|
|
115
|
+
"CREATE VIRTUAL TABLE is not supported as a dirsql table \
|
|
116
|
+
(dirsql tables are per-file row tables); load the extension \
|
|
117
|
+
and call its functions in queries instead"
|
|
118
|
+
.to_string(),
|
|
119
|
+
));
|
|
120
|
+
}
|
|
86
121
|
let table = parse_table_name(ddl).ok_or_else(|| DbError::DdlParse(ddl.to_string()))?;
|
|
87
122
|
validate_identifier(&table)?;
|
|
88
123
|
let augmented = inject_tracking_columns(ddl)?;
|
|
@@ -390,6 +425,16 @@ pub fn parse_table_name(ddl: &str) -> Option<String> {
|
|
|
390
425
|
if name.is_empty() { None } else { Some(name) }
|
|
391
426
|
}
|
|
392
427
|
|
|
428
|
+
/// True if `ddl` is a `CREATE VIRTUAL TABLE` statement. dirsql tables are
|
|
429
|
+
/// per-file row tables (create_table injects `_dirsql_` tracking columns and
|
|
430
|
+
/// inserts one row per file), which is structurally incompatible with an
|
|
431
|
+
/// extension-backed virtual table — those are rejected with a clear error
|
|
432
|
+
/// rather than mangled by column injection.
|
|
433
|
+
fn is_virtual_table_ddl(ddl: &str) -> bool {
|
|
434
|
+
let normalized = ddl.split_whitespace().collect::<Vec<_>>().join(" ");
|
|
435
|
+
normalized.to_uppercase().contains("CREATE VIRTUAL TABLE")
|
|
436
|
+
}
|
|
437
|
+
|
|
393
438
|
impl From<rusqlite::types::Value> for Value {
|
|
394
439
|
fn from(v: rusqlite::types::Value) -> Self {
|
|
395
440
|
match v {
|
|
@@ -1078,4 +1123,55 @@ mod tests {
|
|
|
1078
1123
|
assert_eq!(rows.len(), 1);
|
|
1079
1124
|
assert!(!rows[0].contains_key("_dirsql_file_path"));
|
|
1080
1125
|
}
|
|
1126
|
+
|
|
1127
|
+
// --- load_extension: error path (missing shared library) ---
|
|
1128
|
+
|
|
1129
|
+
#[test]
|
|
1130
|
+
fn load_extension_missing_file_errors() {
|
|
1131
|
+
// Loading is enabled for the call (the guard succeeds), the load of a
|
|
1132
|
+
// nonexistent shared library fails, and the error propagates as
|
|
1133
|
+
// DbError::Sqlite. Exercises the enable→load path; the success arm is
|
|
1134
|
+
// covered by the integration suite against a real extension.
|
|
1135
|
+
let db = Db::new().unwrap();
|
|
1136
|
+
let err = db
|
|
1137
|
+
.load_extension(Path::new("/nonexistent/dirsql-no-such-ext.so"), None)
|
|
1138
|
+
.unwrap_err();
|
|
1139
|
+
assert!(matches!(err, DbError::Sqlite(_)), "got: {err}");
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// --- create_table: virtual tables are not supported as dirsql tables ---
|
|
1143
|
+
|
|
1144
|
+
#[test]
|
|
1145
|
+
fn create_table_rejects_virtual_table_with_clear_error() {
|
|
1146
|
+
// A dirsql table is a per-file row table: create_table injects
|
|
1147
|
+
// _dirsql_ tracking columns and the engine inserts one row per file.
|
|
1148
|
+
// That is structurally incompatible with an extension-backed virtual
|
|
1149
|
+
// table, so a `CREATE VIRTUAL TABLE` DDL must fail with a clear,
|
|
1150
|
+
// specific error rather than a confusing "no such module" / mangled
|
|
1151
|
+
// DDL. (RED for #225 review finding #1.)
|
|
1152
|
+
let db = Db::new().unwrap();
|
|
1153
|
+
let err = db
|
|
1154
|
+
.create_table("CREATE VIRTUAL TABLE vss USING vec0(embedding float[4])")
|
|
1155
|
+
.unwrap_err();
|
|
1156
|
+
// Must be a clear "not supported" message, NOT the generic
|
|
1157
|
+
// `DdlParse` echo (which trivially contains "virtual table" because it
|
|
1158
|
+
// echoes the DDL back).
|
|
1159
|
+
let msg = err.to_string().to_lowercase();
|
|
1160
|
+
assert!(
|
|
1161
|
+
msg.contains("virtual table") && msg.contains("not supported"),
|
|
1162
|
+
"expected a clear 'virtual table not supported' error, not a generic DDL-parse echo, got: {err}"
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
#[test]
|
|
1167
|
+
fn is_virtual_table_ddl_detects_variants() {
|
|
1168
|
+
assert!(is_virtual_table_ddl("CREATE VIRTUAL TABLE x USING vec0(a)"));
|
|
1169
|
+
assert!(is_virtual_table_ddl(
|
|
1170
|
+
"create virtual table x using fts5(a)"
|
|
1171
|
+
));
|
|
1172
|
+
assert!(!is_virtual_table_ddl("CREATE TABLE x (a TEXT)"));
|
|
1173
|
+
assert!(!is_virtual_table_ddl(
|
|
1174
|
+
"CREATE TABLE IF NOT EXISTS x (a TEXT)"
|
|
1175
|
+
));
|
|
1176
|
+
}
|
|
1081
1177
|
}
|
|
@@ -47,6 +47,7 @@ use std::thread;
|
|
|
47
47
|
use std::time::Duration;
|
|
48
48
|
use thiserror::Error;
|
|
49
49
|
|
|
50
|
+
pub use crate::config::ExtensionSpec as Extension;
|
|
50
51
|
pub use crate::db::{DbError, Value};
|
|
51
52
|
pub use crate::differ::RowEvent;
|
|
52
53
|
#[doc(hidden)]
|
|
@@ -89,6 +90,13 @@ pub enum DirSqlError {
|
|
|
89
90
|
#[error("table DDL could not be parsed: {0}")]
|
|
90
91
|
Ddl(String),
|
|
91
92
|
|
|
93
|
+
#[error("failed to load extension '{}': {source}", .path.display())]
|
|
94
|
+
Extension {
|
|
95
|
+
path: PathBuf,
|
|
96
|
+
#[source]
|
|
97
|
+
source: DbError,
|
|
98
|
+
},
|
|
99
|
+
|
|
92
100
|
#[error("duplicate table name: {0}")]
|
|
93
101
|
DuplicateTable(String),
|
|
94
102
|
|
|
@@ -232,6 +240,8 @@ struct DirSqlInner {
|
|
|
232
240
|
table_configs: Vec<TableConfig>,
|
|
233
241
|
/// Resolved ignore patterns preserved for [`DirSQL::config`].
|
|
234
242
|
ignore: Vec<String>,
|
|
243
|
+
/// Configured SQLite extensions, preserved for [`DirSQL::config`].
|
|
244
|
+
extensions: Vec<Extension>,
|
|
235
245
|
/// Whether persistent caching is enabled.
|
|
236
246
|
persist: bool,
|
|
237
247
|
/// Override location of the persistent cache file.
|
|
@@ -278,6 +288,8 @@ pub struct DirSQLConfig {
|
|
|
278
288
|
pub ignore: Vec<String>,
|
|
279
289
|
pub persist: bool,
|
|
280
290
|
pub persist_path: Option<PathBuf>,
|
|
291
|
+
/// SQLite extensions loaded onto the connection at startup, in load order.
|
|
292
|
+
pub extensions: Vec<Extension>,
|
|
281
293
|
}
|
|
282
294
|
|
|
283
295
|
/// Serializable per-table portion of [`DirSQLConfig`]. Captures only the
|
|
@@ -357,6 +369,7 @@ impl DirSQL {
|
|
|
357
369
|
ignore: self.inner.ignore.clone(),
|
|
358
370
|
persist: self.inner.persist,
|
|
359
371
|
persist_path: self.inner.persist_path.clone(),
|
|
372
|
+
extensions: self.inner.extensions.clone(),
|
|
360
373
|
}
|
|
361
374
|
}
|
|
362
375
|
|
|
@@ -641,6 +654,7 @@ impl DirSQL {
|
|
|
641
654
|
root,
|
|
642
655
|
tables,
|
|
643
656
|
ignore,
|
|
657
|
+
extensions,
|
|
644
658
|
persist,
|
|
645
659
|
persist_path,
|
|
646
660
|
poll_interval,
|
|
@@ -687,6 +701,7 @@ impl DirSQL {
|
|
|
687
701
|
Ok(PreparedBuild {
|
|
688
702
|
root,
|
|
689
703
|
tables,
|
|
704
|
+
extensions,
|
|
690
705
|
matcher,
|
|
691
706
|
scanned_files,
|
|
692
707
|
persist: persist_ctx.map(|ctx| PreparedPersist {
|
|
@@ -716,6 +731,7 @@ impl DirSQL {
|
|
|
716
731
|
let PreparedBuild {
|
|
717
732
|
root,
|
|
718
733
|
tables,
|
|
734
|
+
extensions,
|
|
719
735
|
matcher,
|
|
720
736
|
scanned_files,
|
|
721
737
|
persist,
|
|
@@ -730,6 +746,20 @@ impl DirSQL {
|
|
|
730
746
|
None => (Db::new()?, None),
|
|
731
747
|
};
|
|
732
748
|
|
|
749
|
+
// Load configured SQLite extensions onto the connection before any
|
|
750
|
+
// CREATE TABLE so a table's DDL and later queries can use
|
|
751
|
+
// extension-provided functions. (An extension-backed *virtual table*
|
|
752
|
+
// cannot be a dirsql-managed `[[table]]` — those inject per-file
|
|
753
|
+
// tracking columns; see Db::create_table.) Loading is enabled only for
|
|
754
|
+
// the duration of each load and disabled again afterwards.
|
|
755
|
+
for ext in &extensions {
|
|
756
|
+
db.load_extension(&ext.path, ext.entrypoint.as_deref())
|
|
757
|
+
.map_err(|source| DirSqlError::Extension {
|
|
758
|
+
path: ext.path.clone(),
|
|
759
|
+
source,
|
|
760
|
+
})?;
|
|
761
|
+
}
|
|
762
|
+
|
|
733
763
|
let mut extract_map: HashMap<String, Arc<ExtractFn>> = HashMap::new();
|
|
734
764
|
let mut strict_map: HashMap<String, bool> = HashMap::new();
|
|
735
765
|
let mut ddl_map: HashMap<String, String> = HashMap::new();
|
|
@@ -849,6 +879,7 @@ impl DirSQL {
|
|
|
849
879
|
strict_map,
|
|
850
880
|
table_configs,
|
|
851
881
|
ignore,
|
|
882
|
+
extensions,
|
|
852
883
|
persist: persist_enabled,
|
|
853
884
|
persist_path,
|
|
854
885
|
file_rows: Mutex::new(file_rows),
|
|
@@ -892,6 +923,7 @@ pub struct DirSQLBuilder {
|
|
|
892
923
|
root: Option<PathBuf>,
|
|
893
924
|
tables: Vec<Table>,
|
|
894
925
|
ignore: Vec<String>,
|
|
926
|
+
extensions: Vec<Extension>,
|
|
895
927
|
config_path: Option<PathBuf>,
|
|
896
928
|
persist: bool,
|
|
897
929
|
persist_path: Option<PathBuf>,
|
|
@@ -929,6 +961,27 @@ impl DirSQLBuilder {
|
|
|
929
961
|
self
|
|
930
962
|
}
|
|
931
963
|
|
|
964
|
+
/// Append a single SQLite extension to load at startup. Extensions are
|
|
965
|
+
/// loaded onto the connection before any `CREATE TABLE`, then loading is
|
|
966
|
+
/// disabled again. See [`Extension`].
|
|
967
|
+
///
|
|
968
|
+
/// A relative `path` here is used verbatim — the OS resolves it against the
|
|
969
|
+
/// process working directory at load time. Config-file paths, by contrast,
|
|
970
|
+
/// resolve against the config file's parent directory.
|
|
971
|
+
pub fn extension(mut self, extension: Extension) -> Self {
|
|
972
|
+
self.extensions.push(extension);
|
|
973
|
+
self
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/// Replace the accumulated extension list with `extensions`.
|
|
977
|
+
pub fn extensions<I>(mut self, extensions: I) -> Self
|
|
978
|
+
where
|
|
979
|
+
I: IntoIterator<Item = Extension>,
|
|
980
|
+
{
|
|
981
|
+
self.extensions = extensions.into_iter().collect();
|
|
982
|
+
self
|
|
983
|
+
}
|
|
984
|
+
|
|
932
985
|
/// Load a `.dirsql.toml` config file at build time. The file's `[[table]]`
|
|
933
986
|
/// entries are appended after any programmatic tables; its `[dirsql].ignore`
|
|
934
987
|
/// patterns are appended; its optional `[dirsql].root` is resolved relative
|
|
@@ -975,6 +1028,7 @@ impl DirSQLBuilder {
|
|
|
975
1028
|
root: explicit_root,
|
|
976
1029
|
mut tables,
|
|
977
1030
|
mut ignore,
|
|
1031
|
+
mut extensions,
|
|
978
1032
|
config_path,
|
|
979
1033
|
mut persist,
|
|
980
1034
|
mut persist_path,
|
|
@@ -1005,6 +1059,21 @@ impl DirSQLBuilder {
|
|
|
1005
1059
|
tables.extend(cfg_tables);
|
|
1006
1060
|
ignore.extend(cfg.ignore);
|
|
1007
1061
|
|
|
1062
|
+
// Resolve config-supplied extension paths against the config
|
|
1063
|
+
// file's parent directory (absolute paths pass through). Appended
|
|
1064
|
+
// after any programmatically-supplied extensions.
|
|
1065
|
+
for ext in cfg.extensions {
|
|
1066
|
+
let path = if ext.path.is_absolute() {
|
|
1067
|
+
ext.path
|
|
1068
|
+
} else {
|
|
1069
|
+
cfg_parent.join(&ext.path)
|
|
1070
|
+
};
|
|
1071
|
+
extensions.push(Extension {
|
|
1072
|
+
path,
|
|
1073
|
+
entrypoint: ext.entrypoint,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1008
1077
|
if cfg.persist {
|
|
1009
1078
|
persist = true;
|
|
1010
1079
|
}
|
|
@@ -1045,6 +1114,7 @@ impl DirSQLBuilder {
|
|
|
1045
1114
|
root,
|
|
1046
1115
|
tables,
|
|
1047
1116
|
ignore,
|
|
1117
|
+
extensions,
|
|
1048
1118
|
persist,
|
|
1049
1119
|
persist_path,
|
|
1050
1120
|
poll_interval: poll_interval.unwrap_or(DEFAULT_POLL_INTERVAL),
|
|
@@ -1087,6 +1157,7 @@ pub struct ResolvedBuild {
|
|
|
1087
1157
|
pub root: PathBuf,
|
|
1088
1158
|
pub tables: Vec<Table>,
|
|
1089
1159
|
pub ignore: Vec<String>,
|
|
1160
|
+
pub extensions: Vec<Extension>,
|
|
1090
1161
|
pub persist: bool,
|
|
1091
1162
|
pub persist_path: Option<PathBuf>,
|
|
1092
1163
|
pub poll_interval: Duration,
|
|
@@ -1109,6 +1180,8 @@ pub struct ScannedFile {
|
|
|
1109
1180
|
pub struct PreparedBuild {
|
|
1110
1181
|
root: PathBuf,
|
|
1111
1182
|
tables: Vec<Table>,
|
|
1183
|
+
/// SQLite extensions to load onto the connection before any table DDL.
|
|
1184
|
+
extensions: Vec<Extension>,
|
|
1112
1185
|
matcher: TableMatcher,
|
|
1113
1186
|
scanned_files: Vec<ScannedFile>,
|
|
1114
1187
|
persist: Option<PreparedPersist>,
|
|
@@ -1616,6 +1689,28 @@ mod readonly_tests {
|
|
|
1616
1689
|
assert!(matches!(err, DirSqlError::Core(_)), "got: {err:?}");
|
|
1617
1690
|
}
|
|
1618
1691
|
|
|
1692
|
+
#[test]
|
|
1693
|
+
fn missing_extension_build_fails_with_extension_error() {
|
|
1694
|
+
// The .extension() builder surface loads at startup; a missing file
|
|
1695
|
+
// must surface as DirSqlError::Extension (naming the library), not the
|
|
1696
|
+
// generic Core(Sqlite) error. (#225 review finding #9; also exercises
|
|
1697
|
+
// the .extension() builder method in-process.)
|
|
1698
|
+
let dir = tempfile::tempdir().unwrap();
|
|
1699
|
+
let err = match DirSQL::builder()
|
|
1700
|
+
.root(dir.path())
|
|
1701
|
+
.extension(Extension {
|
|
1702
|
+
path: "/nonexistent/dirsql-no-such.so".into(),
|
|
1703
|
+
entrypoint: None,
|
|
1704
|
+
})
|
|
1705
|
+
.build()
|
|
1706
|
+
{
|
|
1707
|
+
Ok(_) => panic!("expected build to fail on a missing extension"),
|
|
1708
|
+
Err(e) => e,
|
|
1709
|
+
};
|
|
1710
|
+
assert!(matches!(err, DirSqlError::Extension { .. }), "got: {err:?}");
|
|
1711
|
+
assert!(err.to_string().contains("failed to load extension"));
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1619
1714
|
#[test]
|
|
1620
1715
|
fn error_helpers_build_expected_variants() {
|
|
1621
1716
|
// Exercise the `map_err` helper constructors directly; their runtime
|
|
@@ -1716,6 +1811,7 @@ mod internal_tests {
|
|
|
1716
1811
|
let prepared = PreparedBuild {
|
|
1717
1812
|
root: dir.path().to_path_buf(),
|
|
1718
1813
|
tables: Vec::new(),
|
|
1814
|
+
extensions: Vec::new(),
|
|
1719
1815
|
matcher,
|
|
1720
1816
|
scanned_files: vec![ScannedFile {
|
|
1721
1817
|
rel_path: "ghost.txt".into(),
|