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.
- {dirsql-0.3.21 → dirsql-0.3.23}/Cargo.lock +1 -1
- {dirsql-0.3.21 → dirsql-0.3.23}/Cargo.toml +4 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/PKG-INFO +1 -1
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/_async_test.py +19 -19
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/binary_path_test.py +1 -1
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/main_test.py +13 -16
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/cli/config.md +27 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.21/packages/rust → dirsql-0.3.23/packages/python}/docs/cli/config.md +27 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/Cargo.toml +1 -1
- {dirsql-0.3.21/packages/python → dirsql-0.3.23/packages/rust}/docs/cli/config.md +27 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/config.rs +137 -2
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/db.rs +96 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/lib.rs +96 -0
- dirsql-0.3.23/packages/rust/tests/extensions.rs +247 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/README.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/_async.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/dispatch_extract.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/dispatch_extract_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/load_app.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/load_app_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/run.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/run_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/write_message.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/interpret/write_message_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/is_windows_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/py.typed +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/resolve_config.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/dirsql/resolve_config_test.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/AGENTS.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/api/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/cli/http-api.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/cli/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/cli/init.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/cli/server.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/getting-started.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/async.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/querying.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/tables.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/guide/watching.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/migrations.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/package.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/README.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/conftest.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/api/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/cli/http-api.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/data/a/meta.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/data/b/meta.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/dirsql.config.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/interpret/data/a/meta.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/interpret/data/b/meta.json +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_no_app.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_raises.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/interpret_subprocess.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_async_dirsql.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_binding.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_dirsql.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_docs_examples.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_docs_gaps.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_from_config.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_interpret.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_native_config.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_persist.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/python/tests/integration/test_serialization.py +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/README.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/api/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/cli/http-api.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/native_config.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/async_sdk.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/cli_e2e.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/cli_integration.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/code_review_findings.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/docs_examples.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/docs_gaps.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/from_config.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/init_e2e.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/init_integration.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/persist.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/readonly_query.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/sdk.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/packages/rust/tests/serialization.rs +0 -0
- {dirsql-0.3.21 → dirsql-0.3.23}/pyproject.toml +0 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
[workspace]
|
|
2
2
|
members = ["packages/python", "packages/rust"]
|
|
3
|
+
# Test-only loadable-extension fixture, built on demand by
|
|
4
|
+
# packages/rust/tests/extensions.rs. Excluded so it isn't built/linted by
|
|
5
|
+
# `cargo build --workspace` / `cargo clippy --workspace`.
|
|
6
|
+
exclude = ["packages/rust/tests/fixtures/testext"]
|
|
3
7
|
resolver = "2"
|
|
4
8
|
|
|
5
9
|
[workspace.package]
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"""Unit tests for the DirSQL async wrapper."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from unittest.mock import patch
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
|
|
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(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
+
results = await db.query("SELECT 1")
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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(
|
|
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
|
-
|
|
74
|
+
with patch.object(async_mod, "_RustDirSQL", _BoomDirSQL):
|
|
75
|
+
db = async_mod.DirSQL("/tmp/root", tables=["table-a"])
|
|
74
76
|
|
|
75
|
-
|
|
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
|
|
@@ -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
|
|
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.
|
|
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
|
|
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
|
|
97
|
-
# -- each call resolves the name freshly
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|