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.
Files changed (121) hide show
  1. {dirsql-0.3.45 → dirsql-0.3.47}/Cargo.lock +1 -1
  2. {dirsql-0.3.45 → dirsql-0.3.47}/PKG-INFO +1 -1
  3. {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/_async.py +23 -1
  4. {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/cli/main.py +9 -0
  5. dirsql-0.3.47/dirsql/cli/resolve_config_extensions.py +81 -0
  6. dirsql-0.3.47/dirsql/resolve_extension.py +106 -0
  7. {dirsql-0.3.45 → dirsql-0.3.47}/docs/api/index.md +1 -1
  8. {dirsql-0.3.45/packages/python → dirsql-0.3.47}/docs/cli/config.md +72 -4
  9. {dirsql-0.3.45 → dirsql-0.3.47}/docs/cli/http-api.md +9 -0
  10. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/Cargo.toml +1 -1
  11. {dirsql-0.3.45/packages/rust → dirsql-0.3.47/packages/python}/docs/api/index.md +1 -1
  12. {dirsql-0.3.45 → dirsql-0.3.47/packages/python}/docs/cli/config.md +72 -4
  13. {dirsql-0.3.45/packages/rust → dirsql-0.3.47/packages/python}/docs/cli/http-api.md +9 -0
  14. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/e2e-attestation.json +2 -2
  15. {dirsql-0.3.45/packages/python → dirsql-0.3.47/packages/rust}/docs/api/index.md +1 -1
  16. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/cli/config.md +72 -4
  17. {dirsql-0.3.45/packages/python → dirsql-0.3.47/packages/rust}/docs/cli/http-api.md +9 -0
  18. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/bin/dirsql.rs +96 -4
  19. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/cli/mod.rs +57 -1
  20. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/cli/router.rs +81 -14
  21. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/cli/server.rs +1 -0
  22. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/config.rs +65 -3
  23. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/lib.rs +32 -11
  24. {dirsql-0.3.45 → dirsql-0.3.47}/Cargo.toml +0 -0
  25. {dirsql-0.3.45 → dirsql-0.3.47}/README.md +0 -0
  26. {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/__init__.py +0 -0
  27. {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/_dirsql.pyi +0 -0
  28. {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/cli/__init__.py +0 -0
  29. {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/cli/binary_path.py +0 -0
  30. {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/cli/interpret/__init__.py +0 -0
  31. {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/cli/is_windows.py +0 -0
  32. {dirsql-0.3.45 → dirsql-0.3.47}/dirsql/py.typed +0 -0
  33. {dirsql-0.3.45 → dirsql-0.3.47}/docs/.claude/CLAUDE.md +0 -0
  34. {dirsql-0.3.45 → dirsql-0.3.47}/docs/.vitepress/config.ts +0 -0
  35. {dirsql-0.3.45 → dirsql-0.3.47}/docs/.vitepress/theme/index.ts +0 -0
  36. {dirsql-0.3.45 → dirsql-0.3.47}/docs/.vitepress/theme/lang.ts +0 -0
  37. {dirsql-0.3.45 → dirsql-0.3.47}/docs/AGENTS.md +0 -0
  38. {dirsql-0.3.45 → dirsql-0.3.47}/docs/cli/index.md +0 -0
  39. {dirsql-0.3.45 → dirsql-0.3.47}/docs/cli/init.md +0 -0
  40. {dirsql-0.3.45 → dirsql-0.3.47}/docs/cli/server.md +0 -0
  41. {dirsql-0.3.45 → dirsql-0.3.47}/docs/getting-started.md +0 -0
  42. {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/async.md +0 -0
  43. {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/crdt.md +0 -0
  44. {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/persistence.md +0 -0
  45. {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/querying.md +0 -0
  46. {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/tables.md +0 -0
  47. {dirsql-0.3.45 → dirsql-0.3.47}/docs/guide/watching.md +0 -0
  48. {dirsql-0.3.45 → dirsql-0.3.47}/docs/index.md +0 -0
  49. {dirsql-0.3.45 → dirsql-0.3.47}/docs/migrations.md +0 -0
  50. {dirsql-0.3.45 → dirsql-0.3.47}/docs/package.json +0 -0
  51. {dirsql-0.3.45 → dirsql-0.3.47}/docs/playwright.config.ts +0 -0
  52. {dirsql-0.3.45 → dirsql-0.3.47}/docs/pnpm-lock.yaml +0 -0
  53. {dirsql-0.3.45 → dirsql-0.3.47}/docs/pnpm-workspace.yaml +0 -0
  54. {dirsql-0.3.45 → dirsql-0.3.47}/docs/tests/integration/home.spec.ts +0 -0
  55. {dirsql-0.3.45 → dirsql-0.3.47}/docs/tests/integration/language-flag.spec.ts +0 -0
  56. {dirsql-0.3.45 → dirsql-0.3.47}/docs/tests/integration/sidebar.spec.ts +0 -0
  57. {dirsql-0.3.45 → dirsql-0.3.47}/docs/tests/unit/config.test.ts +0 -0
  58. {dirsql-0.3.45 → dirsql-0.3.47}/docs/tests/unit/lang.test.ts +0 -0
  59. {dirsql-0.3.45 → dirsql-0.3.47}/docs/vitest.config.ts +0 -0
  60. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/README.md +0 -0
  61. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/conftest.py +0 -0
  62. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/.claude/CLAUDE.md +0 -0
  63. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/.vitepress/config.ts +0 -0
  64. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  65. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  66. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/AGENTS.md +0 -0
  67. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/cli/index.md +0 -0
  68. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/cli/init.md +0 -0
  69. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/cli/server.md +0 -0
  70. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/getting-started.md +0 -0
  71. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/async.md +0 -0
  72. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/crdt.md +0 -0
  73. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/persistence.md +0 -0
  74. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/querying.md +0 -0
  75. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/tables.md +0 -0
  76. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/guide/watching.md +0 -0
  77. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/index.md +0 -0
  78. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/migrations.md +0 -0
  79. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/package.json +0 -0
  80. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/playwright.config.ts +0 -0
  81. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/pnpm-lock.yaml +0 -0
  82. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/pnpm-workspace.yaml +0 -0
  83. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  84. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  85. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
  86. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/tests/unit/config.test.ts +0 -0
  87. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  88. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/docs/vitest.config.ts +0 -0
  89. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/src/lib.rs +0 -0
  90. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/tests/__init__.py +0 -0
  91. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/tests/conftest.py +0 -0
  92. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/tests/e2e/__init__.py +0 -0
  93. {dirsql-0.3.45 → dirsql-0.3.47}/packages/python/tests/integration/__init__.py +0 -0
  94. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/Cargo.toml +0 -0
  95. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/README.md +0 -0
  96. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/benches/db_bench.rs +0 -0
  97. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/benches/differ_bench.rs +0 -0
  98. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/benches/matcher_bench.rs +0 -0
  99. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/benches/scanner_bench.rs +0 -0
  100. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/cli/index.md +0 -0
  101. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/cli/init.md +0 -0
  102. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/cli/server.md +0 -0
  103. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/getting-started.md +0 -0
  104. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/async.md +0 -0
  105. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/crdt.md +0 -0
  106. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/persistence.md +0 -0
  107. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/querying.md +0 -0
  108. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/tables.md +0 -0
  109. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/guide/watching.md +0 -0
  110. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/index.md +0 -0
  111. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/docs/migrations.md +0 -0
  112. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/cli/init.rs +0 -0
  113. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/cli/serialize.rs +0 -0
  114. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/command.rs +0 -0
  115. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/db.rs +0 -0
  116. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/differ.rs +0 -0
  117. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/matcher.rs +0 -0
  118. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/persist.rs +0 -0
  119. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/scanner.rs +0 -0
  120. {dirsql-0.3.45 → dirsql-0.3.47}/packages/rust/src/watcher.rs +0 -0
  121. {dirsql-0.3.45 → dirsql-0.3.47}/pyproject.toml +0 -0
@@ -501,7 +501,7 @@ dependencies = [
501
501
 
502
502
  [[package]]
503
503
  name = "dirsql-py-ext"
504
- version = "0.3.45"
504
+ version = "0.3.47"
505
505
  dependencies = [
506
506
  "dirsql",
507
507
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.3.45
3
+ Version: 0.3.47
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,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._extensions,
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`. Available in the Python, Rust, and TypeScript SDKs. See [Loading extensions](../cli/config.md#loading-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). 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`** — a path to the extension's shared library (`.so` / `.dylib` /
162
- `.dll`). Relative paths resolve against the config file's parent directory.
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`, with more events to
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.45"
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`. Available in the Python, Rust, and TypeScript SDKs. See [Loading extensions](../cli/config.md#loading-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). 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`** — a path to the extension's shared library (`.so` / `.dylib` /
162
- `.dll`). Relative paths resolve against the config file's parent directory.
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`, with more events to
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' \
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "command": "uv run python -m pytest tests/e2e/ -x -q",
3
- "ran_at": 1782833107,
3
+ "ran_at": 1782996616,
4
4
  "exit_code": 0,
5
- "commit": "07bd37825c22f9ad73153d70fc65c9e3c92f855e"
5
+ "commit": "19cf2e30ef5bff8fe5e77b2c59fd54ca0fc46575"
6
6
  }
@@ -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`. Available in the Python, Rust, and TypeScript SDKs. See [Loading extensions](../cli/config.md#loading-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). 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`** — a path to the extension's shared library (`.so` / `.dylib` /
162
- `.dll`). Relative paths resolve against the config file's parent directory.
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`, with more events to
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