dirsql 0.3.70__tar.gz → 0.3.72__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 (136) hide show
  1. {dirsql-0.3.70 → dirsql-0.3.72}/Cargo.lock +1 -1
  2. {dirsql-0.3.70 → dirsql-0.3.72}/PKG-INFO +1 -1
  3. {dirsql-0.3.70 → dirsql-0.3.72}/dirsql/_async.py +7 -10
  4. {dirsql-0.3.70 → dirsql-0.3.72}/dirsql/cli/main.py +1 -1
  5. {dirsql-0.3.70 → dirsql-0.3.72}/dirsql/cli/resolve_config_extensions.py +7 -9
  6. {dirsql-0.3.70 → dirsql-0.3.72}/dirsql/resolve_config_extensions.py +8 -12
  7. {dirsql-0.3.70 → dirsql-0.3.72}/dirsql/resolve_extension.py +9 -18
  8. {dirsql-0.3.70/packages/rust → dirsql-0.3.72}/docs/reference/cli.md +42 -1
  9. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/Cargo.toml +1 -1
  10. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/conftest.py +4 -10
  11. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/reference/cli.md +42 -1
  12. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/e2e-attestation.json +2 -2
  13. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/src/lib.rs +11 -26
  14. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/tests/conftest.py +0 -1
  15. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/benches/differ_bench.rs +0 -3
  16. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/benches/scanner_bench.rs +0 -2
  17. {dirsql-0.3.70 → dirsql-0.3.72/packages/rust}/docs/reference/cli.md +42 -1
  18. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/bin/dirsql.rs +94 -17
  19. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/cli/execute.rs +1 -1
  20. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/cli/init.rs +5 -9
  21. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/cli/mod.rs +8 -27
  22. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/cli/serialize.rs +1 -9
  23. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/cli/server.rs +2 -7
  24. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/command.rs +5 -19
  25. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/config.rs +14 -58
  26. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/db.rs +58 -195
  27. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/differ.rs +3 -40
  28. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/lib.rs +115 -435
  29. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/matcher.rs +1 -15
  30. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/persist.rs +18 -57
  31. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/scanner.rs +3 -13
  32. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/watcher.rs +7 -11
  33. {dirsql-0.3.70 → dirsql-0.3.72}/Cargo.toml +0 -0
  34. {dirsql-0.3.70 → dirsql-0.3.72}/README.md +0 -0
  35. {dirsql-0.3.70 → dirsql-0.3.72}/dirsql/__init__.py +0 -0
  36. {dirsql-0.3.70 → dirsql-0.3.72}/dirsql/_dirsql.pyi +0 -0
  37. {dirsql-0.3.70 → dirsql-0.3.72}/dirsql/cli/__init__.py +0 -0
  38. {dirsql-0.3.70 → dirsql-0.3.72}/dirsql/cli/binary_path.py +0 -0
  39. {dirsql-0.3.70 → dirsql-0.3.72}/dirsql/cli/is_windows.py +0 -0
  40. {dirsql-0.3.70 → dirsql-0.3.72}/dirsql/py.typed +0 -0
  41. {dirsql-0.3.70 → dirsql-0.3.72}/docs/.claude/CLAUDE.md +0 -0
  42. {dirsql-0.3.70 → dirsql-0.3.72}/docs/.vitepress/config.ts +0 -0
  43. {dirsql-0.3.70 → dirsql-0.3.72}/docs/.vitepress/theme/index.ts +0 -0
  44. {dirsql-0.3.70 → dirsql-0.3.72}/docs/.vitepress/theme/lang.ts +0 -0
  45. {dirsql-0.3.70 → dirsql-0.3.72}/docs/AGENTS.md +0 -0
  46. {dirsql-0.3.70 → dirsql-0.3.72}/docs/explanation.md +0 -0
  47. {dirsql-0.3.70 → dirsql-0.3.72}/docs/getting-started.md +0 -0
  48. {dirsql-0.3.70 → dirsql-0.3.72}/docs/howto/columns-from-paths.md +0 -0
  49. {dirsql-0.3.70 → dirsql-0.3.72}/docs/howto/define-tables.md +0 -0
  50. {dirsql-0.3.70 → dirsql-0.3.72}/docs/howto/embed.md +0 -0
  51. {dirsql-0.3.70 → dirsql-0.3.72}/docs/howto/extract-from-contents.md +0 -0
  52. {dirsql-0.3.70 → dirsql-0.3.72}/docs/howto/load-extension.md +0 -0
  53. {dirsql-0.3.70 → dirsql-0.3.72}/docs/howto/persist.md +0 -0
  54. {dirsql-0.3.70 → dirsql-0.3.72}/docs/howto/react-to-changes.md +0 -0
  55. {dirsql-0.3.70 → dirsql-0.3.72}/docs/howto/search-by-meaning.md +0 -0
  56. {dirsql-0.3.70 → dirsql-0.3.72}/docs/howto/skip-files.md +0 -0
  57. {dirsql-0.3.70 → dirsql-0.3.72}/docs/index.md +0 -0
  58. {dirsql-0.3.70 → dirsql-0.3.72}/docs/migrations.md +0 -0
  59. {dirsql-0.3.70 → dirsql-0.3.72}/docs/package.json +0 -0
  60. {dirsql-0.3.70 → dirsql-0.3.72}/docs/playwright.config.ts +0 -0
  61. {dirsql-0.3.70 → dirsql-0.3.72}/docs/pnpm-lock.yaml +0 -0
  62. {dirsql-0.3.70 → dirsql-0.3.72}/docs/pnpm-workspace.yaml +0 -0
  63. {dirsql-0.3.70 → dirsql-0.3.72}/docs/reference/columns.md +0 -0
  64. {dirsql-0.3.70 → dirsql-0.3.72}/docs/reference/config.md +0 -0
  65. {dirsql-0.3.70 → dirsql-0.3.72}/docs/reference/hooks.md +0 -0
  66. {dirsql-0.3.70 → dirsql-0.3.72}/docs/reference/http-api.md +0 -0
  67. {dirsql-0.3.70 → dirsql-0.3.72}/docs/reference/sdk.md +0 -0
  68. {dirsql-0.3.70 → dirsql-0.3.72}/docs/tests/integration/home.spec.ts +0 -0
  69. {dirsql-0.3.70 → dirsql-0.3.72}/docs/tests/integration/language-flag.spec.ts +0 -0
  70. {dirsql-0.3.70 → dirsql-0.3.72}/docs/tests/integration/sidebar.spec.ts +0 -0
  71. {dirsql-0.3.70 → dirsql-0.3.72}/docs/tests/unit/config.test.ts +0 -0
  72. {dirsql-0.3.70 → dirsql-0.3.72}/docs/tests/unit/lang.test.ts +0 -0
  73. {dirsql-0.3.70 → dirsql-0.3.72}/docs/vitest.config.ts +0 -0
  74. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/README.md +0 -0
  75. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/.claude/CLAUDE.md +0 -0
  76. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/.vitepress/config.ts +0 -0
  77. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  78. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  79. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/AGENTS.md +0 -0
  80. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/explanation.md +0 -0
  81. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/getting-started.md +0 -0
  82. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/howto/columns-from-paths.md +0 -0
  83. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/howto/define-tables.md +0 -0
  84. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/howto/embed.md +0 -0
  85. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/howto/extract-from-contents.md +0 -0
  86. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/howto/load-extension.md +0 -0
  87. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/howto/persist.md +0 -0
  88. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/howto/react-to-changes.md +0 -0
  89. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/howto/search-by-meaning.md +0 -0
  90. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/howto/skip-files.md +0 -0
  91. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/index.md +0 -0
  92. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/migrations.md +0 -0
  93. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/package.json +0 -0
  94. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/playwright.config.ts +0 -0
  95. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/pnpm-lock.yaml +0 -0
  96. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/pnpm-workspace.yaml +0 -0
  97. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/reference/columns.md +0 -0
  98. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/reference/config.md +0 -0
  99. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/reference/hooks.md +0 -0
  100. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/reference/http-api.md +0 -0
  101. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/reference/sdk.md +0 -0
  102. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  103. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  104. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
  105. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/tests/unit/config.test.ts +0 -0
  106. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  107. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/docs/vitest.config.ts +0 -0
  108. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/tests/__init__.py +0 -0
  109. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/tests/binding/__init__.py +0 -0
  110. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/tests/e2e/__init__.py +0 -0
  111. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/tests/integration/__init__.py +0 -0
  112. {dirsql-0.3.70 → dirsql-0.3.72}/packages/python/tests/smoke/__init__.py +0 -0
  113. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/Cargo.toml +0 -0
  114. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/README.md +0 -0
  115. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/benches/db_bench.rs +0 -0
  116. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/benches/matcher_bench.rs +0 -0
  117. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/explanation.md +0 -0
  118. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/getting-started.md +0 -0
  119. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/howto/columns-from-paths.md +0 -0
  120. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/howto/define-tables.md +0 -0
  121. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/howto/embed.md +0 -0
  122. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/howto/extract-from-contents.md +0 -0
  123. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/howto/load-extension.md +0 -0
  124. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/howto/persist.md +0 -0
  125. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/howto/react-to-changes.md +0 -0
  126. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/howto/search-by-meaning.md +0 -0
  127. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/howto/skip-files.md +0 -0
  128. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/index.md +0 -0
  129. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/migrations.md +0 -0
  130. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/reference/columns.md +0 -0
  131. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/reference/config.md +0 -0
  132. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/reference/hooks.md +0 -0
  133. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/reference/http-api.md +0 -0
  134. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/docs/reference/sdk.md +0 -0
  135. {dirsql-0.3.70 → dirsql-0.3.72}/packages/rust/src/cli/router.rs +0 -0
  136. {dirsql-0.3.70 → dirsql-0.3.72}/pyproject.toml +0 -0
@@ -500,7 +500,7 @@ dependencies = [
500
500
 
501
501
  [[package]]
502
502
  name = "dirsql-py-ext"
503
- version = "0.3.70"
503
+ version = "0.3.72"
504
504
  dependencies = [
505
505
  "dirsql",
506
506
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirsql
3
- Version: 0.3.70
3
+ Version: 0.3.72
4
4
  Summary: Ephemeral SQL index over a local directory
5
5
  Keywords: sql,filesystem,directory,sqlite,index
6
6
  Author: Kevin Scott
@@ -59,7 +59,7 @@ class DirSQL:
59
59
  at startup. Any ``[[dirsql.extension]]`` entries in a ``config`` file are
60
60
  appended after the programmatic ones. A ``path`` (programmatic or
61
61
  config-file) may be a bare **package name**, resolved from the installed
62
- package in the runtime env (#298 / #313).
62
+ package in the runtime env.
63
63
  """
64
64
 
65
65
  def __init__(
@@ -98,15 +98,13 @@ class DirSQL:
98
98
  """Resolve extensions and construct the Rust-backed instance.
99
99
 
100
100
  Runs on a worker thread (via ``asyncio.to_thread``): both the
101
- package-name resolution (``importlib`` + filesystem globs) and the
102
- core's initial scan are blocking work.
101
+ package-name resolution and the core's initial scan are blocking.
103
102
 
104
103
  When the ``config`` file names an extension by bare package name, the
105
104
  SDK resolves every one of the config's ``[[dirsql.extension]]`` entries
106
- itself (#313) -- appended after the programmatic ones, matching the
107
- core's ordering -- and suppresses the core's own config-extension
108
- loading so the entries are not loaded a second time (and the core never
109
- sees the unresolvable bare name).
105
+ itself -- appended after the programmatic ones -- and suppresses the
106
+ core's own config-extension loading so the entries are not loaded a
107
+ second time (the core cannot resolve a bare name).
110
108
  """
111
109
  extensions = self._resolved_extensions()
112
110
  suppress = False
@@ -129,9 +127,8 @@ class DirSQL:
129
127
  def _resolved_extensions(self):
130
128
  """Resolve each programmatic extension's ``path`` to a loadable file.
131
129
 
132
- A bare package name is resolved to the loadable installed in the runtime
133
- env (#298); path-looking values are passed through verbatim (mirroring
134
- the Rust builder, which takes programmatic paths as-is). Config-file
130
+ A bare package name is resolved to the loadable installed in the
131
+ runtime env; path-looking values pass through verbatim. Config-file
135
132
  ``[[dirsql.extension]]`` entries are handled by ``_build_db``.
136
133
  """
137
134
  if not self._extensions:
@@ -24,7 +24,7 @@ def main(argv: list[str] | None = None) -> int:
24
24
  return 1
25
25
 
26
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).
27
+ # can't) and pass them as `--extension` flags; a no-op otherwise.
28
28
  try:
29
29
  argv = with_resolved_extensions(argv)
30
30
  except Exception as exc:
@@ -1,14 +1,12 @@
1
1
  """Launcher-side resolution of a TOML config's ``[[dirsql.extension]]`` entries.
2
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, the
7
- shared SDK resolver (:mod:`dirsql.resolve_config_extensions`, #313) resolves
8
- every one of its extensions and this launcher passes the resolved literal
9
- paths to the binary via repeatable ``--extension`` flags; the binary then
10
- loads those and ignores the config's own extension entries (the Rust
11
- ``--extension`` flag / ``suppress_config_extensions``).
3
+ The compiled ``dirsql`` binary loads a config's extensions literally -- it
4
+ has no ``importlib``, so it cannot resolve a bare **package name**. When a
5
+ TOML config names an extension by package name, the shared SDK resolver
6
+ (:mod:`dirsql.resolve_config_extensions`) resolves every one of its
7
+ extensions and this launcher passes the resolved literal paths to the binary
8
+ via repeatable ``--extension`` flags; the binary then loads those and ignores
9
+ the config's own extension entries.
12
10
 
13
11
  Native-language configs (``.py`` / ``.js`` / ``.mjs`` / ``.cjs``) are untouched:
14
12
  the binary dispatches those to ``dirsql interpret``, whose handshake already
@@ -1,16 +1,14 @@
1
1
  """SDK-side resolution of a TOML config's ``[[dirsql.extension]]`` entries.
2
2
 
3
- The Rust core parses a ``.dirsql.toml`` itself and loads its extensions
4
- literally -- it has no ``importlib``, so it cannot resolve a bare **package
5
- name** (#227). The SDK can (#313). When a TOML config names an extension by
6
- package name, the SDK resolves every one of its extensions here, hands the
7
- core the resolved literal paths, and suppresses the core's own config-extension
8
- loading (the Rust ``suppress_config_extensions`` builder toggle) so the
9
- config's entries are not loaded a second time.
3
+ The Rust core loads a config's extensions literally -- it has no
4
+ ``importlib``, so it cannot resolve a bare **package name**. When a TOML
5
+ config names an extension by package name, the SDK resolves every one of its
6
+ extensions here, hands the core the resolved literal paths, and suppresses
7
+ the core's own config-extension loading (``suppress_config_extensions``) so
8
+ the config's entries are not loaded a second time.
10
9
 
11
10
  Shared by the ``DirSQL`` constructor (``config=`` path) and the CLI launcher
12
- (``dirsql.cli.resolve_config_extensions``, which converts the resolved specs
13
- into ``--extension`` flags for the binary).
11
+ (which converts the resolved specs into ``--extension`` flags).
14
12
  """
15
13
 
16
14
  from __future__ import annotations
@@ -45,9 +43,7 @@ def resolve_config_extension_specs(config_path):
45
43
  entries = cfg.get("extension")
46
44
  if not isinstance(entries, list):
47
45
  return None
48
- # Only intervene when at least one path is a bare package name; a config
49
- # with only literal paths (or no entries at all) keeps the core's existing
50
- # behavior untouched.
46
+ # Only intervene when at least one path is a bare package name.
51
47
  if not any(
52
48
  isinstance(e, dict)
53
49
  and isinstance(e.get("path"), str)
@@ -1,23 +1,17 @@
1
1
  """Resolve an extension entry's ``path`` to a concrete loadable file.
2
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:
3
+ Resolution is an ordered probe (file-first, then package), so only a bare
4
+ package name reaches the package machinery:
11
5
 
12
6
  1. **Path-looking** (contains a separator, or ends in ``.so`` / ``.dylib`` /
13
7
  ``.dll`` / ``.pyd``) -- returned as a file path: made absolute against
14
8
  ``base`` when ``resolve_relative`` is set (config-file entries), else
15
- verbatim (programmatic entries, mirroring the Rust builder).
9
+ verbatim (programmatic entries).
16
10
  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.
11
+ package; otherwise the package dir is located via
12
+ :func:`importlib.util.find_spec` and the current platform's loadable is
13
+ globbed from inside it. Zero matches and multiple matches are both hard
14
+ errors -- the caller must disambiguate with a literal path.
21
15
  """
22
16
 
23
17
  import glob as _glob
@@ -25,9 +19,8 @@ import importlib.util
25
19
  import os
26
20
  import sys
27
21
 
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.
22
+ # Suffixes that mark a value as "already a file path", so package resolution
23
+ # is never attempted.
31
24
  _LOADABLE_SUFFIXES = (".so", ".dylib", ".dll", ".pyd")
32
25
 
33
26
 
@@ -73,8 +66,6 @@ def _resolve_package(name):
73
66
  matches.update(_glob.glob(os.path.join(d, "**", pat), recursive=True))
74
67
  found = sorted(matches)
75
68
 
76
- # Exactly one loadable resolves; unpacking (rather than a guarded
77
- # subscript) makes the zero- and multiple-match failures each observable.
78
69
  try:
79
70
  (single,) = found
80
71
  except ValueError:
@@ -1,10 +1,11 @@
1
1
  # CLI
2
2
 
3
- The `dirsql` binary has two modes:
3
+ The `dirsql` binary has three modes:
4
4
 
5
5
  | Invocation | Behavior |
6
6
  |---|---|
7
7
  | `dirsql` (no subcommand) | Start a long-lived HTTP server exposing a SQL view of a directory. See [HTTP API](./http-api.md). |
8
+ | `dirsql query "<sql>"` | Build the index, run one query, print the rows as JSON, exit. No server, no watch. |
8
9
  | `dirsql init` | Generate a starter `.dirsql.toml` by running `claude` over a directory. |
9
10
 
10
11
  ## Installation
@@ -97,6 +98,46 @@ with a JSON body describing the failure:
97
98
  | `0` | Clean shutdown after `SIGINT` / `SIGTERM`. |
98
99
  | `1` | Failed to bind `host:port`, or an error during shutdown. |
99
100
 
101
+ ## `dirsql query`
102
+
103
+ Run one SQL query from the shell — for ad-hoc inspection, scripting, and
104
+ docs verification snippets — without booting the server and `curl`ing it:
105
+
106
+ ```bash
107
+ dirsql query "SELECT _basename, _size FROM files ORDER BY _size DESC LIMIT 5"
108
+ # [{"_basename":"model.bin","_size":104857600}, …]
109
+
110
+ dirsql query "SELECT COUNT(*) AS n FROM posts" | jq '.[0].n'
111
+ ```
112
+
113
+ The subcommand builds the index, runs the SQL, prints the result rows as a
114
+ JSON array on stdout (byte-identical to the [`POST /query`](./http-api.md)
115
+ response body), and exits `0`.
116
+
117
+ `dirsql query` is a thin adapter over the **same query pipeline the server
118
+ uses**, so behavior is identical to `POST /query` by construction:
119
+
120
+ - **Config discovery** honors `--config` (default `./.dirsql.toml`),
121
+ [zero-config mode](#zero-config-mode), and `--extension` overrides,
122
+ exactly as server mode does.
123
+ - **Hooks** ([`pre-query`](./hooks.md#pre-query) /
124
+ [`post-query`](./hooks.md#post-query)) and the
125
+ [`[dirsql].hook-timeout`](./config.md#dirsql-keys) apply identically.
126
+ - The **30-second query timeout**, the **read-only rule**, and the
127
+ `_dirsql_*` **internal-table denial** apply identically. A rejected read
128
+ is an error, not empty output.
129
+
130
+ Errors print the same diagnostic the HTTP `{"error": …}` body carries —
131
+ config failures, SQL errors, rejected reads, hook failures, timeouts — to
132
+ stderr, with exit code `1`.
133
+
134
+ ### Exit codes
135
+
136
+ | Code | Meaning |
137
+ |---|---|
138
+ | `0` | Query succeeded; rows printed on stdout. |
139
+ | `1` | Any failure: config, SQL, rejected read, hook, or timeout. The diagnostic is on stderr. |
140
+
100
141
  ## `dirsql init`
101
142
 
102
143
  Generates a `.dirsql.toml` by running the `claude` CLI over the target
@@ -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.70"
7
+ version = "0.3.72"
8
8
  edition.workspace = true
9
9
  publish = false
10
10
  readme = "README.md"
@@ -1,15 +1,9 @@
1
1
  """Stub the compiled `_dirsql` extension for unit tests that don't need it.
2
2
 
3
- `cli_test.py` exercises the pure-Python launcher; it must be runnable
4
- without `maturin develop` having built the PyO3 extension. We stub the
5
- missing module here (rather than in the test file) so the stub is in
6
- place before pytest imports `dirsql.__init__`, which transitively
7
- imports real types from `dirsql._dirsql`.
8
-
9
- The stub is installed ONLY when the real extension can't be imported.
10
- When `maturin develop` has been run (as in CI), the real extension wins
11
- and tests that depend on it — `_async_test.py`, the integration suites —
12
- see the real bindings.
3
+ Must live here (not in a test file) so the stub is in place before pytest
4
+ imports `dirsql.__init__`, which imports real types from `dirsql._dirsql`.
5
+ Installed only when the real extension can't be imported; when `maturin
6
+ develop` has been run, the real bindings win.
13
7
  """
14
8
 
15
9
  from __future__ import annotations
@@ -1,10 +1,11 @@
1
1
  # CLI
2
2
 
3
- The `dirsql` binary has two modes:
3
+ The `dirsql` binary has three modes:
4
4
 
5
5
  | Invocation | Behavior |
6
6
  |---|---|
7
7
  | `dirsql` (no subcommand) | Start a long-lived HTTP server exposing a SQL view of a directory. See [HTTP API](./http-api.md). |
8
+ | `dirsql query "<sql>"` | Build the index, run one query, print the rows as JSON, exit. No server, no watch. |
8
9
  | `dirsql init` | Generate a starter `.dirsql.toml` by running `claude` over a directory. |
9
10
 
10
11
  ## Installation
@@ -97,6 +98,46 @@ with a JSON body describing the failure:
97
98
  | `0` | Clean shutdown after `SIGINT` / `SIGTERM`. |
98
99
  | `1` | Failed to bind `host:port`, or an error during shutdown. |
99
100
 
101
+ ## `dirsql query`
102
+
103
+ Run one SQL query from the shell — for ad-hoc inspection, scripting, and
104
+ docs verification snippets — without booting the server and `curl`ing it:
105
+
106
+ ```bash
107
+ dirsql query "SELECT _basename, _size FROM files ORDER BY _size DESC LIMIT 5"
108
+ # [{"_basename":"model.bin","_size":104857600}, …]
109
+
110
+ dirsql query "SELECT COUNT(*) AS n FROM posts" | jq '.[0].n'
111
+ ```
112
+
113
+ The subcommand builds the index, runs the SQL, prints the result rows as a
114
+ JSON array on stdout (byte-identical to the [`POST /query`](./http-api.md)
115
+ response body), and exits `0`.
116
+
117
+ `dirsql query` is a thin adapter over the **same query pipeline the server
118
+ uses**, so behavior is identical to `POST /query` by construction:
119
+
120
+ - **Config discovery** honors `--config` (default `./.dirsql.toml`),
121
+ [zero-config mode](#zero-config-mode), and `--extension` overrides,
122
+ exactly as server mode does.
123
+ - **Hooks** ([`pre-query`](./hooks.md#pre-query) /
124
+ [`post-query`](./hooks.md#post-query)) and the
125
+ [`[dirsql].hook-timeout`](./config.md#dirsql-keys) apply identically.
126
+ - The **30-second query timeout**, the **read-only rule**, and the
127
+ `_dirsql_*` **internal-table denial** apply identically. A rejected read
128
+ is an error, not empty output.
129
+
130
+ Errors print the same diagnostic the HTTP `{"error": …}` body carries —
131
+ config failures, SQL errors, rejected reads, hook failures, timeouts — to
132
+ stderr, with exit code `1`.
133
+
134
+ ### Exit codes
135
+
136
+ | Code | Meaning |
137
+ |---|---|
138
+ | `0` | Query succeeded; rows printed on stdout. |
139
+ | `1` | Any failure: config, SQL, rejected read, hook, or timeout. The diagnostic is on stderr. |
140
+
100
141
  ## `dirsql init`
101
142
 
102
143
  Generates a `.dirsql.toml` by running the `claude` CLI over the target
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "command": "uv run python -m pytest tests/e2e/ -x -q",
3
- "ran_at": 1783387386,
3
+ "ran_at": 1783420501,
4
4
  "exit_code": 0,
5
- "commit": "a9cf031533c564bf43c77d893745f9e85fed059f"
5
+ "commit": "fe9d09208be0b3c6084d9b916a41403d5822d096"
6
6
  }
@@ -20,8 +20,6 @@ mod python {
20
20
  use std::path::PathBuf;
21
21
  use std::time::Duration;
22
22
 
23
- // -- Public PyO3 classes ------------------------------------------------
24
-
25
23
  /// A table definition. Mirrors `dirsql::Table` but holds a Python
26
24
  /// callable for `extract`.
27
25
  #[pyclass(name = "Table", frozen)]
@@ -34,15 +32,11 @@ mod python {
34
32
  extract: Py<PyAny>,
35
33
  #[pyo3(get)]
36
34
  strict: bool,
37
- /// Parsed table name (from `ddl`), or `None` if the DDL doesn't
38
- /// match `CREATE TABLE <name> (...)`. Computed once at construction
39
- /// via `dirsql::db::parse_table_name` so the `dirsql interpret`
40
- /// dispatcher (#196) and other consumers share the core's single
41
- /// source of truth instead of re-parsing DDL. We return `None`
42
- /// rather than raising at construction so `DirSQL.ready()` keeps
43
- /// surfacing malformed DDLs as the loud failure path (the Rust
44
- /// core's `DirSqlError::Ddl`); callers that care about the name
45
- /// before that point can check `t.name is None` themselves.
35
+ /// Parsed table name (from `ddl`) via `dirsql::db::parse_table_name`,
36
+ /// or `None` if the DDL doesn't match `CREATE TABLE <name> (...)`.
37
+ /// `None` rather than a construction error so `DirSQL.ready()` keeps
38
+ /// surfacing malformed DDLs as the loud failure path (the core's
39
+ /// `DirSqlError::Ddl`).
46
40
  #[pyo3(get)]
47
41
  name: Option<String>,
48
42
  }
@@ -64,11 +58,9 @@ mod python {
64
58
  }
65
59
 
66
60
  /// Marshals a Python `{"path": str, "entrypoint"?: str}` mapping from the
67
- /// `extensions=` constructor argument into a [`dirsql::Extension`]. Mirrors
68
- /// the `[[dirsql.extension]]` config-file fields and the Rust builder's
69
- /// `Extension { path, entrypoint }`. Paths are taken verbatim (the
70
- /// programmatic surface does not resolve relative paths), matching
71
- /// `DirSQLBuilder::extensions`.
61
+ /// `extensions=` constructor argument into a [`dirsql::Extension`]. Paths
62
+ /// are taken verbatim; the programmatic surface does not resolve relative
63
+ /// paths.
72
64
  #[derive(FromPyObject)]
73
65
  struct PyExtensionSpec {
74
66
  #[pyo3(item)]
@@ -117,8 +109,8 @@ mod python {
117
109
  /// `suppress_config_extensions` skips the core's own loading of the
118
110
  /// config's `[[dirsql.extension]]` entries; the SDK sets it after
119
111
  /// resolving those entries itself (package names need `importlib`,
120
- /// which the core lacks -- #313) and passing the resolved literal
121
- /// paths via `extensions`, so the entries are not loaded twice.
112
+ /// which the core lacks) and passing the resolved literal paths via
113
+ /// `extensions`, so the entries are not loaded twice.
122
114
  #[new]
123
115
  #[pyo3(signature = (root=None, *, tables=None, ignore=None, config=None, persist=false, persist_path=None, extensions=None, suppress_config_extensions=false))]
124
116
  fn new(
@@ -205,8 +197,6 @@ mod python {
205
197
  }
206
198
  }
207
199
 
208
- // -- Helpers ------------------------------------------------------------
209
-
210
200
  fn build_table(py: Python<'_>, t: &PyTable) -> Table {
211
201
  let extract_ref = t.extract.clone_ref(py);
212
202
  let mut table = Table::try_new(
@@ -260,10 +250,7 @@ mod python {
260
250
  /// Pure, GIL-free intermediate for a row event. [`row_event_to_plain`]
261
251
  /// builds it from a core [`RowEvent`] (unit-testable without a Python
262
252
  /// interpreter); [`row_event_to_py`] then marshals it into the
263
- /// Python-facing [`PyRowEvent`] (the GIL step). Splitting the two keeps the
264
- /// variant -> action / field-selection mapping testable at the unit tier,
265
- /// mirroring the napi binding's pure `row_event_to_js`. The value-level
266
- /// `Row -> PyDict` conversion stays GIL-bound (binding-tier covered).
253
+ /// Python-facing [`PyRowEvent`] (the GIL step).
267
254
  struct PlainRowEvent {
268
255
  table: Option<String>,
269
256
  action: &'static str,
@@ -406,8 +393,6 @@ mod python {
406
393
  }
407
394
  }
408
395
 
409
- // -- Module registration ------------------------------------------------
410
-
411
396
  #[pymodule]
412
397
  #[pyo3(name = "_dirsql")]
413
398
  fn py_dirsql_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
@@ -15,7 +15,6 @@ def tmp_dir():
15
15
  @pytest.fixture
16
16
  def jsonl_dir(tmp_dir):
17
17
  """Create a temp dir with JSONL files for testing."""
18
- # Create a simple JSONL file
19
18
  os.makedirs(os.path.join(tmp_dir, "comments", "abc"), exist_ok=True)
20
19
  os.makedirs(os.path.join(tmp_dir, "comments", "def"), exist_ok=True)
21
20
 
@@ -46,7 +46,6 @@ fn bench_diff_single_line_change(c: &mut Criterion) {
46
46
  for count in [10, 100, 1000] {
47
47
  let old = make_rows(count);
48
48
  let mut new = old.clone();
49
- // Change one row in the middle
50
49
  new[count / 2].insert("name".to_string(), Value::Text("CHANGED".to_string()));
51
50
  group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, _| {
52
51
  b.iter(|| {
@@ -62,7 +61,6 @@ fn bench_diff_append(c: &mut Criterion) {
62
61
  for count in [10, 100, 1000] {
63
62
  let old = make_rows(count);
64
63
  let mut new = old.clone();
65
- // Append 10% more rows
66
64
  let extra = make_rows(count + count / 10);
67
65
  new.extend_from_slice(&extra[count..]);
68
66
  group.bench_with_input(BenchmarkId::from_parameter(count), &count, |b, _| {
@@ -78,7 +76,6 @@ fn bench_diff_full_replace(c: &mut Criterion) {
78
76
  let mut group = c.benchmark_group("differ/full_replace");
79
77
  for count in [10, 100, 1000] {
80
78
  let old = make_rows(count);
81
- // Completely different rows trigger full replace
82
79
  let new: Vec<HashMap<String, Value>> = (0..count)
83
80
  .map(|i| {
84
81
  HashMap::from([
@@ -7,7 +7,6 @@ use tempfile::TempDir;
7
7
  fn create_temp_tree(file_count: usize) -> TempDir {
8
8
  let dir = TempDir::new().unwrap();
9
9
  for i in 0..file_count {
10
- // Distribute files across subdirectories
11
10
  let subdir = dir.path().join(format!("dir_{}", i % 10));
12
11
  fs::create_dir_all(&subdir).unwrap();
13
12
  fs::write(
@@ -16,7 +15,6 @@ fn create_temp_tree(file_count: usize) -> TempDir {
16
15
  )
17
16
  .unwrap();
18
17
  }
19
- // Add some non-matching files
20
18
  for i in 0..file_count / 5 {
21
19
  fs::write(dir.path().join(format!("readme_{i}.md")), "# not matched").unwrap();
22
20
  }
@@ -1,10 +1,11 @@
1
1
  # CLI
2
2
 
3
- The `dirsql` binary has two modes:
3
+ The `dirsql` binary has three modes:
4
4
 
5
5
  | Invocation | Behavior |
6
6
  |---|---|
7
7
  | `dirsql` (no subcommand) | Start a long-lived HTTP server exposing a SQL view of a directory. See [HTTP API](./http-api.md). |
8
+ | `dirsql query "<sql>"` | Build the index, run one query, print the rows as JSON, exit. No server, no watch. |
8
9
  | `dirsql init` | Generate a starter `.dirsql.toml` by running `claude` over a directory. |
9
10
 
10
11
  ## Installation
@@ -97,6 +98,46 @@ with a JSON body describing the failure:
97
98
  | `0` | Clean shutdown after `SIGINT` / `SIGTERM`. |
98
99
  | `1` | Failed to bind `host:port`, or an error during shutdown. |
99
100
 
101
+ ## `dirsql query`
102
+
103
+ Run one SQL query from the shell — for ad-hoc inspection, scripting, and
104
+ docs verification snippets — without booting the server and `curl`ing it:
105
+
106
+ ```bash
107
+ dirsql query "SELECT _basename, _size FROM files ORDER BY _size DESC LIMIT 5"
108
+ # [{"_basename":"model.bin","_size":104857600}, …]
109
+
110
+ dirsql query "SELECT COUNT(*) AS n FROM posts" | jq '.[0].n'
111
+ ```
112
+
113
+ The subcommand builds the index, runs the SQL, prints the result rows as a
114
+ JSON array on stdout (byte-identical to the [`POST /query`](./http-api.md)
115
+ response body), and exits `0`.
116
+
117
+ `dirsql query` is a thin adapter over the **same query pipeline the server
118
+ uses**, so behavior is identical to `POST /query` by construction:
119
+
120
+ - **Config discovery** honors `--config` (default `./.dirsql.toml`),
121
+ [zero-config mode](#zero-config-mode), and `--extension` overrides,
122
+ exactly as server mode does.
123
+ - **Hooks** ([`pre-query`](./hooks.md#pre-query) /
124
+ [`post-query`](./hooks.md#post-query)) and the
125
+ [`[dirsql].hook-timeout`](./config.md#dirsql-keys) apply identically.
126
+ - The **30-second query timeout**, the **read-only rule**, and the
127
+ `_dirsql_*` **internal-table denial** apply identically. A rejected read
128
+ is an error, not empty output.
129
+
130
+ Errors print the same diagnostic the HTTP `{"error": …}` body carries —
131
+ config failures, SQL errors, rejected reads, hook failures, timeouts — to
132
+ stderr, with exit code `1`.
133
+
134
+ ### Exit codes
135
+
136
+ | Code | Meaning |
137
+ |---|---|
138
+ | `0` | Query succeeded; rows printed on stdout. |
139
+ | `1` | Any failure: config, SQL, rejected read, hook, or timeout. The diagnostic is on stderr. |
140
+
100
141
  ## `dirsql init`
101
142
 
102
143
  Generates a `.dirsql.toml` by running the `claude` CLI over the target