dirsql 0.3.46__tar.gz → 0.3.48__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.46 → dirsql-0.3.48}/Cargo.lock +1 -1
  2. {dirsql-0.3.46 → dirsql-0.3.48}/PKG-INFO +1 -1
  3. {dirsql-0.3.46 → dirsql-0.3.48}/dirsql/_async.py +23 -1
  4. {dirsql-0.3.46 → dirsql-0.3.48}/dirsql/cli/main.py +9 -0
  5. dirsql-0.3.48/dirsql/cli/resolve_config_extensions.py +81 -0
  6. dirsql-0.3.48/dirsql/resolve_extension.py +106 -0
  7. {dirsql-0.3.46 → dirsql-0.3.48}/docs/api/index.md +1 -1
  8. {dirsql-0.3.46/packages/python → dirsql-0.3.48}/docs/cli/config.md +5 -2
  9. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/Cargo.toml +1 -1
  10. {dirsql-0.3.46/packages/rust → dirsql-0.3.48/packages/python}/docs/api/index.md +1 -1
  11. {dirsql-0.3.46 → dirsql-0.3.48/packages/python}/docs/cli/config.md +5 -2
  12. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/e2e-attestation.json +2 -2
  13. {dirsql-0.3.46/packages/python → dirsql-0.3.48/packages/rust}/docs/api/index.md +1 -1
  14. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/cli/config.md +5 -2
  15. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/bin/dirsql.rs +71 -2
  16. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/lib.rs +32 -11
  17. {dirsql-0.3.46 → dirsql-0.3.48}/Cargo.toml +0 -0
  18. {dirsql-0.3.46 → dirsql-0.3.48}/README.md +0 -0
  19. {dirsql-0.3.46 → dirsql-0.3.48}/dirsql/__init__.py +0 -0
  20. {dirsql-0.3.46 → dirsql-0.3.48}/dirsql/_dirsql.pyi +0 -0
  21. {dirsql-0.3.46 → dirsql-0.3.48}/dirsql/cli/__init__.py +0 -0
  22. {dirsql-0.3.46 → dirsql-0.3.48}/dirsql/cli/binary_path.py +0 -0
  23. {dirsql-0.3.46 → dirsql-0.3.48}/dirsql/cli/interpret/__init__.py +0 -0
  24. {dirsql-0.3.46 → dirsql-0.3.48}/dirsql/cli/is_windows.py +0 -0
  25. {dirsql-0.3.46 → dirsql-0.3.48}/dirsql/py.typed +0 -0
  26. {dirsql-0.3.46 → dirsql-0.3.48}/docs/.claude/CLAUDE.md +0 -0
  27. {dirsql-0.3.46 → dirsql-0.3.48}/docs/.vitepress/config.ts +0 -0
  28. {dirsql-0.3.46 → dirsql-0.3.48}/docs/.vitepress/theme/index.ts +0 -0
  29. {dirsql-0.3.46 → dirsql-0.3.48}/docs/.vitepress/theme/lang.ts +0 -0
  30. {dirsql-0.3.46 → dirsql-0.3.48}/docs/AGENTS.md +0 -0
  31. {dirsql-0.3.46 → dirsql-0.3.48}/docs/cli/http-api.md +0 -0
  32. {dirsql-0.3.46 → dirsql-0.3.48}/docs/cli/index.md +0 -0
  33. {dirsql-0.3.46 → dirsql-0.3.48}/docs/cli/init.md +0 -0
  34. {dirsql-0.3.46 → dirsql-0.3.48}/docs/cli/server.md +0 -0
  35. {dirsql-0.3.46 → dirsql-0.3.48}/docs/getting-started.md +0 -0
  36. {dirsql-0.3.46 → dirsql-0.3.48}/docs/guide/async.md +0 -0
  37. {dirsql-0.3.46 → dirsql-0.3.48}/docs/guide/crdt.md +0 -0
  38. {dirsql-0.3.46 → dirsql-0.3.48}/docs/guide/persistence.md +0 -0
  39. {dirsql-0.3.46 → dirsql-0.3.48}/docs/guide/querying.md +0 -0
  40. {dirsql-0.3.46 → dirsql-0.3.48}/docs/guide/tables.md +0 -0
  41. {dirsql-0.3.46 → dirsql-0.3.48}/docs/guide/watching.md +0 -0
  42. {dirsql-0.3.46 → dirsql-0.3.48}/docs/index.md +0 -0
  43. {dirsql-0.3.46 → dirsql-0.3.48}/docs/migrations.md +0 -0
  44. {dirsql-0.3.46 → dirsql-0.3.48}/docs/package.json +0 -0
  45. {dirsql-0.3.46 → dirsql-0.3.48}/docs/playwright.config.ts +0 -0
  46. {dirsql-0.3.46 → dirsql-0.3.48}/docs/pnpm-lock.yaml +0 -0
  47. {dirsql-0.3.46 → dirsql-0.3.48}/docs/pnpm-workspace.yaml +0 -0
  48. {dirsql-0.3.46 → dirsql-0.3.48}/docs/tests/integration/home.spec.ts +0 -0
  49. {dirsql-0.3.46 → dirsql-0.3.48}/docs/tests/integration/language-flag.spec.ts +0 -0
  50. {dirsql-0.3.46 → dirsql-0.3.48}/docs/tests/integration/sidebar.spec.ts +0 -0
  51. {dirsql-0.3.46 → dirsql-0.3.48}/docs/tests/unit/config.test.ts +0 -0
  52. {dirsql-0.3.46 → dirsql-0.3.48}/docs/tests/unit/lang.test.ts +0 -0
  53. {dirsql-0.3.46 → dirsql-0.3.48}/docs/vitest.config.ts +0 -0
  54. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/README.md +0 -0
  55. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/conftest.py +0 -0
  56. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/.claude/CLAUDE.md +0 -0
  57. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/.vitepress/config.ts +0 -0
  58. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  59. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  60. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/AGENTS.md +0 -0
  61. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/cli/http-api.md +0 -0
  62. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/cli/index.md +0 -0
  63. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/cli/init.md +0 -0
  64. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/cli/server.md +0 -0
  65. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/getting-started.md +0 -0
  66. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/guide/async.md +0 -0
  67. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/guide/crdt.md +0 -0
  68. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/guide/persistence.md +0 -0
  69. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/guide/querying.md +0 -0
  70. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/guide/tables.md +0 -0
  71. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/guide/watching.md +0 -0
  72. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/index.md +0 -0
  73. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/migrations.md +0 -0
  74. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/package.json +0 -0
  75. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/playwright.config.ts +0 -0
  76. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/pnpm-lock.yaml +0 -0
  77. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/pnpm-workspace.yaml +0 -0
  78. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  79. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  80. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
  81. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/tests/unit/config.test.ts +0 -0
  82. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  83. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/docs/vitest.config.ts +0 -0
  84. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/src/lib.rs +0 -0
  85. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/tests/__init__.py +0 -0
  86. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/tests/conftest.py +0 -0
  87. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/tests/e2e/__init__.py +0 -0
  88. {dirsql-0.3.46 → dirsql-0.3.48}/packages/python/tests/integration/__init__.py +0 -0
  89. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/Cargo.toml +0 -0
  90. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/README.md +0 -0
  91. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/benches/db_bench.rs +0 -0
  92. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/benches/differ_bench.rs +0 -0
  93. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/benches/matcher_bench.rs +0 -0
  94. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/benches/scanner_bench.rs +0 -0
  95. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/cli/http-api.md +0 -0
  96. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/cli/index.md +0 -0
  97. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/cli/init.md +0 -0
  98. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/cli/server.md +0 -0
  99. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/getting-started.md +0 -0
  100. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/guide/async.md +0 -0
  101. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/guide/crdt.md +0 -0
  102. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/guide/persistence.md +0 -0
  103. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/guide/querying.md +0 -0
  104. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/guide/tables.md +0 -0
  105. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/guide/watching.md +0 -0
  106. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/index.md +0 -0
  107. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/docs/migrations.md +0 -0
  108. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/cli/init.rs +0 -0
  109. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/cli/mod.rs +0 -0
  110. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/cli/router.rs +0 -0
  111. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/cli/serialize.rs +0 -0
  112. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/cli/server.rs +0 -0
  113. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/command.rs +0 -0
  114. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/config.rs +0 -0
  115. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/db.rs +0 -0
  116. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/differ.rs +0 -0
  117. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/matcher.rs +0 -0
  118. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/persist.rs +0 -0
  119. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/scanner.rs +0 -0
  120. {dirsql-0.3.46 → dirsql-0.3.48}/packages/rust/src/watcher.rs +0 -0
  121. {dirsql-0.3.46 → dirsql-0.3.48}/pyproject.toml +0 -0
@@ -501,7 +501,7 @@ dependencies = [
501
501
 
502
502
  [[package]]
503
503
  name = "dirsql-py-ext"
504
- version = "0.3.46"
504
+ version = "0.3.48"
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.46
3
+ Version: 0.3.48
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`).
@@ -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.46"
7
+ version = "0.3.48"
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`).
@@ -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`).
@@ -9,7 +9,7 @@ use std::process::ExitCode;
9
9
 
10
10
  use clap::{Args, Parser, Subcommand};
11
11
  use dirsql::cli::{AppState, PreQuery, ServerConfig, init::InitOptions, serve_with_state};
12
- use dirsql::{DirSQL, Row, Table};
12
+ use dirsql::{DirSQL, Extension, Row, Table};
13
13
 
14
14
  #[derive(Debug, Parser)]
15
15
  #[command(
@@ -41,6 +41,19 @@ struct Cli {
41
41
  /// TCP port to bind. Used when no subcommand is given.
42
42
  #[arg(long, default_value_t = 7117)]
43
43
  port: u16,
44
+
45
+ /// Load a SQLite extension by literal path, overriding a TOML config's
46
+ /// `[[dirsql.extension]]` entries. Repeatable. Format: `<path>` or
47
+ /// `<path>::<entrypoint>`.
48
+ ///
49
+ /// Intended for the language launcher (pip/npm), not end users: the
50
+ /// launcher resolves config extensions — including bare **package names**,
51
+ /// which need an interpreter this compiled binary lacks (see #227) — and
52
+ /// passes the resolved literal paths here. When any are present, the TOML
53
+ /// config's own extension entries are not loaded (the launcher already
54
+ /// merged and resolved them).
55
+ #[arg(long = "extension")]
56
+ extension: Vec<String>,
44
57
  }
45
58
 
46
59
  #[derive(Debug, Subcommand)]
@@ -155,12 +168,46 @@ fn load_state(cli: &Cli) -> AppState {
155
168
  }
156
169
  };
157
170
 
158
- match DirSQL::from_config_path(&resolved) {
171
+ // Launcher-resolved extensions (`--extension`) override the TOML config's
172
+ // own `[[dirsql.extension]]` entries: the launcher has already merged and
173
+ // resolved them (including package names the compiled binary can't resolve;
174
+ // #227), so build from the config but suppress its extension loading and
175
+ // supply the resolved literal paths instead.
176
+ let build = if cli.extension.is_empty() {
177
+ DirSQL::from_config_path(&resolved)
178
+ } else {
179
+ DirSQL::builder()
180
+ .config(&resolved)
181
+ .extensions(parse_extension_specs(&cli.extension))
182
+ .suppress_config_extensions(true)
183
+ .build()
184
+ };
185
+ match build {
159
186
  Ok(db) => AppState::Ready(db),
160
187
  Err(err) => AppState::Unavailable(format!("failed to load config: {err}")),
161
188
  }
162
189
  }
163
190
 
191
+ /// Parse `--extension` specs (`<path>` or `<path>::<entrypoint>`) into
192
+ /// [`Extension`]s. Splitting on the first `::` keeps a path that itself
193
+ /// contains `::` unambiguous only after the entrypoint boundary — entrypoints
194
+ /// are C identifiers, so the first `::` is the boundary.
195
+ fn parse_extension_specs(specs: &[String]) -> Vec<Extension> {
196
+ specs
197
+ .iter()
198
+ .map(|spec| match spec.split_once("::") {
199
+ Some((path, entrypoint)) => Extension {
200
+ path: PathBuf::from(path),
201
+ entrypoint: Some(entrypoint.to_string()),
202
+ },
203
+ None => Extension {
204
+ path: PathBuf::from(spec),
205
+ entrypoint: None,
206
+ },
207
+ })
208
+ .collect()
209
+ }
210
+
164
211
  /// Extract the server-wide `pre-query` hook from the config, if any.
165
212
  ///
166
213
  /// Returns `None` when the config is absent, unresolvable, unparsable, or
@@ -243,6 +290,28 @@ async fn wait_for_shutdown() -> std::io::Result<()> {
243
290
  mod tests {
244
291
  use super::*;
245
292
 
293
+ #[test]
294
+ fn parse_extension_specs_handles_bare_path_and_entrypoint() {
295
+ let specs = vec![
296
+ "/abs/vec0.so".to_string(),
297
+ "/abs/spellfix.so::sqlite3_spellfix_init".to_string(),
298
+ ];
299
+ let exts = parse_extension_specs(&specs);
300
+ assert_eq!(exts.len(), 2);
301
+ assert_eq!(exts[0].path, PathBuf::from("/abs/vec0.so"));
302
+ assert!(exts[0].entrypoint.is_none());
303
+ assert_eq!(exts[1].path, PathBuf::from("/abs/spellfix.so"));
304
+ assert_eq!(exts[1].entrypoint.as_deref(), Some("sqlite3_spellfix_init"));
305
+ }
306
+
307
+ #[test]
308
+ fn parse_extension_specs_splits_on_first_double_colon() {
309
+ let specs = vec!["/a.so::init::extra".to_string()];
310
+ let exts = parse_extension_specs(&specs);
311
+ assert_eq!(exts[0].path, PathBuf::from("/a.so"));
312
+ assert_eq!(exts[0].entrypoint.as_deref(), Some("init::extra"));
313
+ }
314
+
246
315
  #[test]
247
316
  fn default_files_table_declares_filesystem_fact_columns_over_recursive_glob() {
248
317
  // The zero-config fallback table is pure data: a fixed DDL naming only
@@ -920,6 +920,7 @@ pub struct DirSQLBuilder {
920
920
  ignore: Vec<String>,
921
921
  extensions: Vec<Extension>,
922
922
  config_path: Option<PathBuf>,
923
+ suppress_config_extensions: bool,
923
924
  persist: bool,
924
925
  persist_path: Option<PathBuf>,
925
926
  poll_interval: Option<Duration>,
@@ -987,6 +988,20 @@ impl DirSQLBuilder {
987
988
  self
988
989
  }
989
990
 
991
+ /// Suppress loading of a config file's `[[dirsql.extension]]` entries.
992
+ ///
993
+ /// The core resolves config-file extension paths only literally (relative
994
+ /// to the config's parent). A launcher that resolves extensions itself —
995
+ /// e.g. by **package name**, which needs an interpreter the compiled core
996
+ /// lacks (Python `importlib`, Node `require.resolve`; see #227) — sets this
997
+ /// and supplies the already-resolved literal paths via
998
+ /// [`extensions`](Self::extensions) instead, so the config's own extension
999
+ /// entries are not loaded a second time.
1000
+ pub fn suppress_config_extensions(mut self, suppress: bool) -> Self {
1001
+ self.suppress_config_extensions = suppress;
1002
+ self
1003
+ }
1004
+
990
1005
  /// Enable persistent on-disk storage. When `true`, the SQLite database is
991
1006
  /// written to `<root>/.dirsql/cache.db` (override via
992
1007
  /// [`persist_path`](Self::persist_path)) so subsequent startups only
@@ -1025,6 +1040,7 @@ impl DirSQLBuilder {
1025
1040
  mut ignore,
1026
1041
  mut extensions,
1027
1042
  config_path,
1043
+ suppress_config_extensions,
1028
1044
  mut persist,
1029
1045
  mut persist_path,
1030
1046
  poll_interval,
@@ -1058,17 +1074,22 @@ impl DirSQLBuilder {
1058
1074
 
1059
1075
  // Resolve config-supplied extension paths against the config
1060
1076
  // file's parent directory (absolute paths pass through). Appended
1061
- // after any programmatically-supplied extensions.
1062
- for ext in cfg.extensions {
1063
- let path = if ext.path.is_absolute() {
1064
- ext.path
1065
- } else {
1066
- cfg_parent.join(&ext.path)
1067
- };
1068
- extensions.push(Extension {
1069
- path,
1070
- entrypoint: ext.entrypoint,
1071
- });
1077
+ // after any programmatically-supplied extensions. Skipped entirely
1078
+ // when the caller has pre-resolved the config's extensions itself
1079
+ // (e.g. a launcher resolving package names) and supplied them via
1080
+ // `.extensions(...)` — see `suppress_config_extensions`.
1081
+ if !suppress_config_extensions {
1082
+ for ext in cfg.extensions {
1083
+ let path = if ext.path.is_absolute() {
1084
+ ext.path
1085
+ } else {
1086
+ cfg_parent.join(&ext.path)
1087
+ };
1088
+ extensions.push(Extension {
1089
+ path,
1090
+ entrypoint: ext.entrypoint,
1091
+ });
1092
+ }
1072
1093
  }
1073
1094
 
1074
1095
  if cfg.persist {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes