dirsql 0.3.45__tar.gz → 0.3.47__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.45 → dirsql-0.3.47}/Cargo.lock +1 -1
- {dirsql-0.3.45 → dirsql-0.3.47}/PKG-INFO +1 -1
- {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/_async.py +23 -1
- {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/cli/main.py +9 -0
- dirsql-0.3.47/dirsql/cli/resolve_config_extensions.py +81 -0
- dirsql-0.3.47/dirsql/resolve_extension.py +106 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/api/index.md +1 -1
- {dirsql-0.3.45/packages/python → dirsql-0.3.47}/docs/cli/config.md +72 -4
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/cli/http-api.md +9 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.45/packages/rust → dirsql-0.3.47/packages/python}/docs/api/index.md +1 -1
- {dirsql-0.3.45 → dirsql-0.3.47/packages/python}/docs/cli/config.md +72 -4
- {dirsql-0.3.45/packages/rust → dirsql-0.3.47/packages/python}/docs/cli/http-api.md +9 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/e2e-attestation.json +2 -2
- {dirsql-0.3.45/packages/python → dirsql-0.3.47/packages/rust}/docs/api/index.md +1 -1
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/cli/config.md +72 -4
- {dirsql-0.3.45/packages/python → dirsql-0.3.47/packages/rust}/docs/cli/http-api.md +9 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/bin/dirsql.rs +96 -4
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/cli/mod.rs +57 -1
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/cli/router.rs +81 -14
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/cli/server.rs +1 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/config.rs +65 -3
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/lib.rs +32 -11
- {dirsql-0.3.45 → dirsql-0.3.47}/Cargo.toml +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/README.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/__init__.py +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/cli/interpret/__init__.py +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/py.typed +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/AGENTS.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/cli/index.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/cli/init.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/cli/server.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/getting-started.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/async.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/querying.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/tables.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/watching.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/index.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/migrations.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/package.json +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/README.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/conftest.py +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/README.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/command.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/db.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.45 → dirsql-0.3.47}/pyproject.toml +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Async-by-default DirSQL wrapper."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import os
|
|
4
5
|
|
|
5
6
|
from dirsql._dirsql import DirSQL as _RustDirSQL
|
|
7
|
+
from dirsql.resolve_extension import resolve_extension_path
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class _WatchStream:
|
|
@@ -91,13 +93,33 @@ class DirSQL:
|
|
|
91
93
|
config=self._config,
|
|
92
94
|
persist=self._persist,
|
|
93
95
|
persist_path=self._persist_path,
|
|
94
|
-
extensions=self.
|
|
96
|
+
extensions=self._resolved_extensions(),
|
|
95
97
|
)
|
|
96
98
|
except Exception as exc:
|
|
97
99
|
self._init_error = exc
|
|
98
100
|
finally:
|
|
99
101
|
self._ready_event.set()
|
|
100
102
|
|
|
103
|
+
def _resolved_extensions(self):
|
|
104
|
+
"""Resolve each programmatic extension's ``path`` to a loadable file.
|
|
105
|
+
|
|
106
|
+
A bare package name is resolved to the loadable installed in the runtime
|
|
107
|
+
env (#298); path-looking values are passed through verbatim (mirroring
|
|
108
|
+
the Rust builder, which takes programmatic paths as-is). Config-file
|
|
109
|
+
``[[dirsql.extension]]`` entries are resolved by the Rust core, not here.
|
|
110
|
+
"""
|
|
111
|
+
if not self._extensions:
|
|
112
|
+
return self._extensions
|
|
113
|
+
return [
|
|
114
|
+
{
|
|
115
|
+
"path": resolve_extension_path(
|
|
116
|
+
e["path"], base=os.getcwd(), resolve_relative=False
|
|
117
|
+
),
|
|
118
|
+
"entrypoint": e.get("entrypoint"),
|
|
119
|
+
}
|
|
120
|
+
for e in self._extensions
|
|
121
|
+
]
|
|
122
|
+
|
|
101
123
|
async def ready(self):
|
|
102
124
|
"""Wait until the initial scan is complete.
|
|
103
125
|
|
|
@@ -10,6 +10,7 @@ import sys
|
|
|
10
10
|
|
|
11
11
|
from .binary_path import binary_path
|
|
12
12
|
from .is_windows import is_windows
|
|
13
|
+
from .resolve_config_extensions import with_resolved_extensions
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def main(argv: list[str] | None = None) -> int:
|
|
@@ -22,6 +23,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
22
23
|
print(f"dirsql: {exc}", file=sys.stderr)
|
|
23
24
|
return 1
|
|
24
25
|
|
|
26
|
+
# Resolve any package-name extensions in a TOML config here (the binary
|
|
27
|
+
# can't) and pass them as `--extension` flags; a no-op otherwise (#227).
|
|
28
|
+
try:
|
|
29
|
+
argv = with_resolved_extensions(argv)
|
|
30
|
+
except Exception as exc:
|
|
31
|
+
print(f"dirsql: {exc}", file=sys.stderr)
|
|
32
|
+
return 1
|
|
33
|
+
|
|
25
34
|
if is_windows():
|
|
26
35
|
completed = subprocess.run([binary, *argv])
|
|
27
36
|
return completed.returncode
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Launcher-side resolution of a TOML config's ``[[dirsql.extension]]`` entries.
|
|
2
|
+
|
|
3
|
+
Mirrors the TypeScript launcher. The compiled ``dirsql`` binary reads a
|
|
4
|
+
``.dirsql.toml`` itself and loads its extensions literally -- it has no
|
|
5
|
+
``importlib``, so it cannot resolve a bare **package name** (#227). This
|
|
6
|
+
launcher can. When a TOML config names an extension by package name, we resolve
|
|
7
|
+
every one of its extensions here and pass the resolved literal paths to the
|
|
8
|
+
binary via repeatable ``--extension`` flags; the binary then loads those and
|
|
9
|
+
ignores the config's own extension entries (the Rust ``--extension`` flag /
|
|
10
|
+
``suppress_config_extensions``).
|
|
11
|
+
|
|
12
|
+
Native-language configs (``.py`` / ``.js`` / ``.mjs`` / ``.cjs``) are untouched:
|
|
13
|
+
the binary dispatches those to ``dirsql interpret``, whose handshake already
|
|
14
|
+
carries resolved paths.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import tomllib
|
|
21
|
+
|
|
22
|
+
from ..resolve_extension import is_bare_name, resolve_extension_path
|
|
23
|
+
|
|
24
|
+
# Config extensions the binary dispatches to `dirsql interpret`; never
|
|
25
|
+
# pre-resolved here (that path resolves via the handshake).
|
|
26
|
+
_NATIVE_SUFFIXES = (".py", ".js", ".mjs", ".cjs")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _config_path_from_argv(argv: list[str]) -> str:
|
|
30
|
+
"""The ``--config`` value (``--config X`` or ``--config=X``), or the default."""
|
|
31
|
+
i = 0
|
|
32
|
+
while i < len(argv):
|
|
33
|
+
a = argv[i]
|
|
34
|
+
if a == "--config":
|
|
35
|
+
return argv[i + 1] if i + 1 < len(argv) else ""
|
|
36
|
+
if a.startswith("--config="):
|
|
37
|
+
return a[len("--config=") :]
|
|
38
|
+
i += 1
|
|
39
|
+
return "./.dirsql.toml"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def with_resolved_extensions(argv: list[str]) -> list[str]:
|
|
43
|
+
"""Return ``argv`` plus ``--extension`` flags when the TOML config names an
|
|
44
|
+
extension by package name; otherwise return ``argv`` unchanged. Raises if a
|
|
45
|
+
package name cannot be resolved (the launcher surfaces a clean error)."""
|
|
46
|
+
if argv and argv[0] == "init":
|
|
47
|
+
return argv
|
|
48
|
+
config_path = _config_path_from_argv(argv)
|
|
49
|
+
if config_path.endswith(_NATIVE_SUFFIXES):
|
|
50
|
+
return argv
|
|
51
|
+
if not os.path.isfile(config_path):
|
|
52
|
+
return argv
|
|
53
|
+
try:
|
|
54
|
+
with open(config_path, "rb") as f:
|
|
55
|
+
doc = tomllib.load(f)
|
|
56
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
57
|
+
# Leave a malformed / unreadable config for the binary to report.
|
|
58
|
+
return argv
|
|
59
|
+
|
|
60
|
+
cfg = doc.get("dirsql") or {}
|
|
61
|
+
entries = cfg.get("extension") or []
|
|
62
|
+
if not isinstance(entries, list) or not entries:
|
|
63
|
+
return argv
|
|
64
|
+
# Only intervene when at least one path is a bare package name; a config
|
|
65
|
+
# with only literal paths keeps the binary's existing behavior untouched.
|
|
66
|
+
if not any(
|
|
67
|
+
isinstance(e, dict)
|
|
68
|
+
and isinstance(e.get("path"), str)
|
|
69
|
+
and is_bare_name(e["path"])
|
|
70
|
+
for e in entries
|
|
71
|
+
):
|
|
72
|
+
return argv
|
|
73
|
+
|
|
74
|
+
base = os.path.dirname(os.path.abspath(config_path))
|
|
75
|
+
flags: list[str] = []
|
|
76
|
+
for e in entries:
|
|
77
|
+
path = resolve_extension_path(e["path"], base=base, resolve_relative=True)
|
|
78
|
+
entrypoint = e.get("entrypoint")
|
|
79
|
+
flags.append("--extension")
|
|
80
|
+
flags.append(f"{path}::{entrypoint}" if isinstance(entrypoint, str) else path)
|
|
81
|
+
return [*argv, *flags]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Resolve an extension entry's ``path`` to a concrete loadable file.
|
|
2
|
+
|
|
3
|
+
#225 supports only literal file paths. #298 adds resolving a bare **package
|
|
4
|
+
name**: when ``path`` carries no path separator and no loadable-file suffix, it
|
|
5
|
+
names a package installed in the runtime env, and dirsql discovers the loadable
|
|
6
|
+
file *inside* that package.
|
|
7
|
+
|
|
8
|
+
Resolution is an ordered probe (file-first, then package), so every literal
|
|
9
|
+
path from #225 keeps its old behavior and only a bare name reaches the package
|
|
10
|
+
machinery:
|
|
11
|
+
|
|
12
|
+
1. **Path-looking** (contains a separator, or ends in ``.so`` / ``.dylib`` /
|
|
13
|
+
``.dll`` / ``.pyd``) -- returned as a file path: made absolute against
|
|
14
|
+
``base`` when ``resolve_relative`` is set (config-file entries), else
|
|
15
|
+
verbatim (programmatic entries, mirroring the Rust builder).
|
|
16
|
+
2. **Bare name** -- a same-named local file under ``base`` *shadows* the
|
|
17
|
+
package (parity with #225's file-first probe); otherwise the package dir is
|
|
18
|
+
located via :func:`importlib.util.find_spec` and the current platform's
|
|
19
|
+
loadable is globbed from inside it. Zero matches and multiple matches are
|
|
20
|
+
both hard errors -- the caller must disambiguate with a literal path.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import glob as _glob
|
|
24
|
+
import importlib.util
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
# Suffixes that mark a value as "already a file path" (so package resolution is
|
|
29
|
+
# never attempted) and, per platform, the globs used to find a loadable inside
|
|
30
|
+
# a package directory.
|
|
31
|
+
_LOADABLE_SUFFIXES = (".so", ".dylib", ".dll", ".pyd")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _platform_patterns():
|
|
35
|
+
"""Loadable-file glob(s) for the current platform."""
|
|
36
|
+
if sys.platform == "darwin":
|
|
37
|
+
return ("*.dylib",)
|
|
38
|
+
if sys.platform == "win32":
|
|
39
|
+
return ("*.dll", "*.pyd")
|
|
40
|
+
return ("*.so",)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_bare_name(path):
|
|
44
|
+
"""True when ``path`` is a bare package name rather than a file path."""
|
|
45
|
+
if os.sep in path or (os.altsep and os.altsep in path):
|
|
46
|
+
return False
|
|
47
|
+
return not path.endswith(_LOADABLE_SUFFIXES)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve_package(name):
|
|
51
|
+
"""Locate ``name``'s package dir and glob its platform loadable file."""
|
|
52
|
+
try:
|
|
53
|
+
spec = importlib.util.find_spec(name)
|
|
54
|
+
except (ImportError, ValueError) as exc:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"could not resolve extension package {name!r}: {exc}"
|
|
57
|
+
) from exc
|
|
58
|
+
if spec is None:
|
|
59
|
+
raise ValueError(f"could not resolve extension package {name!r}: not installed")
|
|
60
|
+
|
|
61
|
+
dirs = list(spec.submodule_search_locations or [])
|
|
62
|
+
if not dirs and spec.origin and spec.origin not in ("built-in", "frozen"):
|
|
63
|
+
dirs.append(os.path.dirname(spec.origin))
|
|
64
|
+
if not dirs:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"could not resolve extension package {name!r}: no package directory"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
patterns = _platform_patterns()
|
|
70
|
+
matches = set()
|
|
71
|
+
for d in dirs:
|
|
72
|
+
for pat in patterns:
|
|
73
|
+
matches.update(_glob.glob(os.path.join(d, "**", pat), recursive=True))
|
|
74
|
+
found = sorted(matches)
|
|
75
|
+
|
|
76
|
+
pat_desc = " / ".join(patterns)
|
|
77
|
+
if not found:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"no loadable extension file ({pat_desc}) found in package "
|
|
80
|
+
f"{name!r} (searched {', '.join(dirs)})"
|
|
81
|
+
)
|
|
82
|
+
if len(found) > 1:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
f"multiple loadable extension files found in package {name!r}: "
|
|
85
|
+
f"{', '.join(found)}; disambiguate with a literal path"
|
|
86
|
+
)
|
|
87
|
+
return found[0]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def resolve_extension_path(path, base, resolve_relative):
|
|
91
|
+
"""Resolve an extension ``path`` to a concrete file.
|
|
92
|
+
|
|
93
|
+
``base`` is the directory a relative path and the bare-name shadow probe
|
|
94
|
+
resolve against (a config file's parent dir, or the cwd for programmatic
|
|
95
|
+
entries). ``resolve_relative`` makes a relative path-looking value absolute
|
|
96
|
+
against ``base`` (config-file semantics); when false it is returned verbatim
|
|
97
|
+
(programmatic semantics).
|
|
98
|
+
"""
|
|
99
|
+
if not is_bare_name(path):
|
|
100
|
+
if resolve_relative and not os.path.isabs(path):
|
|
101
|
+
return os.path.join(base, path)
|
|
102
|
+
return path
|
|
103
|
+
local = os.path.join(base, path)
|
|
104
|
+
if os.path.isfile(local):
|
|
105
|
+
return local
|
|
106
|
+
return _resolve_package(path)
|
|
@@ -77,7 +77,7 @@ In Python, the constructor starts scanning in a background thread and returns im
|
|
|
77
77
|
- `tables` -- List of `Table` definitions. Each defines a SQLite table, a glob pattern, and an extract function.
|
|
78
78
|
- `ignore` -- Optional list of glob patterns. Files matching any ignore pattern are skipped regardless of table globs.
|
|
79
79
|
- `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`.
|
|
80
|
-
- `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 }`; TypeScript: `{ path, entrypoint? }` objects). Programmatic entries load first, then any `[[dirsql.extension]]` from `config`.
|
|
80
|
+
- `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 }`; TypeScript: `{ path, entrypoint? }` objects). A `path` is either a file path or a bare **package name**; a package name is resolved from the installed package (Python `importlib` / TypeScript `node_modules`, file-first, erroring on zero or multiple matches). This works on the Python/TypeScript constructor and CLI; the Rust binary and SDK are file-path-only. Programmatic entries load first, then any `[[dirsql.extension]]` from `config`. See [Loading extensions](../cli/config.md#loading-extensions).
|
|
81
81
|
|
|
82
82
|
### Methods
|
|
83
83
|
|
|
@@ -158,8 +158,11 @@ path = "./ext/myext.dylib"
|
|
|
158
158
|
entrypoint = "sqlite3_myext_init"
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
- **`path`** —
|
|
162
|
-
`.
|
|
161
|
+
- **`path`** — the extension's shared library: either a file path (`.so` /
|
|
162
|
+
`.dylib` / `.dll`, relative to the config file's directory) or a bare
|
|
163
|
+
**package name**. A package name is resolved from the installed package when
|
|
164
|
+
run through the pip/npm `dirsql` CLI; the standalone Rust binary is
|
|
165
|
+
file-path-only.
|
|
163
166
|
- **`entrypoint`** *(optional)* — the extension's init symbol. When omitted,
|
|
164
167
|
SQLite derives a default from the filename; set it when that default does not
|
|
165
168
|
match (for example, `sqlite-vec`'s entry point is `sqlite3_vec_init`).
|
|
@@ -242,6 +245,71 @@ other files' rows are indexed normally.
|
|
|
242
245
|
See [Command execution](#command-execution) for the full contract (argv
|
|
243
246
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
244
247
|
|
|
248
|
+
### Rewriting queries (`pre-query`)
|
|
249
|
+
|
|
250
|
+
The `pre-query` hook intercepts every incoming request and transforms it into
|
|
251
|
+
the SQL that runs against the index. Because the hook owns SQL construction,
|
|
252
|
+
`POST /query` can accept whatever shape you want — a natural-language question,
|
|
253
|
+
a saved-query name, a templating DSL — and your command translates it to SQL
|
|
254
|
+
before it runs. Unlike `on-file` (a per-`[[table]]` key), `pre-query` is a
|
|
255
|
+
**server-wide** `[dirsql]` key: every query flows through it.
|
|
256
|
+
|
|
257
|
+
```toml
|
|
258
|
+
[dirsql]
|
|
259
|
+
pre-query = "uv run python to_sql.py {args}"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
With `pre-query` set, the **raw `POST /query` request body** is passed to the
|
|
263
|
+
command as the `{args}` placeholder — a single, injection-safe argv token even
|
|
264
|
+
though the body is untrusted. The command prints **plain-text SQL** on stdout
|
|
265
|
+
(the last non-empty line is used); `dirsql` runs that SQL and returns rows
|
|
266
|
+
exactly as it would for a normal query.
|
|
267
|
+
|
|
268
|
+
| Placeholder | Value |
|
|
269
|
+
|-------------|-------|
|
|
270
|
+
| `{args}` | The raw `POST /query` request body, verbatim, as one argv token. |
|
|
271
|
+
|
|
272
|
+
When `pre-query` is **absent**, nothing changes: the request body is parsed as
|
|
273
|
+
`{"sql": "…"}` JSON and executed — the [HTTP API](./http-api.md#post-query)
|
|
274
|
+
default. Enabling the hook is fully backward compatible in reverse: remove the
|
|
275
|
+
key and the `{"sql": …}` contract returns.
|
|
276
|
+
|
|
277
|
+
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
278
|
+
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
279
|
+
`error` body. The command runs in the config file's directory and is bounded by
|
|
280
|
+
a fixed **30-second** timeout.
|
|
281
|
+
|
|
282
|
+
#### The hook owns SQL safety
|
|
283
|
+
|
|
284
|
+
Because the hook returns **plain SQL** (not a parameterized query), it is the
|
|
285
|
+
**trusted component** that turns the untrusted request body into safe SQL. The
|
|
286
|
+
`{args}` substitution keeps the body inert *as an argv token* — it can never
|
|
287
|
+
break out into extra command arguments — but whatever SQL string the hook
|
|
288
|
+
prints is executed as-is. Validate, escape, or parameterize **inside** the
|
|
289
|
+
hook. This trade-off is intentional for v1: it keeps the contract a simple
|
|
290
|
+
plain-text-SQL pipe and puts translation logic — and its safety — in your hook.
|
|
291
|
+
|
|
292
|
+
Worked example — a hook that maps a saved-query name to SQL:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
# to_sql.py
|
|
296
|
+
import sys
|
|
297
|
+
|
|
298
|
+
QUERIES = {
|
|
299
|
+
"recent-posts": "SELECT title, author FROM posts ORDER BY _mtime DESC LIMIT 10",
|
|
300
|
+
}
|
|
301
|
+
name = sys.argv[1].strip() if len(sys.argv) > 1 else ""
|
|
302
|
+
# Fall back to an empty result rather than trusting arbitrary input.
|
|
303
|
+
print(QUERIES.get(name, "SELECT 1 WHERE 0"))
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
curl -s http://localhost:7117/query -d 'recent-posts' | jq
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
See [Command execution](#command-execution) for the full contract (argv
|
|
311
|
+
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
312
|
+
|
|
245
313
|
### Full Example
|
|
246
314
|
|
|
247
315
|
```toml
|
|
@@ -263,8 +331,8 @@ glob = "logs/*.csv"
|
|
|
263
331
|
|
|
264
332
|
## Command execution
|
|
265
333
|
|
|
266
|
-
Config keys that run an external command — today `on-file
|
|
267
|
-
follow — share one execution contract:
|
|
334
|
+
Config keys that run an external command — today `on-file` and `pre-query`,
|
|
335
|
+
with more events to follow — share one execution contract:
|
|
268
336
|
|
|
269
337
|
- **argv, not a shell.** The command string is split into an argv with
|
|
270
338
|
shell-like quoting (spaces separate arguments; quotes group them), but **no
|
|
@@ -35,6 +35,15 @@ On error, the server returns a non-2xx status with a JSON body:
|
|
|
35
35
|
|
|
36
36
|
Malformed SQL returns `400`. An unreadable or malformed config returns `503`; a *missing* config is not an error — the server serves the default `files` table.
|
|
37
37
|
|
|
38
|
+
::: tip `pre-query` changes the body contract
|
|
39
|
+
When [`[dirsql].pre-query`](./config.md#rewriting-queries-pre-query) is
|
|
40
|
+
configured, the request body is **not** parsed as `{"sql": …}`. Instead the raw
|
|
41
|
+
body is passed verbatim to the hook command, which prints the SQL to run. A hook
|
|
42
|
+
that fails (non-zero exit, timeout, or spawn error) returns `500` with the
|
|
43
|
+
command's stderr tail. With no `pre-query` key, the `{"sql": …}` contract above
|
|
44
|
+
applies.
|
|
45
|
+
:::
|
|
46
|
+
|
|
38
47
|
```bash
|
|
39
48
|
curl -s http://localhost:7117/query \
|
|
40
49
|
-H 'content-type: application/json' \
|
|
@@ -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.47"
|
|
8
8
|
edition.workspace = true
|
|
9
9
|
publish = false
|
|
10
10
|
readme = "README.md"
|
|
@@ -77,7 +77,7 @@ In Python, the constructor starts scanning in a background thread and returns im
|
|
|
77
77
|
- `tables` -- List of `Table` definitions. Each defines a SQLite table, a glob pattern, and an extract function.
|
|
78
78
|
- `ignore` -- Optional list of glob patterns. Files matching any ignore pattern are skipped regardless of table globs.
|
|
79
79
|
- `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`.
|
|
80
|
-
- `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 }`; TypeScript: `{ path, entrypoint? }` objects). Programmatic entries load first, then any `[[dirsql.extension]]` from `config`.
|
|
80
|
+
- `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 }`; TypeScript: `{ path, entrypoint? }` objects). A `path` is either a file path or a bare **package name**; a package name is resolved from the installed package (Python `importlib` / TypeScript `node_modules`, file-first, erroring on zero or multiple matches). This works on the Python/TypeScript constructor and CLI; the Rust binary and SDK are file-path-only. Programmatic entries load first, then any `[[dirsql.extension]]` from `config`. See [Loading extensions](../cli/config.md#loading-extensions).
|
|
81
81
|
|
|
82
82
|
### Methods
|
|
83
83
|
|
|
@@ -158,8 +158,11 @@ path = "./ext/myext.dylib"
|
|
|
158
158
|
entrypoint = "sqlite3_myext_init"
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
- **`path`** —
|
|
162
|
-
`.
|
|
161
|
+
- **`path`** — the extension's shared library: either a file path (`.so` /
|
|
162
|
+
`.dylib` / `.dll`, relative to the config file's directory) or a bare
|
|
163
|
+
**package name**. A package name is resolved from the installed package when
|
|
164
|
+
run through the pip/npm `dirsql` CLI; the standalone Rust binary is
|
|
165
|
+
file-path-only.
|
|
163
166
|
- **`entrypoint`** *(optional)* — the extension's init symbol. When omitted,
|
|
164
167
|
SQLite derives a default from the filename; set it when that default does not
|
|
165
168
|
match (for example, `sqlite-vec`'s entry point is `sqlite3_vec_init`).
|
|
@@ -242,6 +245,71 @@ other files' rows are indexed normally.
|
|
|
242
245
|
See [Command execution](#command-execution) for the full contract (argv
|
|
243
246
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
244
247
|
|
|
248
|
+
### Rewriting queries (`pre-query`)
|
|
249
|
+
|
|
250
|
+
The `pre-query` hook intercepts every incoming request and transforms it into
|
|
251
|
+
the SQL that runs against the index. Because the hook owns SQL construction,
|
|
252
|
+
`POST /query` can accept whatever shape you want — a natural-language question,
|
|
253
|
+
a saved-query name, a templating DSL — and your command translates it to SQL
|
|
254
|
+
before it runs. Unlike `on-file` (a per-`[[table]]` key), `pre-query` is a
|
|
255
|
+
**server-wide** `[dirsql]` key: every query flows through it.
|
|
256
|
+
|
|
257
|
+
```toml
|
|
258
|
+
[dirsql]
|
|
259
|
+
pre-query = "uv run python to_sql.py {args}"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
With `pre-query` set, the **raw `POST /query` request body** is passed to the
|
|
263
|
+
command as the `{args}` placeholder — a single, injection-safe argv token even
|
|
264
|
+
though the body is untrusted. The command prints **plain-text SQL** on stdout
|
|
265
|
+
(the last non-empty line is used); `dirsql` runs that SQL and returns rows
|
|
266
|
+
exactly as it would for a normal query.
|
|
267
|
+
|
|
268
|
+
| Placeholder | Value |
|
|
269
|
+
|-------------|-------|
|
|
270
|
+
| `{args}` | The raw `POST /query` request body, verbatim, as one argv token. |
|
|
271
|
+
|
|
272
|
+
When `pre-query` is **absent**, nothing changes: the request body is parsed as
|
|
273
|
+
`{"sql": "…"}` JSON and executed — the [HTTP API](./http-api.md#post-query)
|
|
274
|
+
default. Enabling the hook is fully backward compatible in reverse: remove the
|
|
275
|
+
key and the `{"sql": …}` contract returns.
|
|
276
|
+
|
|
277
|
+
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
278
|
+
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
279
|
+
`error` body. The command runs in the config file's directory and is bounded by
|
|
280
|
+
a fixed **30-second** timeout.
|
|
281
|
+
|
|
282
|
+
#### The hook owns SQL safety
|
|
283
|
+
|
|
284
|
+
Because the hook returns **plain SQL** (not a parameterized query), it is the
|
|
285
|
+
**trusted component** that turns the untrusted request body into safe SQL. The
|
|
286
|
+
`{args}` substitution keeps the body inert *as an argv token* — it can never
|
|
287
|
+
break out into extra command arguments — but whatever SQL string the hook
|
|
288
|
+
prints is executed as-is. Validate, escape, or parameterize **inside** the
|
|
289
|
+
hook. This trade-off is intentional for v1: it keeps the contract a simple
|
|
290
|
+
plain-text-SQL pipe and puts translation logic — and its safety — in your hook.
|
|
291
|
+
|
|
292
|
+
Worked example — a hook that maps a saved-query name to SQL:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
# to_sql.py
|
|
296
|
+
import sys
|
|
297
|
+
|
|
298
|
+
QUERIES = {
|
|
299
|
+
"recent-posts": "SELECT title, author FROM posts ORDER BY _mtime DESC LIMIT 10",
|
|
300
|
+
}
|
|
301
|
+
name = sys.argv[1].strip() if len(sys.argv) > 1 else ""
|
|
302
|
+
# Fall back to an empty result rather than trusting arbitrary input.
|
|
303
|
+
print(QUERIES.get(name, "SELECT 1 WHERE 0"))
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
curl -s http://localhost:7117/query -d 'recent-posts' | jq
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
See [Command execution](#command-execution) for the full contract (argv
|
|
311
|
+
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
312
|
+
|
|
245
313
|
### Full Example
|
|
246
314
|
|
|
247
315
|
```toml
|
|
@@ -263,8 +331,8 @@ glob = "logs/*.csv"
|
|
|
263
331
|
|
|
264
332
|
## Command execution
|
|
265
333
|
|
|
266
|
-
Config keys that run an external command — today `on-file
|
|
267
|
-
follow — share one execution contract:
|
|
334
|
+
Config keys that run an external command — today `on-file` and `pre-query`,
|
|
335
|
+
with more events to follow — share one execution contract:
|
|
268
336
|
|
|
269
337
|
- **argv, not a shell.** The command string is split into an argv with
|
|
270
338
|
shell-like quoting (spaces separate arguments; quotes group them), but **no
|
|
@@ -35,6 +35,15 @@ On error, the server returns a non-2xx status with a JSON body:
|
|
|
35
35
|
|
|
36
36
|
Malformed SQL returns `400`. An unreadable or malformed config returns `503`; a *missing* config is not an error — the server serves the default `files` table.
|
|
37
37
|
|
|
38
|
+
::: tip `pre-query` changes the body contract
|
|
39
|
+
When [`[dirsql].pre-query`](./config.md#rewriting-queries-pre-query) is
|
|
40
|
+
configured, the request body is **not** parsed as `{"sql": …}`. Instead the raw
|
|
41
|
+
body is passed verbatim to the hook command, which prints the SQL to run. A hook
|
|
42
|
+
that fails (non-zero exit, timeout, or spawn error) returns `500` with the
|
|
43
|
+
command's stderr tail. With no `pre-query` key, the `{"sql": …}` contract above
|
|
44
|
+
applies.
|
|
45
|
+
:::
|
|
46
|
+
|
|
38
47
|
```bash
|
|
39
48
|
curl -s http://localhost:7117/query \
|
|
40
49
|
-H 'content-type: application/json' \
|
|
@@ -77,7 +77,7 @@ In Python, the constructor starts scanning in a background thread and returns im
|
|
|
77
77
|
- `tables` -- List of `Table` definitions. Each defines a SQLite table, a glob pattern, and an extract function.
|
|
78
78
|
- `ignore` -- Optional list of glob patterns. Files matching any ignore pattern are skipped regardless of table globs.
|
|
79
79
|
- `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`.
|
|
80
|
-
- `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 }`; TypeScript: `{ path, entrypoint? }` objects). Programmatic entries load first, then any `[[dirsql.extension]]` from `config`.
|
|
80
|
+
- `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 }`; TypeScript: `{ path, entrypoint? }` objects). A `path` is either a file path or a bare **package name**; a package name is resolved from the installed package (Python `importlib` / TypeScript `node_modules`, file-first, erroring on zero or multiple matches). This works on the Python/TypeScript constructor and CLI; the Rust binary and SDK are file-path-only. Programmatic entries load first, then any `[[dirsql.extension]]` from `config`. See [Loading extensions](../cli/config.md#loading-extensions).
|
|
81
81
|
|
|
82
82
|
### Methods
|
|
83
83
|
|
|
@@ -158,8 +158,11 @@ path = "./ext/myext.dylib"
|
|
|
158
158
|
entrypoint = "sqlite3_myext_init"
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
- **`path`** —
|
|
162
|
-
`.
|
|
161
|
+
- **`path`** — the extension's shared library: either a file path (`.so` /
|
|
162
|
+
`.dylib` / `.dll`, relative to the config file's directory) or a bare
|
|
163
|
+
**package name**. A package name is resolved from the installed package when
|
|
164
|
+
run through the pip/npm `dirsql` CLI; the standalone Rust binary is
|
|
165
|
+
file-path-only.
|
|
163
166
|
- **`entrypoint`** *(optional)* — the extension's init symbol. When omitted,
|
|
164
167
|
SQLite derives a default from the filename; set it when that default does not
|
|
165
168
|
match (for example, `sqlite-vec`'s entry point is `sqlite3_vec_init`).
|
|
@@ -242,6 +245,71 @@ other files' rows are indexed normally.
|
|
|
242
245
|
See [Command execution](#command-execution) for the full contract (argv
|
|
243
246
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
244
247
|
|
|
248
|
+
### Rewriting queries (`pre-query`)
|
|
249
|
+
|
|
250
|
+
The `pre-query` hook intercepts every incoming request and transforms it into
|
|
251
|
+
the SQL that runs against the index. Because the hook owns SQL construction,
|
|
252
|
+
`POST /query` can accept whatever shape you want — a natural-language question,
|
|
253
|
+
a saved-query name, a templating DSL — and your command translates it to SQL
|
|
254
|
+
before it runs. Unlike `on-file` (a per-`[[table]]` key), `pre-query` is a
|
|
255
|
+
**server-wide** `[dirsql]` key: every query flows through it.
|
|
256
|
+
|
|
257
|
+
```toml
|
|
258
|
+
[dirsql]
|
|
259
|
+
pre-query = "uv run python to_sql.py {args}"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
With `pre-query` set, the **raw `POST /query` request body** is passed to the
|
|
263
|
+
command as the `{args}` placeholder — a single, injection-safe argv token even
|
|
264
|
+
though the body is untrusted. The command prints **plain-text SQL** on stdout
|
|
265
|
+
(the last non-empty line is used); `dirsql` runs that SQL and returns rows
|
|
266
|
+
exactly as it would for a normal query.
|
|
267
|
+
|
|
268
|
+
| Placeholder | Value |
|
|
269
|
+
|-------------|-------|
|
|
270
|
+
| `{args}` | The raw `POST /query` request body, verbatim, as one argv token. |
|
|
271
|
+
|
|
272
|
+
When `pre-query` is **absent**, nothing changes: the request body is parsed as
|
|
273
|
+
`{"sql": "…"}` JSON and executed — the [HTTP API](./http-api.md#post-query)
|
|
274
|
+
default. Enabling the hook is fully backward compatible in reverse: remove the
|
|
275
|
+
key and the `{"sql": …}` contract returns.
|
|
276
|
+
|
|
277
|
+
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
278
|
+
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
279
|
+
`error` body. The command runs in the config file's directory and is bounded by
|
|
280
|
+
a fixed **30-second** timeout.
|
|
281
|
+
|
|
282
|
+
#### The hook owns SQL safety
|
|
283
|
+
|
|
284
|
+
Because the hook returns **plain SQL** (not a parameterized query), it is the
|
|
285
|
+
**trusted component** that turns the untrusted request body into safe SQL. The
|
|
286
|
+
`{args}` substitution keeps the body inert *as an argv token* — it can never
|
|
287
|
+
break out into extra command arguments — but whatever SQL string the hook
|
|
288
|
+
prints is executed as-is. Validate, escape, or parameterize **inside** the
|
|
289
|
+
hook. This trade-off is intentional for v1: it keeps the contract a simple
|
|
290
|
+
plain-text-SQL pipe and puts translation logic — and its safety — in your hook.
|
|
291
|
+
|
|
292
|
+
Worked example — a hook that maps a saved-query name to SQL:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
# to_sql.py
|
|
296
|
+
import sys
|
|
297
|
+
|
|
298
|
+
QUERIES = {
|
|
299
|
+
"recent-posts": "SELECT title, author FROM posts ORDER BY _mtime DESC LIMIT 10",
|
|
300
|
+
}
|
|
301
|
+
name = sys.argv[1].strip() if len(sys.argv) > 1 else ""
|
|
302
|
+
# Fall back to an empty result rather than trusting arbitrary input.
|
|
303
|
+
print(QUERIES.get(name, "SELECT 1 WHERE 0"))
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
curl -s http://localhost:7117/query -d 'recent-posts' | jq
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
See [Command execution](#command-execution) for the full contract (argv
|
|
311
|
+
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
312
|
+
|
|
245
313
|
### Full Example
|
|
246
314
|
|
|
247
315
|
```toml
|
|
@@ -263,8 +331,8 @@ glob = "logs/*.csv"
|
|
|
263
331
|
|
|
264
332
|
## Command execution
|
|
265
333
|
|
|
266
|
-
Config keys that run an external command — today `on-file
|
|
267
|
-
follow — share one execution contract:
|
|
334
|
+
Config keys that run an external command — today `on-file` and `pre-query`,
|
|
335
|
+
with more events to follow — share one execution contract:
|
|
268
336
|
|
|
269
337
|
- **argv, not a shell.** The command string is split into an argv with
|
|
270
338
|
shell-like quoting (spaces separate arguments; quotes group them), but **no
|