dirsql 0.3.21__tar.gz → 0.3.23__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.23}/Cargo.lock +1 -1
  2. {dirsql-0.3.21 → dirsql-0.3.23}/Cargo.toml +4 -0
  3. {dirsql-0.3.21 → dirsql-0.3.23}/PKG-INFO +1 -1
  4. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/_async_test.py +19 -19
  5. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/binary_path_test.py +1 -1
  6. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/main_test.py +13 -16
  7. {dirsql-0.3.21 → dirsql-0.3.23}/docs/cli/config.md +27 -0
  8. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/Cargo.toml +1 -1
  9. {dirsql-0.3.21/packages/rust → dirsql-0.3.23/packages/python}/docs/cli/config.md +27 -0
  10. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/Cargo.toml +1 -1
  11. {dirsql-0.3.21/packages/python → dirsql-0.3.23/packages/rust}/docs/cli/config.md +27 -0
  12. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/config.rs +137 -2
  13. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/db.rs +96 -0
  14. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/lib.rs +96 -0
  15. dirsql-0.3.23/packages/rust/tests/extensions.rs +247 -0
  16. {dirsql-0.3.21 → dirsql-0.3.23}/README.md +0 -0
  17. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/__init__.py +0 -0
  18. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/_async.py +0 -0
  19. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/_dirsql.pyi +0 -0
  20. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/__init__.py +0 -0
  21. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/binary_path.py +0 -0
  22. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/__init__.py +0 -0
  23. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/dispatch_extract.py +0 -0
  24. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/dispatch_extract_test.py +0 -0
  25. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/load_app.py +0 -0
  26. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/load_app_test.py +0 -0
  27. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/run.py +0 -0
  28. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/run_test.py +0 -0
  29. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/write_message.py +0 -0
  30. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/write_message_test.py +0 -0
  31. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/is_windows.py +0 -0
  32. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/is_windows_test.py +0 -0
  33. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/main.py +0 -0
  34. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/py.typed +0 -0
  35. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/resolve_config.py +0 -0
  36. {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/resolve_config_test.py +0 -0
  37. {dirsql-0.3.21 → dirsql-0.3.23}/docs/.claude/CLAUDE.md +0 -0
  38. {dirsql-0.3.21 → dirsql-0.3.23}/docs/.vitepress/config.ts +0 -0
  39. {dirsql-0.3.21 → dirsql-0.3.23}/docs/.vitepress/theme/index.ts +0 -0
  40. {dirsql-0.3.21 → dirsql-0.3.23}/docs/.vitepress/theme/lang.ts +0 -0
  41. {dirsql-0.3.21 → dirsql-0.3.23}/docs/AGENTS.md +0 -0
  42. {dirsql-0.3.21 → dirsql-0.3.23}/docs/api/index.md +0 -0
  43. {dirsql-0.3.21 → dirsql-0.3.23}/docs/cli/http-api.md +0 -0
  44. {dirsql-0.3.21 → dirsql-0.3.23}/docs/cli/index.md +0 -0
  45. {dirsql-0.3.21 → dirsql-0.3.23}/docs/cli/init.md +0 -0
  46. {dirsql-0.3.21 → dirsql-0.3.23}/docs/cli/server.md +0 -0
  47. {dirsql-0.3.21 → dirsql-0.3.23}/docs/getting-started.md +0 -0
  48. {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/async.md +0 -0
  49. {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/crdt.md +0 -0
  50. {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/persistence.md +0 -0
  51. {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/querying.md +0 -0
  52. {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/tables.md +0 -0
  53. {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/watching.md +0 -0
  54. {dirsql-0.3.21 → dirsql-0.3.23}/docs/index.md +0 -0
  55. {dirsql-0.3.21 → dirsql-0.3.23}/docs/migrations.md +0 -0
  56. {dirsql-0.3.21 → dirsql-0.3.23}/docs/package.json +0 -0
  57. {dirsql-0.3.21 → dirsql-0.3.23}/docs/playwright.config.ts +0 -0
  58. {dirsql-0.3.21 → dirsql-0.3.23}/docs/pnpm-lock.yaml +0 -0
  59. {dirsql-0.3.21 → dirsql-0.3.23}/docs/pnpm-workspace.yaml +0 -0
  60. {dirsql-0.3.21 → dirsql-0.3.23}/docs/tests/integration/home.spec.ts +0 -0
  61. {dirsql-0.3.21 → dirsql-0.3.23}/docs/tests/integration/language-flag.spec.ts +0 -0
  62. {dirsql-0.3.21 → dirsql-0.3.23}/docs/tests/unit/config.test.ts +0 -0
  63. {dirsql-0.3.21 → dirsql-0.3.23}/docs/tests/unit/lang.test.ts +0 -0
  64. {dirsql-0.3.21 → dirsql-0.3.23}/docs/vitest.config.ts +0 -0
  65. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/README.md +0 -0
  66. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/conftest.py +0 -0
  67. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/.claude/CLAUDE.md +0 -0
  68. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/.vitepress/config.ts +0 -0
  69. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  70. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  71. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/AGENTS.md +0 -0
  72. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/api/index.md +0 -0
  73. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/cli/http-api.md +0 -0
  74. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/cli/index.md +0 -0
  75. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/cli/init.md +0 -0
  76. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/cli/server.md +0 -0
  77. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/getting-started.md +0 -0
  78. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/async.md +0 -0
  79. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/crdt.md +0 -0
  80. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/persistence.md +0 -0
  81. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/querying.md +0 -0
  82. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/tables.md +0 -0
  83. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/watching.md +0 -0
  84. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/index.md +0 -0
  85. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/migrations.md +0 -0
  86. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/package.json +0 -0
  87. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/playwright.config.ts +0 -0
  88. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/pnpm-lock.yaml +0 -0
  89. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/pnpm-workspace.yaml +0 -0
  90. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  91. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  92. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/tests/unit/config.test.ts +0 -0
  93. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  94. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/vitest.config.ts +0 -0
  95. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/src/lib.rs +0 -0
  96. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/__init__.py +0 -0
  97. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/conftest.py +0 -0
  98. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/e2e/__init__.py +0 -0
  99. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/data/a/meta.json +0 -0
  100. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/data/b/meta.json +0 -0
  101. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/dirsql.config.py +0 -0
  102. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/interpret/data/a/meta.json +0 -0
  103. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/interpret/data/b/meta.json +0 -0
  104. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config.py +0 -0
  105. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_no_app.py +0 -0
  106. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_raises.py +0 -0
  107. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__init__.py +0 -0
  108. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/interpret_subprocess.py +0 -0
  109. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_async_dirsql.py +0 -0
  110. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_binding.py +0 -0
  111. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_dirsql.py +0 -0
  112. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_docs_examples.py +0 -0
  113. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_docs_gaps.py +0 -0
  114. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_from_config.py +0 -0
  115. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_interpret.py +0 -0
  116. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_native_config.py +0 -0
  117. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_persist.py +0 -0
  118. {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_serialization.py +0 -0
  119. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/README.md +0 -0
  120. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/benches/db_bench.rs +0 -0
  121. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/benches/differ_bench.rs +0 -0
  122. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/benches/matcher_bench.rs +0 -0
  123. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/benches/scanner_bench.rs +0 -0
  124. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/api/index.md +0 -0
  125. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/cli/http-api.md +0 -0
  126. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/cli/index.md +0 -0
  127. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/cli/init.md +0 -0
  128. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/cli/server.md +0 -0
  129. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/getting-started.md +0 -0
  130. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/async.md +0 -0
  131. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/crdt.md +0 -0
  132. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/persistence.md +0 -0
  133. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/querying.md +0 -0
  134. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/tables.md +0 -0
  135. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/watching.md +0 -0
  136. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/index.md +0 -0
  137. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/migrations.md +0 -0
  138. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/bin/dirsql.rs +0 -0
  139. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/init.rs +0 -0
  140. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/mod.rs +0 -0
  141. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/native_config.rs +0 -0
  142. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/router.rs +0 -0
  143. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/serialize.rs +0 -0
  144. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/server.rs +0 -0
  145. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/differ.rs +0 -0
  146. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/matcher.rs +0 -0
  147. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/persist.rs +0 -0
  148. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/scanner.rs +0 -0
  149. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/watcher.rs +0 -0
  150. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/async_sdk.rs +0 -0
  151. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/cli_e2e.rs +0 -0
  152. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/cli_integration.rs +0 -0
  153. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/code_review_findings.rs +0 -0
  154. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/docs_examples.rs +0 -0
  155. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/docs_gaps.rs +0 -0
  156. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/from_config.rs +0 -0
  157. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/init_e2e.rs +0 -0
  158. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/init_integration.rs +0 -0
  159. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/persist.rs +0 -0
  160. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/readonly_query.rs +0 -0
  161. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/sdk.rs +0 -0
  162. {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/serialization.rs +0 -0
  163. {dirsql-0.3.21 → dirsql-0.3.23}/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.23"
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.23
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'
@@ -1,10 +1,10 @@
1
1
  """Unit tests for the DirSQL async wrapper."""
2
2
 
3
- from __future__ import annotations
3
+ from unittest.mock import patch
4
4
 
5
5
  import pytest
6
6
 
7
- from dirsql import _async as async_mod
7
+ import dirsql._async as async_mod
8
8
 
9
9
 
10
10
  class _FakeRustDirSQL:
@@ -50,32 +50,32 @@ class _FakeWatcherDb:
50
50
  def describe_DirSQL_async():
51
51
  def describe_ready_and_query():
52
52
  @pytest.mark.asyncio
53
- async def it_uses_the_background_db(monkeypatch):
54
- monkeypatch.setattr(async_mod, "_RustDirSQL", _FakeRustDirSQL)
55
-
56
- db = async_mod.DirSQL("/tmp/root", tables=["table-a"], ignore=["**/*.tmp"])
57
- await db.ready()
53
+ async def it_uses_the_background_db():
54
+ with patch.object(async_mod, "_RustDirSQL", _FakeRustDirSQL):
55
+ db = async_mod.DirSQL(
56
+ "/tmp/root", tables=["table-a"], ignore=["**/*.tmp"]
57
+ )
58
+ await db.ready()
58
59
 
59
- results = await db.query("SELECT 1")
60
+ results = await db.query("SELECT 1")
60
61
 
61
- assert db._db.root == "/tmp/root"
62
- assert db._db.tables == ["table-a"]
63
- assert db._db.ignore == ["**/*.tmp"]
64
- assert db._db.query_calls == ["SELECT 1"]
65
- assert results == [{"sql": "SELECT 1"}]
62
+ assert db._db.root == "/tmp/root"
63
+ assert db._db.tables == ["table-a"]
64
+ assert db._db.ignore == ["**/*.tmp"]
65
+ assert db._db.query_calls == ["SELECT 1"]
66
+ assert results == [{"sql": "SELECT 1"}]
66
67
 
67
68
  @pytest.mark.asyncio
68
- async def it_propagates_initialization_errors(monkeypatch):
69
+ async def it_propagates_initialization_errors():
69
70
  class _BoomDirSQL:
70
71
  def __init__(self, *args, **kwargs):
71
72
  raise RuntimeError("boom")
72
73
 
73
- monkeypatch.setattr(async_mod, "_RustDirSQL", _BoomDirSQL)
74
+ with patch.object(async_mod, "_RustDirSQL", _BoomDirSQL):
75
+ db = async_mod.DirSQL("/tmp/root", tables=["table-a"])
74
76
 
75
- db = async_mod.DirSQL("/tmp/root", tables=["table-a"])
76
-
77
- with pytest.raises(RuntimeError, match="boom"):
78
- await db.ready()
77
+ with pytest.raises(RuntimeError, match="boom"):
78
+ await db.ready()
79
79
 
80
80
  def describe_watch_stream():
81
81
  @pytest.mark.asyncio
@@ -5,7 +5,7 @@ from unittest.mock import patch
5
5
 
6
6
  import pytest
7
7
 
8
- from dirsql.cli import binary_path as bp_module
8
+ import dirsql.cli.binary_path as bp_module
9
9
  from dirsql.cli.binary_path import binary_path
10
10
 
11
11
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  import io
4
4
  import os
5
- import subprocess
6
5
  import sys
7
6
  from unittest.mock import patch
8
7
 
@@ -35,7 +34,9 @@ def describe_main():
35
34
  with (
36
35
  patch.object(main_module, "binary_path", return_value="C:/dirsql.exe"),
37
36
  patch.object(main_module, "is_windows", return_value=True),
38
- patch.object(subprocess, "run", return_value=_Completed(7)) as run,
37
+ patch(
38
+ "dirsql.cli.main.subprocess.run", return_value=_Completed(7)
39
+ ) as run,
39
40
  ):
40
41
  assert main(["--version"]) == 7
41
42
  run.assert_called_once_with(["C:/dirsql.exe", "--version"])
@@ -44,7 +45,7 @@ def describe_main():
44
45
  with (
45
46
  patch.object(main_module, "binary_path", return_value="C:/dirsql.exe"),
46
47
  patch.object(main_module, "is_windows", return_value=True),
47
- patch.object(subprocess, "run", return_value=_Completed(0)),
48
+ patch("dirsql.cli.main.subprocess.run", return_value=_Completed(0)),
48
49
  patch.object(os, "execv") as execv,
49
50
  ):
50
51
  main([])
@@ -81,7 +82,9 @@ def describe_main():
81
82
  patch.object(sys, "argv", ["dirsql", "--help"]),
82
83
  patch.object(main_module, "binary_path", return_value="C:/dirsql.exe"),
83
84
  patch.object(main_module, "is_windows", return_value=True),
84
- patch.object(subprocess, "run", return_value=_Completed(0)) as run,
85
+ patch(
86
+ "dirsql.cli.main.subprocess.run", return_value=_Completed(0)
87
+ ) as run,
85
88
  ):
86
89
  main(argv=None)
87
90
  run.assert_called_once_with(["C:/dirsql.exe", "--help"])
@@ -93,12 +96,10 @@ def describe_main():
93
96
 
94
97
  def it_dispatches_to_interpret_run_and_returns_its_exit_code():
95
98
  # main.py does `from .interpret.run import run` lazily, so
96
- # patching the `run` function on the submodule is sufficient
97
- # -- each call resolves the name freshly from the submodule.
98
- from .interpret import run as run_submod
99
-
99
+ # patching the `run` function on its module by its dotted path
100
+ # is sufficient -- each call resolves the name freshly.
100
101
  with (
101
- patch.object(run_submod, "run", return_value=0) as interpret_run,
102
+ patch("dirsql.cli.interpret.run.run", return_value=0) as interpret_run,
102
103
  patch.object(main_module, "binary_path") as binary_path,
103
104
  ):
104
105
  assert main(["interpret", "config.py"]) == 0
@@ -106,15 +107,11 @@ def describe_main():
106
107
  binary_path.assert_not_called()
107
108
 
108
109
  def it_propagates_a_nonzero_interpret_exit():
109
- from .interpret import run as run_submod
110
-
111
- with patch.object(run_submod, "run", return_value=2):
110
+ with patch("dirsql.cli.interpret.run.run", return_value=2):
112
111
  assert main(["interpret", "bad.py"]) == 2
113
112
 
114
113
  def it_returns_130_on_keyboard_interrupt():
115
- from .interpret import run as run_submod
116
-
117
- with patch.object(run_submod, "run", side_effect=KeyboardInterrupt()):
114
+ with patch("dirsql.cli.interpret.run.run", side_effect=KeyboardInterrupt()):
118
115
  assert main(["interpret", "config.py"]) == 130
119
116
 
120
117
  def it_does_not_intercept_when_interpret_is_not_argv_0():
@@ -123,7 +120,7 @@ def describe_main():
123
120
  with (
124
121
  patch.object(main_module, "binary_path", return_value="/bin/dirsql"),
125
122
  patch.object(main_module, "is_windows", return_value=True),
126
- patch.object(subprocess, "run", return_value=_Completed(0)),
123
+ patch("dirsql.cli.main.subprocess.run", return_value=_Completed(0)),
127
124
  ):
128
125
  main(["--verbose", "interpret", "config.py"])
129
126
  # binary_path was reached, meaning we did NOT take the
@@ -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.23"
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
  }