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.
Files changed (163) hide show
  1. {dirsql-0.3.21 → dirsql-0.3.22}/Cargo.lock +1 -1
  2. {dirsql-0.3.21 → dirsql-0.3.22}/Cargo.toml +4 -0
  3. {dirsql-0.3.21 → dirsql-0.3.22}/PKG-INFO +1 -1
  4. {dirsql-0.3.21 → dirsql-0.3.22}/docs/cli/config.md +27 -0
  5. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/Cargo.toml +1 -1
  6. {dirsql-0.3.21/packages/rust → dirsql-0.3.22/packages/python}/docs/cli/config.md +27 -0
  7. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/Cargo.toml +1 -1
  8. {dirsql-0.3.21/packages/python → dirsql-0.3.22/packages/rust}/docs/cli/config.md +27 -0
  9. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/config.rs +137 -2
  10. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/db.rs +96 -0
  11. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/lib.rs +96 -0
  12. dirsql-0.3.22/packages/rust/tests/extensions.rs +247 -0
  13. {dirsql-0.3.21 → dirsql-0.3.22}/README.md +0 -0
  14. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/__init__.py +0 -0
  15. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/_async.py +0 -0
  16. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/_async_test.py +0 -0
  17. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/_dirsql.pyi +0 -0
  18. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/__init__.py +0 -0
  19. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/binary_path.py +0 -0
  20. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/binary_path_test.py +0 -0
  21. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/__init__.py +0 -0
  22. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/dispatch_extract.py +0 -0
  23. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/dispatch_extract_test.py +0 -0
  24. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/load_app.py +0 -0
  25. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/load_app_test.py +0 -0
  26. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/run.py +0 -0
  27. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/run_test.py +0 -0
  28. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/write_message.py +0 -0
  29. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/interpret/write_message_test.py +0 -0
  30. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/is_windows.py +0 -0
  31. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/is_windows_test.py +0 -0
  32. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/main.py +0 -0
  33. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/cli/main_test.py +0 -0
  34. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/py.typed +0 -0
  35. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/resolve_config.py +0 -0
  36. {dirsql-0.3.21 → dirsql-0.3.22}/dirsql/resolve_config_test.py +0 -0
  37. {dirsql-0.3.21 → dirsql-0.3.22}/docs/.claude/CLAUDE.md +0 -0
  38. {dirsql-0.3.21 → dirsql-0.3.22}/docs/.vitepress/config.ts +0 -0
  39. {dirsql-0.3.21 → dirsql-0.3.22}/docs/.vitepress/theme/index.ts +0 -0
  40. {dirsql-0.3.21 → dirsql-0.3.22}/docs/.vitepress/theme/lang.ts +0 -0
  41. {dirsql-0.3.21 → dirsql-0.3.22}/docs/AGENTS.md +0 -0
  42. {dirsql-0.3.21 → dirsql-0.3.22}/docs/api/index.md +0 -0
  43. {dirsql-0.3.21 → dirsql-0.3.22}/docs/cli/http-api.md +0 -0
  44. {dirsql-0.3.21 → dirsql-0.3.22}/docs/cli/index.md +0 -0
  45. {dirsql-0.3.21 → dirsql-0.3.22}/docs/cli/init.md +0 -0
  46. {dirsql-0.3.21 → dirsql-0.3.22}/docs/cli/server.md +0 -0
  47. {dirsql-0.3.21 → dirsql-0.3.22}/docs/getting-started.md +0 -0
  48. {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/async.md +0 -0
  49. {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/crdt.md +0 -0
  50. {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/persistence.md +0 -0
  51. {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/querying.md +0 -0
  52. {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/tables.md +0 -0
  53. {dirsql-0.3.21 → dirsql-0.3.22}/docs/guide/watching.md +0 -0
  54. {dirsql-0.3.21 → dirsql-0.3.22}/docs/index.md +0 -0
  55. {dirsql-0.3.21 → dirsql-0.3.22}/docs/migrations.md +0 -0
  56. {dirsql-0.3.21 → dirsql-0.3.22}/docs/package.json +0 -0
  57. {dirsql-0.3.21 → dirsql-0.3.22}/docs/playwright.config.ts +0 -0
  58. {dirsql-0.3.21 → dirsql-0.3.22}/docs/pnpm-lock.yaml +0 -0
  59. {dirsql-0.3.21 → dirsql-0.3.22}/docs/pnpm-workspace.yaml +0 -0
  60. {dirsql-0.3.21 → dirsql-0.3.22}/docs/tests/integration/home.spec.ts +0 -0
  61. {dirsql-0.3.21 → dirsql-0.3.22}/docs/tests/integration/language-flag.spec.ts +0 -0
  62. {dirsql-0.3.21 → dirsql-0.3.22}/docs/tests/unit/config.test.ts +0 -0
  63. {dirsql-0.3.21 → dirsql-0.3.22}/docs/tests/unit/lang.test.ts +0 -0
  64. {dirsql-0.3.21 → dirsql-0.3.22}/docs/vitest.config.ts +0 -0
  65. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/README.md +0 -0
  66. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/conftest.py +0 -0
  67. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/.claude/CLAUDE.md +0 -0
  68. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/.vitepress/config.ts +0 -0
  69. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  70. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  71. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/AGENTS.md +0 -0
  72. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/api/index.md +0 -0
  73. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/cli/http-api.md +0 -0
  74. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/cli/index.md +0 -0
  75. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/cli/init.md +0 -0
  76. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/cli/server.md +0 -0
  77. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/getting-started.md +0 -0
  78. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/async.md +0 -0
  79. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/crdt.md +0 -0
  80. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/persistence.md +0 -0
  81. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/querying.md +0 -0
  82. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/tables.md +0 -0
  83. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/guide/watching.md +0 -0
  84. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/index.md +0 -0
  85. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/migrations.md +0 -0
  86. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/package.json +0 -0
  87. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/playwright.config.ts +0 -0
  88. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/pnpm-lock.yaml +0 -0
  89. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/pnpm-workspace.yaml +0 -0
  90. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  91. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  92. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/tests/unit/config.test.ts +0 -0
  93. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  94. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/docs/vitest.config.ts +0 -0
  95. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/src/lib.rs +0 -0
  96. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/__init__.py +0 -0
  97. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/conftest.py +0 -0
  98. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/e2e/__init__.py +0 -0
  99. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/data/a/meta.json +0 -0
  100. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/data/b/meta.json +0 -0
  101. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/dirsql.config.py +0 -0
  102. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/interpret/data/a/meta.json +0 -0
  103. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/interpret/data/b/meta.json +0 -0
  104. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config.py +0 -0
  105. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_no_app.py +0 -0
  106. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_raises.py +0 -0
  107. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/__init__.py +0 -0
  108. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/interpret_subprocess.py +0 -0
  109. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_async_dirsql.py +0 -0
  110. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_binding.py +0 -0
  111. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_dirsql.py +0 -0
  112. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_docs_examples.py +0 -0
  113. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_docs_gaps.py +0 -0
  114. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_from_config.py +0 -0
  115. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_interpret.py +0 -0
  116. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_native_config.py +0 -0
  117. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_persist.py +0 -0
  118. {dirsql-0.3.21 → dirsql-0.3.22}/packages/python/tests/integration/test_serialization.py +0 -0
  119. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/README.md +0 -0
  120. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/benches/db_bench.rs +0 -0
  121. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/benches/differ_bench.rs +0 -0
  122. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/benches/matcher_bench.rs +0 -0
  123. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/benches/scanner_bench.rs +0 -0
  124. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/api/index.md +0 -0
  125. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/cli/http-api.md +0 -0
  126. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/cli/index.md +0 -0
  127. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/cli/init.md +0 -0
  128. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/cli/server.md +0 -0
  129. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/getting-started.md +0 -0
  130. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/async.md +0 -0
  131. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/crdt.md +0 -0
  132. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/persistence.md +0 -0
  133. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/querying.md +0 -0
  134. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/tables.md +0 -0
  135. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/guide/watching.md +0 -0
  136. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/index.md +0 -0
  137. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/docs/migrations.md +0 -0
  138. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/bin/dirsql.rs +0 -0
  139. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/init.rs +0 -0
  140. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/mod.rs +0 -0
  141. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/native_config.rs +0 -0
  142. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/router.rs +0 -0
  143. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/serialize.rs +0 -0
  144. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/cli/server.rs +0 -0
  145. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/differ.rs +0 -0
  146. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/matcher.rs +0 -0
  147. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/persist.rs +0 -0
  148. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/scanner.rs +0 -0
  149. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/src/watcher.rs +0 -0
  150. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/async_sdk.rs +0 -0
  151. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/cli_e2e.rs +0 -0
  152. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/cli_integration.rs +0 -0
  153. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/code_review_findings.rs +0 -0
  154. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/docs_examples.rs +0 -0
  155. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/docs_gaps.rs +0 -0
  156. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/from_config.rs +0 -0
  157. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/init_e2e.rs +0 -0
  158. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/init_integration.rs +0 -0
  159. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/persist.rs +0 -0
  160. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/readonly_query.rs +0 -0
  161. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/sdk.rs +0 -0
  162. {dirsql-0.3.21 → dirsql-0.3.22}/packages/rust/tests/serialization.rs +0 -0
  163. {dirsql-0.3.21 → dirsql-0.3.22}/pyproject.toml +0 -0
@@ -499,7 +499,7 @@ dependencies = [
499
499
 
500
500
  [[package]]
501
501
  name = "dirsql-py-ext"
502
- version = "0.3.21"
502
+ version = "0.3.22"
503
503
  dependencies = [
504
504
  "dirsql",
505
505
  "pyo3",
@@ -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]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.3.21
3
+ Version: 0.3.22
4
4
  Requires-Dist: pytest>=8 ; extra == 'dev'
5
5
  Requires-Dist: pytest-describe>=2 ; extra == 'dev'
6
6
  Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
@@ -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.21"
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(),