dirsql 0.3.44__tar.gz → 0.3.46__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 (122) hide show
  1. {dirsql-0.3.44 → dirsql-0.3.46}/Cargo.lock +1 -1
  2. {dirsql-0.3.44 → dirsql-0.3.46}/PKG-INFO +1 -1
  3. dirsql-0.3.46/docs/cli/config.md +358 -0
  4. {dirsql-0.3.44/packages/rust → dirsql-0.3.46}/docs/cli/http-api.md +9 -0
  5. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/Cargo.toml +1 -1
  6. dirsql-0.3.46/packages/python/docs/cli/config.md +358 -0
  7. {dirsql-0.3.44 → dirsql-0.3.46/packages/python}/docs/cli/http-api.md +9 -0
  8. dirsql-0.3.46/packages/rust/docs/cli/config.md +358 -0
  9. {dirsql-0.3.44/packages/python → dirsql-0.3.46/packages/rust}/docs/cli/http-api.md +9 -0
  10. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/bin/dirsql.rs +25 -2
  11. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/cli/mod.rs +57 -1
  12. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/cli/router.rs +81 -14
  13. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/cli/server.rs +1 -0
  14. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/config.rs +126 -2
  15. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/lib.rs +213 -19
  16. dirsql-0.3.44/docs/cli/config.md +0 -211
  17. dirsql-0.3.44/packages/python/docs/cli/config.md +0 -211
  18. dirsql-0.3.44/packages/rust/docs/cli/config.md +0 -211
  19. {dirsql-0.3.44 → dirsql-0.3.46}/Cargo.toml +0 -0
  20. {dirsql-0.3.44 → dirsql-0.3.46}/README.md +0 -0
  21. {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/__init__.py +0 -0
  22. {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/_async.py +0 -0
  23. {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/_dirsql.pyi +0 -0
  24. {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/cli/__init__.py +0 -0
  25. {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/cli/binary_path.py +0 -0
  26. {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/cli/interpret/__init__.py +0 -0
  27. {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/cli/is_windows.py +0 -0
  28. {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/cli/main.py +0 -0
  29. {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/py.typed +0 -0
  30. {dirsql-0.3.44 → dirsql-0.3.46}/docs/.claude/CLAUDE.md +0 -0
  31. {dirsql-0.3.44 → dirsql-0.3.46}/docs/.vitepress/config.ts +0 -0
  32. {dirsql-0.3.44 → dirsql-0.3.46}/docs/.vitepress/theme/index.ts +0 -0
  33. {dirsql-0.3.44 → dirsql-0.3.46}/docs/.vitepress/theme/lang.ts +0 -0
  34. {dirsql-0.3.44 → dirsql-0.3.46}/docs/AGENTS.md +0 -0
  35. {dirsql-0.3.44 → dirsql-0.3.46}/docs/api/index.md +0 -0
  36. {dirsql-0.3.44 → dirsql-0.3.46}/docs/cli/index.md +0 -0
  37. {dirsql-0.3.44 → dirsql-0.3.46}/docs/cli/init.md +0 -0
  38. {dirsql-0.3.44 → dirsql-0.3.46}/docs/cli/server.md +0 -0
  39. {dirsql-0.3.44 → dirsql-0.3.46}/docs/getting-started.md +0 -0
  40. {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/async.md +0 -0
  41. {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/crdt.md +0 -0
  42. {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/persistence.md +0 -0
  43. {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/querying.md +0 -0
  44. {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/tables.md +0 -0
  45. {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/watching.md +0 -0
  46. {dirsql-0.3.44 → dirsql-0.3.46}/docs/index.md +0 -0
  47. {dirsql-0.3.44 → dirsql-0.3.46}/docs/migrations.md +0 -0
  48. {dirsql-0.3.44 → dirsql-0.3.46}/docs/package.json +0 -0
  49. {dirsql-0.3.44 → dirsql-0.3.46}/docs/playwright.config.ts +0 -0
  50. {dirsql-0.3.44 → dirsql-0.3.46}/docs/pnpm-lock.yaml +0 -0
  51. {dirsql-0.3.44 → dirsql-0.3.46}/docs/pnpm-workspace.yaml +0 -0
  52. {dirsql-0.3.44 → dirsql-0.3.46}/docs/tests/integration/home.spec.ts +0 -0
  53. {dirsql-0.3.44 → dirsql-0.3.46}/docs/tests/integration/language-flag.spec.ts +0 -0
  54. {dirsql-0.3.44 → dirsql-0.3.46}/docs/tests/integration/sidebar.spec.ts +0 -0
  55. {dirsql-0.3.44 → dirsql-0.3.46}/docs/tests/unit/config.test.ts +0 -0
  56. {dirsql-0.3.44 → dirsql-0.3.46}/docs/tests/unit/lang.test.ts +0 -0
  57. {dirsql-0.3.44 → dirsql-0.3.46}/docs/vitest.config.ts +0 -0
  58. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/README.md +0 -0
  59. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/conftest.py +0 -0
  60. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/.claude/CLAUDE.md +0 -0
  61. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/.vitepress/config.ts +0 -0
  62. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/.vitepress/theme/index.ts +0 -0
  63. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
  64. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/AGENTS.md +0 -0
  65. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/api/index.md +0 -0
  66. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/cli/index.md +0 -0
  67. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/cli/init.md +0 -0
  68. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/cli/server.md +0 -0
  69. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/getting-started.md +0 -0
  70. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/async.md +0 -0
  71. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/crdt.md +0 -0
  72. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/persistence.md +0 -0
  73. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/querying.md +0 -0
  74. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/tables.md +0 -0
  75. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/watching.md +0 -0
  76. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/index.md +0 -0
  77. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/migrations.md +0 -0
  78. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/package.json +0 -0
  79. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/playwright.config.ts +0 -0
  80. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/pnpm-lock.yaml +0 -0
  81. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/pnpm-workspace.yaml +0 -0
  82. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/tests/integration/home.spec.ts +0 -0
  83. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
  84. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
  85. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/tests/unit/config.test.ts +0 -0
  86. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/tests/unit/lang.test.ts +0 -0
  87. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/vitest.config.ts +0 -0
  88. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/e2e-attestation.json +0 -0
  89. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/src/lib.rs +0 -0
  90. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/tests/__init__.py +0 -0
  91. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/tests/conftest.py +0 -0
  92. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/tests/e2e/__init__.py +0 -0
  93. {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/tests/integration/__init__.py +0 -0
  94. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/Cargo.toml +0 -0
  95. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/README.md +0 -0
  96. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/benches/db_bench.rs +0 -0
  97. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/benches/differ_bench.rs +0 -0
  98. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/benches/matcher_bench.rs +0 -0
  99. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/benches/scanner_bench.rs +0 -0
  100. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/api/index.md +0 -0
  101. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/cli/index.md +0 -0
  102. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/cli/init.md +0 -0
  103. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/cli/server.md +0 -0
  104. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/getting-started.md +0 -0
  105. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/async.md +0 -0
  106. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/crdt.md +0 -0
  107. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/persistence.md +0 -0
  108. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/querying.md +0 -0
  109. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/tables.md +0 -0
  110. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/watching.md +0 -0
  111. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/index.md +0 -0
  112. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/migrations.md +0 -0
  113. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/cli/init.rs +0 -0
  114. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/cli/serialize.rs +0 -0
  115. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/command.rs +0 -0
  116. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/db.rs +0 -0
  117. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/differ.rs +0 -0
  118. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/matcher.rs +0 -0
  119. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/persist.rs +0 -0
  120. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/scanner.rs +0 -0
  121. {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/watcher.rs +0 -0
  122. {dirsql-0.3.44 → dirsql-0.3.46}/pyproject.toml +0 -0
@@ -501,7 +501,7 @@ dependencies = [
501
501
 
502
502
  [[package]]
503
503
  name = "dirsql-py-ext"
504
- version = "0.3.44"
504
+ version = "0.3.46"
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.44
3
+ Version: 0.3.46
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'
@@ -0,0 +1,358 @@
1
+ ---
2
+ canonical: https://thekevinscott.github.io/dirsql/cli/config
3
+ ---
4
+
5
+ # Configuration File
6
+
7
+ > Online: <https://thekevinscott.github.io/dirsql/cli/config>
8
+
9
+ `dirsql` is configured with an optional `.dirsql.toml` file; with none, the
10
+ server falls back to [zero-config defaults](./server.md#defaults). A
11
+ [TOML](#toml) config is declarative: it defines filesystem-fact tables (the
12
+ path, glob captures, and stat metadata) and works with any installation.
13
+
14
+ ## TOML
15
+
16
+ Reach for a TOML config — the default `.dirsql.toml` — to declare tables from
17
+ filesystem facts: a glob selects files, and columns come from path captures and
18
+ stat metadata. No code required, and it works with every installation.
19
+
20
+ ### Basic Example
21
+
22
+ ```toml
23
+ [dirsql]
24
+ ignore = ["node_modules/**", ".git/**"]
25
+
26
+ [[table]]
27
+ ddl = "CREATE TABLE posts (_path TEXT, _basename TEXT, _size INTEGER, _mtime INTEGER)"
28
+ glob = "posts/*.md"
29
+ ```
30
+
31
+ Each `posts/*.md` file produces one row in the `posts` table.
32
+
33
+ ### Loading a Config File
34
+
35
+ The CLI loads `./.dirsql.toml` by default; pass `--config <path>` to point at
36
+ another file. To load the same `.toml` from the SDK, pass its path to the
37
+ `DirSQL` constructor:
38
+
39
+ ::: code-group
40
+
41
+ ```python [Python]
42
+ from dirsql import DirSQL
43
+
44
+ db = DirSQL(config="./my-project/.dirsql.toml")
45
+ await db.ready()
46
+ ```
47
+
48
+ ```rust [Rust]
49
+ use dirsql::DirSQL;
50
+
51
+ let db = DirSQL::builder()
52
+ .config("./my-project/.dirsql.toml")
53
+ .build()?;
54
+ ```
55
+
56
+ ```typescript [TypeScript]
57
+ import { DirSQL } from "dirsql";
58
+
59
+ // String argument is interpreted as a config file path.
60
+ const db = new DirSQL("./my-project/.dirsql.toml");
61
+ await db.ready;
62
+ ```
63
+
64
+ :::
65
+
66
+ By default, the root directory scanned is the config file's parent
67
+ directory. Override it by passing `root` explicitly (the explicit value
68
+ wins and a warning is emitted) or by declaring `[dirsql].root` in the
69
+ config file itself.
70
+
71
+ ### Root Directory
72
+
73
+ By default, the config file's parent directory is the scan root. To index
74
+ a different location, declare `[dirsql].root` (relative paths are resolved
75
+ relative to the config file's parent):
76
+
77
+ ```toml
78
+ [dirsql]
79
+ root = "../data"
80
+ ignore = ["node_modules/**"]
81
+ ```
82
+
83
+ ### Stat Virtuals
84
+
85
+ Every config-defined table can expose any of these reserved columns. Add
86
+ the ones you want to your DDL; the rest are silently dropped.
87
+
88
+ | Column | Type | Source |
89
+ |--------|---------|--------|
90
+ | `_path` | TEXT | The file's path relative to the scan root. |
91
+ | `_basename` | TEXT | The filename including extension. |
92
+ | `_dir` | TEXT | The parent directory path (relative to root). |
93
+ | `_ext` | TEXT | The file extension, lowercased, no leading dot. |
94
+ | `_size` | INTEGER | Size in bytes. |
95
+ | `_mtime` | INTEGER | Last-modified time, unix seconds. |
96
+ | `_ctime` | INTEGER | Created/changed time, unix seconds. |
97
+
98
+ Example query:
99
+
100
+ ```sql
101
+ SELECT _basename, _size
102
+ FROM posts
103
+ WHERE _mtime > strftime('%s', '2024-01-01')
104
+ ORDER BY _mtime DESC;
105
+ ```
106
+
107
+ ### Path Captures
108
+
109
+ Use `{name}` in glob patterns to extract path segments as columns. Add a
110
+ matching column name to the DDL and the capture is auto-populated:
111
+
112
+ ```toml
113
+ [[table]]
114
+ ddl = "CREATE TABLE comments (thread_id TEXT, _basename TEXT, _mtime INTEGER)"
115
+ glob = "_comments/{thread_id}/*.jsonl"
116
+ ```
117
+
118
+ A file at `_comments/abc123/2024-05-05.jsonl` produces a row with
119
+ `thread_id = "abc123"`, `_basename = "2024-05-05.jsonl"`, and `_mtime` set
120
+ to the file's modification time.
121
+
122
+ ### Ignore Patterns
123
+
124
+ The `ignore` list skips files and directories entirely (not even scanned):
125
+
126
+ ```toml
127
+ [dirsql]
128
+ ignore = ["node_modules/**", ".git/**", "*.pyc", "__pycache__/**"]
129
+ ```
130
+
131
+ The top-level `.dirsql/` directory is always excluded, whether you list it
132
+ or not — it is a reserved namespace for `dirsql`'s own metadata (see
133
+ [Persistence](../guide/persistence.md)).
134
+
135
+ ### Persistence
136
+
137
+ Set `persist = true` to keep the SQLite database on disk between runs
138
+ instead of rebuilding from scratch on every startup:
139
+
140
+ ```toml
141
+ [dirsql]
142
+ persist = true
143
+ # persist_path = ".dirsql/cache.db" # optional; this is the default
144
+ ```
145
+
146
+ See [Persistence](../guide/persistence.md) for the full reconcile algorithm,
147
+ storage layout, and limitations.
148
+
149
+ ### Loading extensions
150
+
151
+ You can load SQLite extensions by specifying them in a config.
152
+
153
+ Declare each extension as a `[[dirsql.extension]]` entry:
154
+
155
+ ```toml
156
+ [[dirsql.extension]]
157
+ path = "./ext/myext.dylib"
158
+ entrypoint = "sqlite3_myext_init"
159
+ ```
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.
163
+ - **`entrypoint`** *(optional)* — the extension's init symbol. When omitted,
164
+ SQLite derives a default from the filename; set it when that default does not
165
+ match (for example, `sqlite-vec`'s entry point is `sqlite3_vec_init`).
166
+
167
+ **Note**: `dirsql` enables extension loading only while loading the configured libraries,
168
+ then disables it again, so `load_extension()` is not exposed via SQL to the user.
169
+
170
+ Extensions add **functions** you can call in queries and in a regular table's
171
+ DDL (defaults, generated columns). An extension-backed **virtual table** cannot
172
+ be declared as a `[[table]]` — `dirsql` tables are per-file row tables — so a
173
+ `CREATE VIRTUAL TABLE` DDL is rejected; call the extension's functions in your
174
+ queries instead.
175
+
176
+ ### Strict Mode
177
+
178
+ By default, auto-injected virtuals that aren't in the DDL are silently
179
+ dropped, and undeclared user-extract keys are dropped. Enable strict mode
180
+ to error when an extract emits keys not declared in the DDL:
181
+
182
+ ```toml
183
+ [[table]]
184
+ ddl = "CREATE TABLE comments (thread_id TEXT)"
185
+ glob = "_comments/{thread_id}/*.jsonl"
186
+ strict = true
187
+ ```
188
+
189
+ Strict mode does **not** apply to auto-injected stat virtuals — those are
190
+ always filtered to the DDL's declared columns regardless. Strict mode
191
+ applies only to keys produced by an extract callback (relevant for
192
+ programmatic [tables](../guide/tables.md)).
193
+
194
+ ### Per-file commands (`on-file`)
195
+
196
+ Reach for `on-file` when a table's rows come from the *contents* of each
197
+ matched file, not just its path and stat metadata. A filesystem-fact table
198
+ gives you one row per file; `on-file` runs a command per file that reads the
199
+ file and emits as many rows as it likes.
200
+
201
+ ```toml
202
+ [[table]]
203
+ ddl = "CREATE TABLE papers (paper_id TEXT, title TEXT)"
204
+ glob = "**/meta.json"
205
+ on-file = "uv run python extract_papers.py {path}"
206
+ ```
207
+
208
+ For every file matched by `glob`, `dirsql` runs the command. **The command
209
+ reads the file itself and prints a JSON array of row objects on stdout**; each
210
+ object becomes one row, its fields mapped to columns:
211
+
212
+ ```json
213
+ [
214
+ { "paper_id": "arXiv:2401.001", "title": "On Directories" },
215
+ { "paper_id": "arXiv:2401.002", "title": "SQL All The Way Down" }
216
+ ]
217
+ ```
218
+
219
+ Placeholders substituted into the command:
220
+
221
+ | Placeholder | Value |
222
+ |-------------|-------|
223
+ | `{path}` | The matched file's path **relative to the index root**. Appended automatically when the command omits it, so `extract.py` and `extract.py {path}` behave identically. |
224
+ | `{abspath}` | The matched file's absolute path. |
225
+ | `{root}` | The index root directory. |
226
+
227
+ Filesystem facts (stat virtuals and glob captures) are still merged onto every
228
+ `on-file` row, so you can declare `_path`, `_basename`, `{capture}`, etc. in the
229
+ DDL alongside the command's own columns — a column emitted by the command wins
230
+ over a same-named filesystem fact.
231
+
232
+ JSON values map to SQLite as follows: `null` → NULL; `true`/`false` → `1`/`0`;
233
+ an integer → INTEGER, any other number → REAL; a string → TEXT; a nested array
234
+ or object → its JSON text as TEXT.
235
+
236
+ **Per-file error isolation.** If a file's command fails — a non-zero exit, a
237
+ timeout, a spawn error, or output that isn't a JSON array of objects — that
238
+ file is skipped (it contributes no rows) and a one-line warning naming the file
239
+ and the error is written to stderr. One bad file never aborts the scan; the
240
+ other files' rows are indexed normally.
241
+
242
+ See [Command execution](#command-execution) for the full contract (argv
243
+ splitting, injection safety, cwd, environment, timeout, and output framing).
244
+
245
+ ### Rewriting queries (`pre-query`)
246
+
247
+ The `pre-query` hook intercepts every incoming request and transforms it into
248
+ the SQL that runs against the index. Because the hook owns SQL construction,
249
+ `POST /query` can accept whatever shape you want — a natural-language question,
250
+ a saved-query name, a templating DSL — and your command translates it to SQL
251
+ before it runs. Unlike `on-file` (a per-`[[table]]` key), `pre-query` is a
252
+ **server-wide** `[dirsql]` key: every query flows through it.
253
+
254
+ ```toml
255
+ [dirsql]
256
+ pre-query = "uv run python to_sql.py {args}"
257
+ ```
258
+
259
+ With `pre-query` set, the **raw `POST /query` request body** is passed to the
260
+ command as the `{args}` placeholder — a single, injection-safe argv token even
261
+ though the body is untrusted. The command prints **plain-text SQL** on stdout
262
+ (the last non-empty line is used); `dirsql` runs that SQL and returns rows
263
+ exactly as it would for a normal query.
264
+
265
+ | Placeholder | Value |
266
+ |-------------|-------|
267
+ | `{args}` | The raw `POST /query` request body, verbatim, as one argv token. |
268
+
269
+ When `pre-query` is **absent**, nothing changes: the request body is parsed as
270
+ `{"sql": "…"}` JSON and executed — the [HTTP API](./http-api.md#post-query)
271
+ default. Enabling the hook is fully backward compatible in reverse: remove the
272
+ key and the `{"sql": …}` contract returns.
273
+
274
+ **On failure** — a non-zero exit, a timeout, or a spawn error — the request
275
+ returns `500 Internal Server Error` with the command's stderr tail in the JSON
276
+ `error` body. The command runs in the config file's directory and is bounded by
277
+ a fixed **30-second** timeout.
278
+
279
+ #### The hook owns SQL safety
280
+
281
+ Because the hook returns **plain SQL** (not a parameterized query), it is the
282
+ **trusted component** that turns the untrusted request body into safe SQL. The
283
+ `{args}` substitution keeps the body inert *as an argv token* — it can never
284
+ break out into extra command arguments — but whatever SQL string the hook
285
+ prints is executed as-is. Validate, escape, or parameterize **inside** the
286
+ hook. This trade-off is intentional for v1: it keeps the contract a simple
287
+ plain-text-SQL pipe and puts translation logic — and its safety — in your hook.
288
+
289
+ Worked example — a hook that maps a saved-query name to SQL:
290
+
291
+ ```python
292
+ # to_sql.py
293
+ import sys
294
+
295
+ QUERIES = {
296
+ "recent-posts": "SELECT title, author FROM posts ORDER BY _mtime DESC LIMIT 10",
297
+ }
298
+ name = sys.argv[1].strip() if len(sys.argv) > 1 else ""
299
+ # Fall back to an empty result rather than trusting arbitrary input.
300
+ print(QUERIES.get(name, "SELECT 1 WHERE 0"))
301
+ ```
302
+
303
+ ```bash
304
+ curl -s http://localhost:7117/query -d 'recent-posts' | jq
305
+ ```
306
+
307
+ See [Command execution](#command-execution) for the full contract (argv
308
+ splitting, injection safety, cwd, environment, timeout, and output framing).
309
+
310
+ ### Full Example
311
+
312
+ ```toml
313
+ [dirsql]
314
+ ignore = ["node_modules/**", ".git/**", "dist/**"]
315
+
316
+ [[table]]
317
+ ddl = "CREATE TABLE comments (thread_id TEXT, _basename TEXT, _mtime INTEGER)"
318
+ glob = "_comments/{thread_id}/*.jsonl"
319
+
320
+ [[table]]
321
+ ddl = "CREATE TABLE documents (_path TEXT, _basename TEXT, _size INTEGER)"
322
+ glob = "**/index.md"
323
+
324
+ [[table]]
325
+ ddl = "CREATE TABLE logs (_path TEXT, _size INTEGER, _mtime INTEGER)"
326
+ glob = "logs/*.csv"
327
+ ```
328
+
329
+ ## Command execution
330
+
331
+ Config keys that run an external command — today `on-file` and `pre-query`,
332
+ with more events to follow — share one execution contract:
333
+
334
+ - **argv, not a shell.** The command string is split into an argv with
335
+ shell-like quoting (spaces separate arguments; quotes group them), but **no
336
+ shell is invoked** — there is no globbing, piping, `$VAR` expansion, or
337
+ `&&`/`;` chaining. To get those, ask for a shell explicitly:
338
+ `sh -c 'grep foo {path} | sort'` — the quoted script stays a single argument.
339
+ - **Injection-safe placeholders.** Each placeholder (`{path}`, `{abspath}`,
340
+ `{root}`, …) is substituted into whole argv tokens, every occurrence, in a
341
+ single left-to-right pass. A substituted value is always exactly one argv
342
+ element, so a path with spaces — or untrusted content that itself contains
343
+ `{…}` or shell metacharacters — is inert and never re-scanned. An unknown
344
+ `{…}` is left literal.
345
+ - **Working directory.** The command runs in the **config file's directory**,
346
+ so relative paths in the command resolve predictably regardless of where you
347
+ launched `dirsql`.
348
+ - **Environment.** The command inherits `dirsql`'s environment, so tools like
349
+ `uvx --with …` / `npx …` resolve their dependencies as usual.
350
+ - **Output framing.** The command's result is the **last non-empty line of
351
+ stdout**; any log/chatter lines above it are ignored. stderr is never data —
352
+ it is captured only to enrich error messages.
353
+ - **Timeout.** Each command run is bounded by a fixed **30-second** timeout (no
354
+ per-table override yet); a command that exceeds it is killed and treated as a
355
+ failure.
356
+ - **Errors.** A non-zero exit, a timeout, a spawn failure, or output that does
357
+ not parse as expected is a per-file failure: the file is skipped with a
358
+ stderr warning and the scan continues.
@@ -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.44"
7
+ version = "0.3.46"
8
8
  edition.workspace = true
9
9
  publish = false
10
10
  readme = "README.md"