dirsql 0.3.31__tar.gz → 0.3.33__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 (167) hide show
  1. {dirsql-0.3.31 → dirsql-0.3.33}/Cargo.lock +1 -1
  2. {dirsql-0.3.31 → dirsql-0.3.33}/PKG-INFO +22 -1
  3. {dirsql-0.3.31/packages/python → dirsql-0.3.33}/README.md +21 -0
  4. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/_async.py +9 -0
  5. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/_dirsql.pyi +10 -1
  6. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/resolve_config.py +16 -2
  7. {dirsql-0.3.31 → dirsql-0.3.33}/docs/api/index.md +5 -2
  8. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/Cargo.toml +1 -1
  9. {dirsql-0.3.31 → dirsql-0.3.33/packages/python}/README.md +21 -0
  10. {dirsql-0.3.31/packages/rust → dirsql-0.3.33/packages/python}/docs/api/index.md +5 -2
  11. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/src/lib.rs +29 -2
  12. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/Cargo.toml +4 -0
  13. {dirsql-0.3.31/packages/python → dirsql-0.3.33/packages/rust}/docs/api/index.md +5 -2
  14. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/cli/native_config.rs +91 -2
  15. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/lib.rs +75 -41
  16. {dirsql-0.3.31 → dirsql-0.3.33}/pyproject.toml +6 -0
  17. dirsql-0.3.31/dirsql/_async_test.py +0 -95
  18. dirsql-0.3.31/dirsql/cli/binary_path_test.py +0 -51
  19. dirsql-0.3.31/dirsql/cli/interpret/dispatch_extract_test.py +0 -92
  20. dirsql-0.3.31/dirsql/cli/interpret/load_app_test.py +0 -85
  21. dirsql-0.3.31/dirsql/cli/interpret/run_test.py +0 -172
  22. dirsql-0.3.31/dirsql/cli/interpret/write_message_test.py +0 -30
  23. dirsql-0.3.31/dirsql/cli/is_windows_test.py +0 -20
  24. dirsql-0.3.31/dirsql/cli/main_test.py +0 -127
  25. dirsql-0.3.31/dirsql/resolve_config_test.py +0 -231
  26. dirsql-0.3.31/packages/python/tests/integration/test_async_dirsql.py +0 -415
  27. dirsql-0.3.31/packages/python/tests/integration/test_binding.py +0 -290
  28. dirsql-0.3.31/packages/python/tests/integration/test_dirsql.py +0 -544
  29. dirsql-0.3.31/packages/python/tests/integration/test_docs_examples.py +0 -825
  30. dirsql-0.3.31/packages/python/tests/integration/test_docs_gaps.py +0 -257
  31. dirsql-0.3.31/packages/python/tests/integration/test_from_config.py +0 -230
  32. dirsql-0.3.31/packages/python/tests/integration/test_interpret.py +0 -145
  33. dirsql-0.3.31/packages/python/tests/integration/test_native_config.py +0 -100
  34. dirsql-0.3.31/packages/python/tests/integration/test_persist.py +0 -307
  35. dirsql-0.3.31/packages/python/tests/integration/test_serialization.py +0 -237
  36. dirsql-0.3.31/packages/rust/tests/async_sdk.rs +0 -294
  37. dirsql-0.3.31/packages/rust/tests/cli_e2e.rs +0 -397
  38. dirsql-0.3.31/packages/rust/tests/cli_integration.rs +0 -547
  39. dirsql-0.3.31/packages/rust/tests/code_review_findings.rs +0 -288
  40. dirsql-0.3.31/packages/rust/tests/config.rs +0 -26
  41. dirsql-0.3.31/packages/rust/tests/docs_examples.rs +0 -829
  42. dirsql-0.3.31/packages/rust/tests/docs_gaps.rs +0 -144
  43. dirsql-0.3.31/packages/rust/tests/extensions.rs +0 -247
  44. dirsql-0.3.31/packages/rust/tests/from_config.rs +0 -361
  45. dirsql-0.3.31/packages/rust/tests/init_e2e.rs +0 -94
  46. dirsql-0.3.31/packages/rust/tests/init_integration.rs +0 -301
  47. dirsql-0.3.31/packages/rust/tests/persist.rs +0 -427
  48. dirsql-0.3.31/packages/rust/tests/readonly_query.rs +0 -211
  49. dirsql-0.3.31/packages/rust/tests/scanner.rs +0 -97
  50. dirsql-0.3.31/packages/rust/tests/sdk.rs +0 -857
  51. dirsql-0.3.31/packages/rust/tests/serialization.rs +0 -161
  52. dirsql-0.3.31/packages/rust/tests/watch_relative_root.rs +0 -134
  53. dirsql-0.3.31/packages/rust/tests/watcher.rs +0 -139
  54. {dirsql-0.3.31 → dirsql-0.3.33}/Cargo.toml +0 -0
  55. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/__init__.py +0 -0
  56. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/cli/__init__.py +0 -0
  57. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/cli/binary_path.py +0 -0
  58. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/cli/interpret/__init__.py +0 -0
  59. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/cli/interpret/dispatch_extract.py +0 -0
  60. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/cli/interpret/load_app.py +0 -0
  61. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/cli/interpret/run.py +0 -0
  62. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/cli/interpret/write_message.py +0 -0
  63. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/cli/is_windows.py +0 -0
  64. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/cli/main.py +0 -0
  65. {dirsql-0.3.31 → dirsql-0.3.33}/dirsql/py.typed +0 -0
  66. {dirsql-0.3.31 → dirsql-0.3.33}/docs/.claude/CLAUDE.md +0 -0
  67. {dirsql-0.3.31 → dirsql-0.3.33}/docs/.vitepress/config.ts +0 -0
  68. {dirsql-0.3.31 → dirsql-0.3.33}/docs/.vitepress/theme/index.ts +0 -0
  69. {dirsql-0.3.31 → dirsql-0.3.33}/docs/.vitepress/theme/lang.ts +0 -0
  70. {dirsql-0.3.31 → dirsql-0.3.33}/docs/AGENTS.md +0 -0
  71. {dirsql-0.3.31 → dirsql-0.3.33}/docs/cli/config.md +0 -0
  72. {dirsql-0.3.31 → dirsql-0.3.33}/docs/cli/http-api.md +0 -0
  73. {dirsql-0.3.31 → dirsql-0.3.33}/docs/cli/index.md +0 -0
  74. {dirsql-0.3.31 → dirsql-0.3.33}/docs/cli/init.md +0 -0
  75. {dirsql-0.3.31 → dirsql-0.3.33}/docs/cli/server.md +0 -0
  76. {dirsql-0.3.31 → dirsql-0.3.33}/docs/getting-started.md +0 -0
  77. {dirsql-0.3.31 → dirsql-0.3.33}/docs/guide/async.md +0 -0
  78. {dirsql-0.3.31 → dirsql-0.3.33}/docs/guide/crdt.md +0 -0
  79. {dirsql-0.3.31 → dirsql-0.3.33}/docs/guide/persistence.md +0 -0
  80. {dirsql-0.3.31 → dirsql-0.3.33}/docs/guide/querying.md +0 -0
  81. {dirsql-0.3.31 → dirsql-0.3.33}/docs/guide/tables.md +0 -0
  82. {dirsql-0.3.31 → dirsql-0.3.33}/docs/guide/watching.md +0 -0
  83. {dirsql-0.3.31 → dirsql-0.3.33}/docs/index.md +0 -0
  84. {dirsql-0.3.31 → dirsql-0.3.33}/docs/migrations.md +0 -0
  85. {dirsql-0.3.31 → dirsql-0.3.33}/docs/package.json +0 -0
  86. {dirsql-0.3.31 → dirsql-0.3.33}/docs/playwright.config.ts +0 -0
  87. {dirsql-0.3.31 → dirsql-0.3.33}/docs/pnpm-lock.yaml +0 -0
  88. {dirsql-0.3.31 → dirsql-0.3.33}/docs/pnpm-workspace.yaml +0 -0
  89. {dirsql-0.3.31 → dirsql-0.3.33}/docs/tests/integration/home.spec.ts +0 -0
  90. {dirsql-0.3.31 → dirsql-0.3.33}/docs/tests/integration/language-flag.spec.ts +0 -0
  91. {dirsql-0.3.31 → dirsql-0.3.33}/docs/tests/unit/config.test.ts +0 -0
  92. {dirsql-0.3.31 → dirsql-0.3.33}/docs/tests/unit/lang.test.ts +0 -0
  93. {dirsql-0.3.31 → dirsql-0.3.33}/docs/vitest.config.ts +0 -0
  94. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/conftest.py +0 -0
  95. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/.claude/CLAUDE.md +0 -0
  96. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/.vitepress/config.ts +0 -0
  97. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  98. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  99. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/AGENTS.md +0 -0
  100. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/cli/config.md +0 -0
  101. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/cli/http-api.md +0 -0
  102. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/cli/index.md +0 -0
  103. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/cli/init.md +0 -0
  104. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/cli/server.md +0 -0
  105. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/getting-started.md +0 -0
  106. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/guide/async.md +0 -0
  107. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/guide/crdt.md +0 -0
  108. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/guide/persistence.md +0 -0
  109. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/guide/querying.md +0 -0
  110. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/guide/tables.md +0 -0
  111. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/guide/watching.md +0 -0
  112. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/index.md +0 -0
  113. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/migrations.md +0 -0
  114. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/package.json +0 -0
  115. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/playwright.config.ts +0 -0
  116. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/pnpm-lock.yaml +0 -0
  117. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/pnpm-workspace.yaml +0 -0
  118. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  119. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  120. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/tests/unit/config.test.ts +0 -0
  121. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  122. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/docs/vitest.config.ts +0 -0
  123. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/__init__.py +0 -0
  124. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/conftest.py +0 -0
  125. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/e2e/__init__.py +0 -0
  126. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/integration/__fixtures__/data/a/meta.json +0 -0
  127. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/integration/__fixtures__/data/b/meta.json +0 -0
  128. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/integration/__fixtures__/dirsql.config.py +0 -0
  129. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/integration/__fixtures__/interpret/data/a/meta.json +0 -0
  130. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/integration/__fixtures__/interpret/data/b/meta.json +0 -0
  131. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config.py +0 -0
  132. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_no_app.py +0 -0
  133. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_raises.py +0 -0
  134. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/integration/__init__.py +0 -0
  135. {dirsql-0.3.31 → dirsql-0.3.33}/packages/python/tests/integration/interpret_subprocess.py +0 -0
  136. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/README.md +0 -0
  137. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/benches/db_bench.rs +0 -0
  138. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/benches/differ_bench.rs +0 -0
  139. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/benches/matcher_bench.rs +0 -0
  140. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/benches/scanner_bench.rs +0 -0
  141. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/cli/config.md +0 -0
  142. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/cli/http-api.md +0 -0
  143. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/cli/index.md +0 -0
  144. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/cli/init.md +0 -0
  145. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/cli/server.md +0 -0
  146. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/getting-started.md +0 -0
  147. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/guide/async.md +0 -0
  148. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/guide/crdt.md +0 -0
  149. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/guide/persistence.md +0 -0
  150. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/guide/querying.md +0 -0
  151. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/guide/tables.md +0 -0
  152. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/guide/watching.md +0 -0
  153. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/index.md +0 -0
  154. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/docs/migrations.md +0 -0
  155. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/bin/dirsql.rs +0 -0
  156. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/cli/init.rs +0 -0
  157. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/cli/mod.rs +0 -0
  158. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/cli/router.rs +0 -0
  159. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/cli/serialize.rs +0 -0
  160. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/cli/server.rs +0 -0
  161. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/config.rs +0 -0
  162. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/db.rs +0 -0
  163. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/differ.rs +0 -0
  164. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/matcher.rs +0 -0
  165. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/persist.rs +0 -0
  166. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/scanner.rs +0 -0
  167. {dirsql-0.3.31 → dirsql-0.3.33}/packages/rust/src/watcher.rs +0 -0
@@ -499,7 +499,7 @@ dependencies = [
499
499
 
500
500
  [[package]]
501
501
  name = "dirsql-py-ext"
502
- version = "0.3.31"
502
+ version = "0.3.33"
503
503
  dependencies = [
504
504
  "dirsql",
505
505
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.3.31
3
+ Version: 0.3.33
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'
@@ -98,6 +98,27 @@ db = DirSQL(
98
98
  )
99
99
  ```
100
100
 
101
+ ## Loading SQLite extensions
102
+
103
+ Pass `extensions` to load SQLite extension shared libraries onto the connection at startup (before any `CREATE TABLE`). Each entry is a dict with a `path` and an optional `entrypoint` init-symbol override:
104
+
105
+ ```python
106
+ db = DirSQL(
107
+ "./my-blog",
108
+ tables=[...],
109
+ extensions=[
110
+ {"path": "./ext/vec0.dylib", "entrypoint": "sqlite3_vec_init"},
111
+ {"path": "./ext/myext.so"}, # entrypoint derived from the filename
112
+ ],
113
+ )
114
+ await db.ready()
115
+
116
+ # The extension's functions are now callable in queries:
117
+ rows = await db.query("SELECT vec_version() AS v")
118
+ ```
119
+
120
+ `dirsql` enables extension loading only while loading the configured libraries, then disables it again, so the SQL `load_extension()` function is never exposed to your queries. Programmatic entries load first, followed by any `[[dirsql.extension]]` entries declared in a `config` file. See the [config reference](https://github.com/thekevinscott/dirsql/blob/main/docs/cli/config.md#loading-extensions).
121
+
101
122
  ## Watching for changes
102
123
 
103
124
  `db.watch()` returns an async iterator of row-level change events as files change on disk:
@@ -80,6 +80,27 @@ db = DirSQL(
80
80
  )
81
81
  ```
82
82
 
83
+ ## Loading SQLite extensions
84
+
85
+ Pass `extensions` to load SQLite extension shared libraries onto the connection at startup (before any `CREATE TABLE`). Each entry is a dict with a `path` and an optional `entrypoint` init-symbol override:
86
+
87
+ ```python
88
+ db = DirSQL(
89
+ "./my-blog",
90
+ tables=[...],
91
+ extensions=[
92
+ {"path": "./ext/vec0.dylib", "entrypoint": "sqlite3_vec_init"},
93
+ {"path": "./ext/myext.so"}, # entrypoint derived from the filename
94
+ ],
95
+ )
96
+ await db.ready()
97
+
98
+ # The extension's functions are now callable in queries:
99
+ rows = await db.query("SELECT vec_version() AS v")
100
+ ```
101
+
102
+ `dirsql` enables extension loading only while loading the configured libraries, then disables it again, so the SQL `load_extension()` function is never exposed to your queries. Programmatic entries load first, followed by any `[[dirsql.extension]]` entries declared in a `config` file. See the [config reference](https://github.com/thekevinscott/dirsql/blob/main/docs/cli/config.md#loading-extensions).
103
+
83
104
  ## Watching for changes
84
105
 
85
106
  `db.watch()` returns an async iterator of row-level change events as files change on disk:
@@ -50,6 +50,11 @@ class DirSQL:
50
50
 
51
51
  Pass ``persist=True`` to keep an on-disk SQLite cache (default location:
52
52
  ``<root>/.dirsql/cache.db``). Override the location with ``persist_path``.
53
+
54
+ Pass ``extensions`` -- a list of ``{"path": ..., "entrypoint": ...}`` dicts
55
+ (``entrypoint`` optional) -- to load SQLite extensions onto the connection
56
+ at startup. Any ``[[dirsql.extension]]`` entries in a ``config`` file are
57
+ appended after the programmatic ones.
53
58
  """
54
59
 
55
60
  def __init__(
@@ -61,6 +66,7 @@ class DirSQL:
61
66
  config=None,
62
67
  persist=False,
63
68
  persist_path=None,
69
+ extensions=None,
64
70
  ):
65
71
  if root is None and config is None:
66
72
  raise TypeError("DirSQL requires either a root directory or a config= path")
@@ -70,6 +76,7 @@ class DirSQL:
70
76
  self._config = config
71
77
  self._persist = persist
72
78
  self._persist_path = persist_path
79
+ self._extensions = extensions
73
80
  self._db = None
74
81
  self._ready_event = asyncio.Event()
75
82
  self._init_error = None
@@ -86,6 +93,7 @@ class DirSQL:
86
93
  config=self._config,
87
94
  persist=self._persist,
88
95
  persist_path=self._persist_path,
96
+ extensions=self._extensions,
89
97
  )
90
98
  except Exception as exc:
91
99
  self._init_error = exc
@@ -124,4 +132,5 @@ class DirSQL:
124
132
  self._config,
125
133
  self._persist,
126
134
  self._persist_path,
135
+ self._extensions,
127
136
  )
@@ -11,12 +11,20 @@ same PR -- and ``PARITY.md`` is the canonical reminder.
11
11
 
12
12
  from collections.abc import Callable
13
13
  from os import PathLike
14
- from typing import Any
14
+ from typing import Any, NotRequired, TypedDict
15
15
 
16
16
  __version__: str
17
17
 
18
18
  Row = dict[str, Any]
19
19
 
20
+ class ExtensionSpec(TypedDict):
21
+ """A SQLite extension to load at startup: a shared-library ``path`` and an
22
+ optional ``entrypoint`` init-symbol override. Mirrors a
23
+ ``[[dirsql.extension]]`` config entry."""
24
+
25
+ path: str
26
+ entrypoint: NotRequired[str]
27
+
20
28
  class Table:
21
29
  """A table definition. Construct via keyword arguments only."""
22
30
 
@@ -57,6 +65,7 @@ class DirSQL:
57
65
  config: str | None = None,
58
66
  persist: bool = False,
59
67
  persist_path: str | PathLike[str] | None = None,
68
+ extensions: list[ExtensionSpec] | None = None,
60
69
  ) -> None: ...
61
70
  def query(self, sql: str) -> list[Row]: ...
62
71
  def _start_watcher(self) -> None: ...
@@ -9,14 +9,17 @@ import os
9
9
  import tomllib
10
10
 
11
11
 
12
- def resolve_config(root, tables, ignore, config, persist, persist_path):
12
+ def resolve_config(
13
+ root, tables, ignore, config, persist, persist_path, extensions=None
14
+ ):
13
15
  """Merge kwargs with a `.dirsql.toml` into the serialized state shape."""
14
- cfg, cfg_tables, cfg_dir = {}, [], None
16
+ cfg, cfg_tables, cfg_extensions, cfg_dir = {}, [], [], None
15
17
  if config is not None:
16
18
  with open(config, "rb") as f:
17
19
  doc = tomllib.load(f)
18
20
  cfg = doc.get("dirsql") or {}
19
21
  cfg_tables = doc.get("table") or []
22
+ cfg_extensions = cfg.get("extension") or []
20
23
  cfg_dir = os.path.dirname(os.path.abspath(config))
21
24
 
22
25
  def _abs(p):
@@ -40,4 +43,15 @@ def resolve_config(root, tables, ignore, config, persist, persist_path):
40
43
  "persist": bool(persist or cfg.get("persist")),
41
44
  "persist_path": persist_path
42
45
  or (_abs(cfg["persist_path"]) if "persist_path" in cfg else None),
46
+ # Programmatic extensions first (verbatim paths, mirroring the Rust
47
+ # builder), then config-file `[[dirsql.extension]]` entries with
48
+ # relative paths resolved against the config's parent directory.
49
+ "extensions": [
50
+ {"path": e["path"], "entrypoint": e.get("entrypoint")}
51
+ for e in (extensions or [])
52
+ ]
53
+ + [
54
+ {"path": _abs(e["path"]), "entrypoint": e.get("entrypoint")}
55
+ for e in cfg_extensions
56
+ ],
43
57
  }
@@ -37,6 +37,7 @@ DirSQL(
37
37
  tables: list[Table] | None = None,
38
38
  ignore: list[str] | None = None,
39
39
  config: str | None = None,
40
+ extensions: list[dict] | None = None, # [{ "path": str, "entrypoint"?: str }]
40
41
  )
41
42
  ```
42
43
 
@@ -46,6 +47,7 @@ DirSQL::builder()
46
47
  .tables(tables) // optional; append with .table(t)
47
48
  .ignore(patterns) // optional
48
49
  .config(config_toml_path) // optional
50
+ .extensions(extensions) // optional; Extension { path, entrypoint }
49
51
  .build() // -> Result<DirSQL>
50
52
  ```
51
53
 
@@ -73,7 +75,8 @@ In Python, the constructor starts scanning in a background thread and returns im
73
75
  - `root` -- Path to the directory to index. Optional if `config` is supplied.
74
76
  - `tables` -- List of `Table` definitions. Each defines a SQLite table, a glob pattern, and an extract function.
75
77
  - `ignore` -- Optional list of glob patterns. Files matching any ignore pattern are skipped regardless of table globs.
76
- - `config` -- Optional path to a `.dirsql.toml` config file. Its `[[table]]` entries are appended to any programmatic `tables`; its `[dirsql].ignore` patterns are appended to any explicit `ignore`; its optional `[dirsql].root` supplies the root directory when `root` is not passed explicitly.
78
+ - `config` -- Optional path to a `.dirsql.toml` config file. Its `[[table]]` entries are appended to any programmatic `tables`; its `[dirsql].ignore` patterns are appended to any explicit `ignore`; its optional `[dirsql].root` supplies the root directory when `root` is not passed explicitly; its `[[dirsql.extension]]` entries are appended to any programmatic `extensions`.
79
+ - `extensions` -- Optional SQLite extensions to load onto the connection at startup, before any table DDL (enable → load → disable, so the SQL `load_extension()` function is never left exposed). Each entry pairs a shared-library `path` with an optional `entrypoint` init-symbol override (Python: `{ "path", "entrypoint"? }` dicts; Rust: `Extension { path, entrypoint }`). Programmatic entries load first, then any `[[dirsql.extension]]` from `config`. Available in the Python and Rust SDKs; the TypeScript constructor parameter is tracked in [#230](https://github.com/thekevinscott/dirsql/issues/230). See [Loading extensions](../cli/config.md#loading-extensions).
77
80
 
78
81
  ### Methods
79
82
 
@@ -159,7 +162,7 @@ JSON.stringify(db) // via db.toJSON()
159
162
 
160
163
  :::
161
164
 
162
- Returns the resolved construction state as a JSON-compatible value with fields `root`, `tables`, `ignore`, `persist`, `persist_path` (camelCase `persistPath` in TypeScript). Each table is `{ ddl, glob, strict }`. Excludes the original `config` path (already merged into `root` / `tables` / `ignore`), per-table `extract`, and per-table `name`. Available immediately after construction in Python and TypeScript; Rust's sync `build()` returns a ready instance.
165
+ Returns the resolved construction state as a JSON-compatible value with fields `root`, `tables`, `ignore`, `persist`, `persist_path` (camelCase `persistPath` in TypeScript). Each table is `{ ddl, glob, strict }`. The Python and Rust snapshots also include `extensions` -- an array of `{ path, entrypoint }` (empty when none are configured); the TypeScript `toJSON` snapshot will gain it with [#230](https://github.com/thekevinscott/dirsql/issues/230). Excludes the original `config` path (already merged into `root` / `tables` / `ignore`), per-table `extract`, and per-table `name`. Available immediately after construction in Python and TypeScript; Rust's sync `build()` returns a ready instance.
163
166
 
164
167
  ---
165
168
 
@@ -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.31"
7
+ version = "0.3.33"
8
8
  edition.workspace = true
9
9
  publish = false
10
10
  readme = "README.md"
@@ -80,6 +80,27 @@ db = DirSQL(
80
80
  )
81
81
  ```
82
82
 
83
+ ## Loading SQLite extensions
84
+
85
+ Pass `extensions` to load SQLite extension shared libraries onto the connection at startup (before any `CREATE TABLE`). Each entry is a dict with a `path` and an optional `entrypoint` init-symbol override:
86
+
87
+ ```python
88
+ db = DirSQL(
89
+ "./my-blog",
90
+ tables=[...],
91
+ extensions=[
92
+ {"path": "./ext/vec0.dylib", "entrypoint": "sqlite3_vec_init"},
93
+ {"path": "./ext/myext.so"}, # entrypoint derived from the filename
94
+ ],
95
+ )
96
+ await db.ready()
97
+
98
+ # The extension's functions are now callable in queries:
99
+ rows = await db.query("SELECT vec_version() AS v")
100
+ ```
101
+
102
+ `dirsql` enables extension loading only while loading the configured libraries, then disables it again, so the SQL `load_extension()` function is never exposed to your queries. Programmatic entries load first, followed by any `[[dirsql.extension]]` entries declared in a `config` file. See the [config reference](https://github.com/thekevinscott/dirsql/blob/main/docs/cli/config.md#loading-extensions).
103
+
83
104
  ## Watching for changes
84
105
 
85
106
  `db.watch()` returns an async iterator of row-level change events as files change on disk:
@@ -37,6 +37,7 @@ DirSQL(
37
37
  tables: list[Table] | None = None,
38
38
  ignore: list[str] | None = None,
39
39
  config: str | None = None,
40
+ extensions: list[dict] | None = None, # [{ "path": str, "entrypoint"?: str }]
40
41
  )
41
42
  ```
42
43
 
@@ -46,6 +47,7 @@ DirSQL::builder()
46
47
  .tables(tables) // optional; append with .table(t)
47
48
  .ignore(patterns) // optional
48
49
  .config(config_toml_path) // optional
50
+ .extensions(extensions) // optional; Extension { path, entrypoint }
49
51
  .build() // -> Result<DirSQL>
50
52
  ```
51
53
 
@@ -73,7 +75,8 @@ In Python, the constructor starts scanning in a background thread and returns im
73
75
  - `root` -- Path to the directory to index. Optional if `config` is supplied.
74
76
  - `tables` -- List of `Table` definitions. Each defines a SQLite table, a glob pattern, and an extract function.
75
77
  - `ignore` -- Optional list of glob patterns. Files matching any ignore pattern are skipped regardless of table globs.
76
- - `config` -- Optional path to a `.dirsql.toml` config file. Its `[[table]]` entries are appended to any programmatic `tables`; its `[dirsql].ignore` patterns are appended to any explicit `ignore`; its optional `[dirsql].root` supplies the root directory when `root` is not passed explicitly.
78
+ - `config` -- Optional path to a `.dirsql.toml` config file. Its `[[table]]` entries are appended to any programmatic `tables`; its `[dirsql].ignore` patterns are appended to any explicit `ignore`; its optional `[dirsql].root` supplies the root directory when `root` is not passed explicitly; its `[[dirsql.extension]]` entries are appended to any programmatic `extensions`.
79
+ - `extensions` -- Optional SQLite extensions to load onto the connection at startup, before any table DDL (enable → load → disable, so the SQL `load_extension()` function is never left exposed). Each entry pairs a shared-library `path` with an optional `entrypoint` init-symbol override (Python: `{ "path", "entrypoint"? }` dicts; Rust: `Extension { path, entrypoint }`). Programmatic entries load first, then any `[[dirsql.extension]]` from `config`. Available in the Python and Rust SDKs; the TypeScript constructor parameter is tracked in [#230](https://github.com/thekevinscott/dirsql/issues/230). See [Loading extensions](../cli/config.md#loading-extensions).
77
80
 
78
81
  ### Methods
79
82
 
@@ -159,7 +162,7 @@ JSON.stringify(db) // via db.toJSON()
159
162
 
160
163
  :::
161
164
 
162
- Returns the resolved construction state as a JSON-compatible value with fields `root`, `tables`, `ignore`, `persist`, `persist_path` (camelCase `persistPath` in TypeScript). Each table is `{ ddl, glob, strict }`. Excludes the original `config` path (already merged into `root` / `tables` / `ignore`), per-table `extract`, and per-table `name`. Available immediately after construction in Python and TypeScript; Rust's sync `build()` returns a ready instance.
165
+ Returns the resolved construction state as a JSON-compatible value with fields `root`, `tables`, `ignore`, `persist`, `persist_path` (camelCase `persistPath` in TypeScript). Each table is `{ ddl, glob, strict }`. The Python and Rust snapshots also include `extensions` -- an array of `{ path, entrypoint }` (empty when none are configured); the TypeScript `toJSON` snapshot will gain it with [#230](https://github.com/thekevinscott/dirsql/issues/230). Excludes the original `config` path (already merged into `root` / `tables` / `ignore`), per-table `extract`, and per-table `name`. Available immediately after construction in Python and TypeScript; Rust's sync `build()` returns a ready instance.
163
166
 
164
167
  ---
165
168
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  #[cfg(feature = "extension-module")]
14
14
  mod python {
15
- use ::dirsql::{DirSQL, Row, RowEvent, Table, Value, db::parse_table_name};
15
+ use ::dirsql::{DirSQL, Extension, Row, RowEvent, Table, Value, db::parse_table_name};
16
16
  use pyo3::exceptions::PyRuntimeError;
17
17
  use pyo3::prelude::*;
18
18
  use pyo3::types::{PyDict, PyList};
@@ -63,6 +63,20 @@ mod python {
63
63
  }
64
64
  }
65
65
 
66
+ /// Marshals a Python `{"path": str, "entrypoint"?: str}` mapping from the
67
+ /// `extensions=` constructor argument into a [`dirsql::Extension`]. Mirrors
68
+ /// the `[[dirsql.extension]]` config-file fields and the Rust builder's
69
+ /// `Extension { path, entrypoint }`. Paths are taken verbatim (the
70
+ /// programmatic surface does not resolve relative paths), matching
71
+ /// `DirSQLBuilder::extensions`.
72
+ #[derive(FromPyObject)]
73
+ struct PyExtensionSpec {
74
+ #[pyo3(item)]
75
+ path: String,
76
+ #[pyo3(item, default)]
77
+ entrypoint: Option<String>,
78
+ }
79
+
66
80
  /// A row event produced by the watch loop.
67
81
  ///
68
82
  /// `table` is `Optional[str]` because error events may occur before a
@@ -101,7 +115,7 @@ mod python {
101
115
  #[pymethods]
102
116
  impl PyDirSQL {
103
117
  #[new]
104
- #[pyo3(signature = (root=None, *, tables=None, ignore=None, config=None, persist=false, persist_path=None))]
118
+ #[pyo3(signature = (root=None, *, tables=None, ignore=None, config=None, persist=false, persist_path=None, extensions=None))]
105
119
  fn new(
106
120
  py: Python<'_>,
107
121
  root: Option<String>,
@@ -110,12 +124,22 @@ mod python {
110
124
  config: Option<String>,
111
125
  persist: bool,
112
126
  persist_path: Option<PathBuf>,
127
+ extensions: Option<Vec<PyExtensionSpec>>,
113
128
  ) -> PyResult<Self> {
114
129
  let rust_tables: Vec<Table> = tables
115
130
  .as_deref()
116
131
  .map(|ts| ts.iter().map(|t| build_table(py, t)).collect())
117
132
  .unwrap_or_default();
118
133
 
134
+ let rust_extensions: Vec<Extension> = extensions
135
+ .unwrap_or_default()
136
+ .into_iter()
137
+ .map(|e| Extension {
138
+ path: PathBuf::from(e.path),
139
+ entrypoint: e.entrypoint,
140
+ })
141
+ .collect();
142
+
119
143
  let inner = py
120
144
  .detach(move || {
121
145
  let mut builder = DirSQL::builder();
@@ -137,6 +161,9 @@ mod python {
137
161
  if let Some(p) = persist_path {
138
162
  builder = builder.persist_path(p);
139
163
  }
164
+ if !rust_extensions.is_empty() {
165
+ builder = builder.extensions(rust_extensions);
166
+ }
140
167
  builder.build()
141
168
  })
142
169
  .map_err(to_py_err)?;
@@ -16,7 +16,11 @@ license = "MIT"
16
16
  repository.workspace = true
17
17
  # `docs/` is a symlink to the workspace `docs/` so the markdown ships with the
18
18
  # crate; everything below is the VitePress build tooling we don't want to ship.
19
+ # `tests/` is the integration suite -- excluded so test files never ship in the
20
+ # published `.crate` (enforced by the `packaging` gate in CI). Inline unit tests
21
+ # stay (they are `#[cfg(test)]` modules inside `src/`, not separate files).
19
22
  exclude = [
23
+ "tests/",
20
24
  "docs/.vitepress",
21
25
  "docs/.claude",
22
26
  "docs/tests",
@@ -37,6 +37,7 @@ DirSQL(
37
37
  tables: list[Table] | None = None,
38
38
  ignore: list[str] | None = None,
39
39
  config: str | None = None,
40
+ extensions: list[dict] | None = None, # [{ "path": str, "entrypoint"?: str }]
40
41
  )
41
42
  ```
42
43
 
@@ -46,6 +47,7 @@ DirSQL::builder()
46
47
  .tables(tables) // optional; append with .table(t)
47
48
  .ignore(patterns) // optional
48
49
  .config(config_toml_path) // optional
50
+ .extensions(extensions) // optional; Extension { path, entrypoint }
49
51
  .build() // -> Result<DirSQL>
50
52
  ```
51
53
 
@@ -73,7 +75,8 @@ In Python, the constructor starts scanning in a background thread and returns im
73
75
  - `root` -- Path to the directory to index. Optional if `config` is supplied.
74
76
  - `tables` -- List of `Table` definitions. Each defines a SQLite table, a glob pattern, and an extract function.
75
77
  - `ignore` -- Optional list of glob patterns. Files matching any ignore pattern are skipped regardless of table globs.
76
- - `config` -- Optional path to a `.dirsql.toml` config file. Its `[[table]]` entries are appended to any programmatic `tables`; its `[dirsql].ignore` patterns are appended to any explicit `ignore`; its optional `[dirsql].root` supplies the root directory when `root` is not passed explicitly.
78
+ - `config` -- Optional path to a `.dirsql.toml` config file. Its `[[table]]` entries are appended to any programmatic `tables`; its `[dirsql].ignore` patterns are appended to any explicit `ignore`; its optional `[dirsql].root` supplies the root directory when `root` is not passed explicitly; its `[[dirsql.extension]]` entries are appended to any programmatic `extensions`.
79
+ - `extensions` -- Optional SQLite extensions to load onto the connection at startup, before any table DDL (enable → load → disable, so the SQL `load_extension()` function is never left exposed). Each entry pairs a shared-library `path` with an optional `entrypoint` init-symbol override (Python: `{ "path", "entrypoint"? }` dicts; Rust: `Extension { path, entrypoint }`). Programmatic entries load first, then any `[[dirsql.extension]]` from `config`. Available in the Python and Rust SDKs; the TypeScript constructor parameter is tracked in [#230](https://github.com/thekevinscott/dirsql/issues/230). See [Loading extensions](../cli/config.md#loading-extensions).
77
80
 
78
81
  ### Methods
79
82
 
@@ -159,7 +162,7 @@ JSON.stringify(db) // via db.toJSON()
159
162
 
160
163
  :::
161
164
 
162
- Returns the resolved construction state as a JSON-compatible value with fields `root`, `tables`, `ignore`, `persist`, `persist_path` (camelCase `persistPath` in TypeScript). Each table is `{ ddl, glob, strict }`. Excludes the original `config` path (already merged into `root` / `tables` / `ignore`), per-table `extract`, and per-table `name`. Available immediately after construction in Python and TypeScript; Rust's sync `build()` returns a ready instance.
165
+ Returns the resolved construction state as a JSON-compatible value with fields `root`, `tables`, `ignore`, `persist`, `persist_path` (camelCase `persistPath` in TypeScript). Each table is `{ ddl, glob, strict }`. The Python and Rust snapshots also include `extensions` -- an array of `{ path, entrypoint }` (empty when none are configured); the TypeScript `toJSON` snapshot will gain it with [#230](https://github.com/thekevinscott/dirsql/issues/230). Excludes the original `config` path (already merged into `root` / `tables` / `ignore`), per-table `extract`, and per-table `name`. Available immediately after construction in Python and TypeScript; Rust's sync `build()` returns a ready instance.
163
166
 
164
167
  ---
165
168
 
@@ -21,7 +21,7 @@ use std::sync::{Arc, Mutex};
21
21
  use serde::Deserialize;
22
22
 
23
23
  use crate::db::parse_table_name;
24
- use crate::{DirSQL, Row, Table, Value};
24
+ use crate::{DirSQL, Extension, Row, Table, Value};
25
25
 
26
26
  /// NDJSON helper subprocess + the shared IO it dispatches over.
27
27
  pub struct InterpretHelper {
@@ -57,6 +57,12 @@ struct HandshakeState {
57
57
  persist: bool,
58
58
  #[serde(default, alias = "persistPath")]
59
59
  persist_path: Option<String>,
60
+ /// SQLite extensions the config declared (`{path, entrypoint?}` each).
61
+ /// Defaults to empty so a handshake from an SDK that doesn't yet emit
62
+ /// the key still parses. Paths are taken verbatim — the SDK already
63
+ /// resolved config-relative paths before serializing the snapshot.
64
+ #[serde(default)]
65
+ extensions: Vec<HandshakeExtension>,
60
66
  }
61
67
 
62
68
  #[derive(Debug, Deserialize)]
@@ -67,6 +73,13 @@ struct HandshakeTable {
67
73
  strict: bool,
68
74
  }
69
75
 
76
+ #[derive(Debug, Deserialize)]
77
+ struct HandshakeExtension {
78
+ path: String,
79
+ #[serde(default)]
80
+ entrypoint: Option<String>,
81
+ }
82
+
70
83
  /// Response per extract request. The discriminator `type` field and the
71
84
  /// echoed `id` field are ignored — V1 of the protocol is strictly
72
85
  /// sequential, so the helper's reply is unambiguous.
@@ -149,6 +162,14 @@ fn parse_handshake(stdout: &mut dyn BufRead) -> Result<NativeConfig, String> {
149
162
  ignore: state.ignore,
150
163
  persist: state.persist,
151
164
  persist_path: state.persist_path.map(PathBuf::from),
165
+ extensions: state
166
+ .extensions
167
+ .into_iter()
168
+ .map(|e| Extension {
169
+ path: PathBuf::from(e.path),
170
+ entrypoint: e.entrypoint,
171
+ })
172
+ .collect(),
152
173
  })
153
174
  }
154
175
 
@@ -203,6 +224,10 @@ pub struct NativeConfig {
203
224
  pub ignore: Vec<String>,
204
225
  pub persist: bool,
205
226
  pub persist_path: Option<PathBuf>,
227
+ /// SQLite extensions to load onto the connection at startup. Resolved
228
+ /// verbatim from the handshake (the SDK already merged config-file and
229
+ /// programmatic entries and resolved relative paths). See [`Extension`].
230
+ pub extensions: Vec<Extension>,
206
231
  }
207
232
 
208
233
  /// Build a [`DirSQL`] from a spawned interpret helper. Each table's
@@ -229,7 +254,8 @@ pub fn build_dirsql(helper: Arc<InterpretHelper>, config: NativeConfig) -> Resul
229
254
  let mut builder = DirSQL::builder()
230
255
  .root(config.root)
231
256
  .tables(tables)
232
- .ignore(config.ignore);
257
+ .ignore(config.ignore)
258
+ .extensions(config.extensions);
233
259
  if config.persist {
234
260
  builder = builder.persist(true);
235
261
  }
@@ -317,6 +343,39 @@ mod tests {
317
343
  assert_eq!(cfg.ignore, vec!["a".to_string(), "b".to_string()]);
318
344
  }
319
345
 
346
+ #[test]
347
+ fn parse_handshake_carries_extensions_with_path_and_entrypoint() {
348
+ // A `.py` / `.js` config that declares `extensions=[...]` serializes
349
+ // them into the handshake `state`; the parser must carry both `path`
350
+ // and the optional `entrypoint` through to the resolved config (#229).
351
+ let line = br#"{"type":"config","state":{"root":"/r","tables":[],"extensions":[{"path":"/ext/vec0.so","entrypoint":"sqlite3_vec_init"}]}}
352
+ "#;
353
+ let cfg = parse_handshake(&mut Cursor::new(line.as_slice())).unwrap();
354
+ assert_eq!(cfg.extensions.len(), 1);
355
+ assert_eq!(cfg.extensions[0].path, PathBuf::from("/ext/vec0.so"));
356
+ assert_eq!(
357
+ cfg.extensions[0].entrypoint.as_deref(),
358
+ Some("sqlite3_vec_init"),
359
+ );
360
+ }
361
+
362
+ #[test]
363
+ fn parse_handshake_extension_entrypoint_is_optional() {
364
+ let line = br#"{"type":"config","state":{"root":"/r","tables":[],"extensions":[{"path":"/ext/a.so"}]}}
365
+ "#;
366
+ let cfg = parse_handshake(&mut Cursor::new(line.as_slice())).unwrap();
367
+ assert_eq!(cfg.extensions.len(), 1);
368
+ assert!(cfg.extensions[0].entrypoint.is_none());
369
+ }
370
+
371
+ #[test]
372
+ fn parse_handshake_defaults_extensions_to_empty_when_absent() {
373
+ let line = br#"{"type":"config","state":{"root":"/r","tables":[]}}
374
+ "#;
375
+ let cfg = parse_handshake(&mut Cursor::new(line.as_slice())).unwrap();
376
+ assert!(cfg.extensions.is_empty());
377
+ }
378
+
320
379
  #[test]
321
380
  fn parse_handshake_errors_on_empty_stream() {
322
381
  let mut reader = Cursor::new(b"".as_slice());
@@ -505,6 +564,35 @@ mod tests {
505
564
  assert!(cache_path.exists(), "persist build should create the cache");
506
565
  }
507
566
 
567
+ #[test]
568
+ fn build_dirsql_threads_extensions_into_the_builder() {
569
+ // A handshake whose `extensions` names a missing shared library must
570
+ // fail the build, proving the parsed extensions reach the core's
571
+ // load-at-startup path (enable -> load -> disable). (#229)
572
+ let tmp = tempfile::TempDir::new().unwrap();
573
+ std::fs::write(tmp.path().join("a.json"), b"{}").unwrap();
574
+
575
+ let handshake = format!(
576
+ r#"{{"type":"config","state":{{"root":"{}","tables":[{{"ddl":"CREATE TABLE papers (title TEXT)","glob":"*.json"}}],"extensions":[{{"path":"/nonexistent/dirsql-no-such-ext.so"}}]}}}}"#,
577
+ tmp.path().display(),
578
+ );
579
+ let (helper, config) = spawn_fake_helper(
580
+ &handshake,
581
+ r#"{"type":"result","id":1,"ok":true,"rows":[{"title":"y"}]}"#,
582
+ );
583
+ assert_eq!(config.extensions.len(), 1);
584
+ assert_eq!(
585
+ config.extensions[0].path,
586
+ PathBuf::from("/nonexistent/dirsql-no-such-ext.so"),
587
+ );
588
+
589
+ let err = match build_dirsql(helper, config) {
590
+ Ok(_) => panic!("expected build to fail on a missing extension"),
591
+ Err(e) => e,
592
+ };
593
+ assert!(err.contains("failed to load extension"), "got: {err}");
594
+ }
595
+
508
596
  #[test]
509
597
  fn build_dirsql_round_trip_with_fake_helper_invokes_extract_per_matched_file() {
510
598
  // Create a tempdir with two matching files so the scan invokes
@@ -683,6 +771,7 @@ mod tests {
683
771
  ignore: Vec::new(),
684
772
  persist: false,
685
773
  persist_path: None,
774
+ extensions: Vec::new(),
686
775
  };
687
776
  // `DirSQL` doesn't impl Debug, so we can't use `unwrap_err` directly.
688
777
  let err = match build_dirsql(helper, config) {