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.
- {dirsql-0.3.44 → dirsql-0.3.46}/Cargo.lock +1 -1
- {dirsql-0.3.44 → dirsql-0.3.46}/PKG-INFO +1 -1
- dirsql-0.3.46/docs/cli/config.md +358 -0
- {dirsql-0.3.44/packages/rust → dirsql-0.3.46}/docs/cli/http-api.md +9 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/Cargo.toml +1 -1
- dirsql-0.3.46/packages/python/docs/cli/config.md +358 -0
- {dirsql-0.3.44 → dirsql-0.3.46/packages/python}/docs/cli/http-api.md +9 -0
- dirsql-0.3.46/packages/rust/docs/cli/config.md +358 -0
- {dirsql-0.3.44/packages/python → dirsql-0.3.46/packages/rust}/docs/cli/http-api.md +9 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/bin/dirsql.rs +25 -2
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/cli/mod.rs +57 -1
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/cli/router.rs +81 -14
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/cli/server.rs +1 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/config.rs +126 -2
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/lib.rs +213 -19
- dirsql-0.3.44/docs/cli/config.md +0 -211
- dirsql-0.3.44/packages/python/docs/cli/config.md +0 -211
- dirsql-0.3.44/packages/rust/docs/cli/config.md +0 -211
- {dirsql-0.3.44 → dirsql-0.3.46}/Cargo.toml +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/README.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/__init__.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/_async.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/cli/interpret/__init__.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/dirsql/py.typed +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/AGENTS.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/api/index.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/cli/index.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/cli/init.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/cli/server.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/getting-started.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/async.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/querying.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/tables.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/guide/watching.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/index.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/migrations.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/package.json +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/README.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/conftest.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/api/index.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/e2e-attestation.json +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/README.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/api/index.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/command.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/db.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.44 → dirsql-0.3.46}/pyproject.toml +0 -0
|
@@ -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.
|
|
7
|
+
version = "0.3.46"
|
|
8
8
|
edition.workspace = true
|
|
9
9
|
publish = false
|
|
10
10
|
readme = "README.md"
|