dirsql 0.3.48__tar.gz → 0.3.50__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.48 → dirsql-0.3.50}/Cargo.lock +1 -1
- {dirsql-0.3.48 → dirsql-0.3.50}/PKG-INFO +1 -1
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/cli/config.md +56 -2
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/cli/http-api.md +9 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.48/packages/rust → dirsql-0.3.50/packages/python}/docs/cli/config.md +56 -2
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/cli/http-api.md +9 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/e2e-attestation.json +2 -2
- {dirsql-0.3.48/packages/python → dirsql-0.3.50/packages/rust}/docs/cli/config.md +56 -2
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/cli/http-api.md +9 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/bin/dirsql.rs +23 -1
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/cli/mod.rs +57 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/cli/router.rs +85 -2
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/cli/server.rs +1 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/config.rs +71 -11
- {dirsql-0.3.48 → dirsql-0.3.50}/Cargo.toml +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/README.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/dirsql/__init__.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/dirsql/_async.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/dirsql/cli/interpret/__init__.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/dirsql/cli/resolve_config_extensions.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/dirsql/py.typed +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/dirsql/resolve_extension.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/AGENTS.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/api/index.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/cli/index.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/cli/init.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/cli/server.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/getting-started.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/guide/async.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/guide/querying.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/guide/tables.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/guide/watching.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/index.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/migrations.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/package.json +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/README.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/conftest.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/api/index.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/README.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/api/index.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/command.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/db.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/lib.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.48 → dirsql-0.3.50}/pyproject.toml +0 -0
|
@@ -310,6 +310,60 @@ curl -s http://localhost:7117/query -d 'recent-posts' | jq
|
|
|
310
310
|
See [Command execution](#command-execution) for the full contract (argv
|
|
311
311
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
312
312
|
|
|
313
|
+
### Reshaping responses (`post-query`)
|
|
314
|
+
|
|
315
|
+
The `post-query` hook lets you reshape every query response before it's
|
|
316
|
+
returned — wrap the rows in an envelope, rename or project fields, add
|
|
317
|
+
metadata, or emit whatever JSON shape your clients expect. Because the hook
|
|
318
|
+
owns the response body, `POST /query` can return any structure you like instead
|
|
319
|
+
of the default bare array of row objects. Like [`pre-query`](#rewriting-queries-pre-query),
|
|
320
|
+
`post-query` is a **server-wide** `[dirsql]` key: every successful query flows
|
|
321
|
+
through it.
|
|
322
|
+
|
|
323
|
+
```toml
|
|
324
|
+
[dirsql]
|
|
325
|
+
post-query = "jq -c '{results: .}'"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The `-c` (compact) flag matters: the response body is the command's **last
|
|
329
|
+
non-empty stdout line** (see [Command execution](#command-execution)), so the
|
|
330
|
+
command must print its JSON on a single line — `jq`'s default multi-line
|
|
331
|
+
pretty-printing would leave only a trailing `}` as the last line.
|
|
332
|
+
|
|
333
|
+
After a successful query, `dirsql` serializes the result rows to a **JSON
|
|
334
|
+
array** and hands it to the command two ways:
|
|
335
|
+
|
|
336
|
+
- **On stdin** — always, unbounded and injection-safe. This is the recommended
|
|
337
|
+
path, and it's what the canonical `jq -c '{results: .}'` example reads.
|
|
338
|
+
- **As the `{args}` placeholder** — for small result sets. When the serialized
|
|
339
|
+
payload exceeds **96 KiB** (comfortably under the OS single-argument limit),
|
|
340
|
+
`{args}` is substituted with an **empty string** and a warning naming the byte
|
|
341
|
+
size is logged to stderr, directing the operator to read stdin instead. The
|
|
342
|
+
full payload is still delivered on stdin — this is a fallback, **not**
|
|
343
|
+
truncation.
|
|
344
|
+
|
|
345
|
+
| Placeholder | Value |
|
|
346
|
+
|-------------|-------|
|
|
347
|
+
| `{args}` | The result rows as a JSON array, as one argv token — emptied (with a stderr warning) when the payload exceeds 96 KiB; read stdin for the full set. |
|
|
348
|
+
|
|
349
|
+
The command prints the **JSON response body** on stdout (the last non-empty
|
|
350
|
+
line is used); `dirsql` parses it with `serde_json` and returns it as `200
|
|
351
|
+
application/json`. If that line is **not valid JSON**, the request returns `500
|
|
352
|
+
Internal Server Error` with `post-query did not return valid JSON: <err>`.
|
|
353
|
+
|
|
354
|
+
When `post-query` is **absent**, nothing changes: the rows are returned as-is —
|
|
355
|
+
the [HTTP API](./http-api.md#post-query) default. Enabling the hook is fully
|
|
356
|
+
backward compatible in reverse: remove the key and the bare-array contract
|
|
357
|
+
returns.
|
|
358
|
+
|
|
359
|
+
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
360
|
+
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
361
|
+
`error` body. The command runs in the config file's directory and is bounded by
|
|
362
|
+
a fixed **30-second** timeout.
|
|
363
|
+
|
|
364
|
+
See [Command execution](#command-execution) for the full contract (argv
|
|
365
|
+
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
366
|
+
|
|
313
367
|
### Full Example
|
|
314
368
|
|
|
315
369
|
```toml
|
|
@@ -331,8 +385,8 @@ glob = "logs/*.csv"
|
|
|
331
385
|
|
|
332
386
|
## Command execution
|
|
333
387
|
|
|
334
|
-
Config keys that run an external command — today `on-file
|
|
335
|
-
with more events to follow — share one execution contract:
|
|
388
|
+
Config keys that run an external command — today `on-file`, `pre-query`, and
|
|
389
|
+
`post-query`, with more events to follow — share one execution contract:
|
|
336
390
|
|
|
337
391
|
- **argv, not a shell.** The command string is split into an argv with
|
|
338
392
|
shell-like quoting (spaces separate arguments; quotes group them), but **no
|
|
@@ -44,6 +44,15 @@ command's stderr tail. With no `pre-query` key, the `{"sql": …}` contract abov
|
|
|
44
44
|
applies.
|
|
45
45
|
:::
|
|
46
46
|
|
|
47
|
+
::: tip `post-query` changes the response body
|
|
48
|
+
When [`[dirsql].post-query`](./config.md#reshaping-responses-post-query) is
|
|
49
|
+
configured, the result rows are handed to the hook command (as a JSON array on
|
|
50
|
+
stdin, and as `{args}` for small sets) and the JSON body it prints is returned
|
|
51
|
+
instead of the bare array above. A hook that fails (non-zero exit, timeout,
|
|
52
|
+
spawn error) or prints invalid JSON returns `500`. With no `post-query` key, the
|
|
53
|
+
array-of-rows response above applies.
|
|
54
|
+
:::
|
|
55
|
+
|
|
47
56
|
```bash
|
|
48
57
|
curl -s http://localhost:7117/query \
|
|
49
58
|
-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.50"
|
|
8
8
|
edition.workspace = true
|
|
9
9
|
publish = false
|
|
10
10
|
readme = "README.md"
|
|
@@ -310,6 +310,60 @@ curl -s http://localhost:7117/query -d 'recent-posts' | jq
|
|
|
310
310
|
See [Command execution](#command-execution) for the full contract (argv
|
|
311
311
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
312
312
|
|
|
313
|
+
### Reshaping responses (`post-query`)
|
|
314
|
+
|
|
315
|
+
The `post-query` hook lets you reshape every query response before it's
|
|
316
|
+
returned — wrap the rows in an envelope, rename or project fields, add
|
|
317
|
+
metadata, or emit whatever JSON shape your clients expect. Because the hook
|
|
318
|
+
owns the response body, `POST /query` can return any structure you like instead
|
|
319
|
+
of the default bare array of row objects. Like [`pre-query`](#rewriting-queries-pre-query),
|
|
320
|
+
`post-query` is a **server-wide** `[dirsql]` key: every successful query flows
|
|
321
|
+
through it.
|
|
322
|
+
|
|
323
|
+
```toml
|
|
324
|
+
[dirsql]
|
|
325
|
+
post-query = "jq -c '{results: .}'"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The `-c` (compact) flag matters: the response body is the command's **last
|
|
329
|
+
non-empty stdout line** (see [Command execution](#command-execution)), so the
|
|
330
|
+
command must print its JSON on a single line — `jq`'s default multi-line
|
|
331
|
+
pretty-printing would leave only a trailing `}` as the last line.
|
|
332
|
+
|
|
333
|
+
After a successful query, `dirsql` serializes the result rows to a **JSON
|
|
334
|
+
array** and hands it to the command two ways:
|
|
335
|
+
|
|
336
|
+
- **On stdin** — always, unbounded and injection-safe. This is the recommended
|
|
337
|
+
path, and it's what the canonical `jq -c '{results: .}'` example reads.
|
|
338
|
+
- **As the `{args}` placeholder** — for small result sets. When the serialized
|
|
339
|
+
payload exceeds **96 KiB** (comfortably under the OS single-argument limit),
|
|
340
|
+
`{args}` is substituted with an **empty string** and a warning naming the byte
|
|
341
|
+
size is logged to stderr, directing the operator to read stdin instead. The
|
|
342
|
+
full payload is still delivered on stdin — this is a fallback, **not**
|
|
343
|
+
truncation.
|
|
344
|
+
|
|
345
|
+
| Placeholder | Value |
|
|
346
|
+
|-------------|-------|
|
|
347
|
+
| `{args}` | The result rows as a JSON array, as one argv token — emptied (with a stderr warning) when the payload exceeds 96 KiB; read stdin for the full set. |
|
|
348
|
+
|
|
349
|
+
The command prints the **JSON response body** on stdout (the last non-empty
|
|
350
|
+
line is used); `dirsql` parses it with `serde_json` and returns it as `200
|
|
351
|
+
application/json`. If that line is **not valid JSON**, the request returns `500
|
|
352
|
+
Internal Server Error` with `post-query did not return valid JSON: <err>`.
|
|
353
|
+
|
|
354
|
+
When `post-query` is **absent**, nothing changes: the rows are returned as-is —
|
|
355
|
+
the [HTTP API](./http-api.md#post-query) default. Enabling the hook is fully
|
|
356
|
+
backward compatible in reverse: remove the key and the bare-array contract
|
|
357
|
+
returns.
|
|
358
|
+
|
|
359
|
+
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
360
|
+
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
361
|
+
`error` body. The command runs in the config file's directory and is bounded by
|
|
362
|
+
a fixed **30-second** timeout.
|
|
363
|
+
|
|
364
|
+
See [Command execution](#command-execution) for the full contract (argv
|
|
365
|
+
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
366
|
+
|
|
313
367
|
### Full Example
|
|
314
368
|
|
|
315
369
|
```toml
|
|
@@ -331,8 +385,8 @@ glob = "logs/*.csv"
|
|
|
331
385
|
|
|
332
386
|
## Command execution
|
|
333
387
|
|
|
334
|
-
Config keys that run an external command — today `on-file
|
|
335
|
-
with more events to follow — share one execution contract:
|
|
388
|
+
Config keys that run an external command — today `on-file`, `pre-query`, and
|
|
389
|
+
`post-query`, with more events to follow — share one execution contract:
|
|
336
390
|
|
|
337
391
|
- **argv, not a shell.** The command string is split into an argv with
|
|
338
392
|
shell-like quoting (spaces separate arguments; quotes group them), but **no
|
|
@@ -44,6 +44,15 @@ command's stderr tail. With no `pre-query` key, the `{"sql": …}` contract abov
|
|
|
44
44
|
applies.
|
|
45
45
|
:::
|
|
46
46
|
|
|
47
|
+
::: tip `post-query` changes the response body
|
|
48
|
+
When [`[dirsql].post-query`](./config.md#reshaping-responses-post-query) is
|
|
49
|
+
configured, the result rows are handed to the hook command (as a JSON array on
|
|
50
|
+
stdin, and as `{args}` for small sets) and the JSON body it prints is returned
|
|
51
|
+
instead of the bare array above. A hook that fails (non-zero exit, timeout,
|
|
52
|
+
spawn error) or prints invalid JSON returns `500`. With no `post-query` key, the
|
|
53
|
+
array-of-rows response above applies.
|
|
54
|
+
:::
|
|
55
|
+
|
|
47
56
|
```bash
|
|
48
57
|
curl -s http://localhost:7117/query \
|
|
49
58
|
-H 'content-type: application/json' \
|
|
@@ -310,6 +310,60 @@ curl -s http://localhost:7117/query -d 'recent-posts' | jq
|
|
|
310
310
|
See [Command execution](#command-execution) for the full contract (argv
|
|
311
311
|
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
312
312
|
|
|
313
|
+
### Reshaping responses (`post-query`)
|
|
314
|
+
|
|
315
|
+
The `post-query` hook lets you reshape every query response before it's
|
|
316
|
+
returned — wrap the rows in an envelope, rename or project fields, add
|
|
317
|
+
metadata, or emit whatever JSON shape your clients expect. Because the hook
|
|
318
|
+
owns the response body, `POST /query` can return any structure you like instead
|
|
319
|
+
of the default bare array of row objects. Like [`pre-query`](#rewriting-queries-pre-query),
|
|
320
|
+
`post-query` is a **server-wide** `[dirsql]` key: every successful query flows
|
|
321
|
+
through it.
|
|
322
|
+
|
|
323
|
+
```toml
|
|
324
|
+
[dirsql]
|
|
325
|
+
post-query = "jq -c '{results: .}'"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The `-c` (compact) flag matters: the response body is the command's **last
|
|
329
|
+
non-empty stdout line** (see [Command execution](#command-execution)), so the
|
|
330
|
+
command must print its JSON on a single line — `jq`'s default multi-line
|
|
331
|
+
pretty-printing would leave only a trailing `}` as the last line.
|
|
332
|
+
|
|
333
|
+
After a successful query, `dirsql` serializes the result rows to a **JSON
|
|
334
|
+
array** and hands it to the command two ways:
|
|
335
|
+
|
|
336
|
+
- **On stdin** — always, unbounded and injection-safe. This is the recommended
|
|
337
|
+
path, and it's what the canonical `jq -c '{results: .}'` example reads.
|
|
338
|
+
- **As the `{args}` placeholder** — for small result sets. When the serialized
|
|
339
|
+
payload exceeds **96 KiB** (comfortably under the OS single-argument limit),
|
|
340
|
+
`{args}` is substituted with an **empty string** and a warning naming the byte
|
|
341
|
+
size is logged to stderr, directing the operator to read stdin instead. The
|
|
342
|
+
full payload is still delivered on stdin — this is a fallback, **not**
|
|
343
|
+
truncation.
|
|
344
|
+
|
|
345
|
+
| Placeholder | Value |
|
|
346
|
+
|-------------|-------|
|
|
347
|
+
| `{args}` | The result rows as a JSON array, as one argv token — emptied (with a stderr warning) when the payload exceeds 96 KiB; read stdin for the full set. |
|
|
348
|
+
|
|
349
|
+
The command prints the **JSON response body** on stdout (the last non-empty
|
|
350
|
+
line is used); `dirsql` parses it with `serde_json` and returns it as `200
|
|
351
|
+
application/json`. If that line is **not valid JSON**, the request returns `500
|
|
352
|
+
Internal Server Error` with `post-query did not return valid JSON: <err>`.
|
|
353
|
+
|
|
354
|
+
When `post-query` is **absent**, nothing changes: the rows are returned as-is —
|
|
355
|
+
the [HTTP API](./http-api.md#post-query) default. Enabling the hook is fully
|
|
356
|
+
backward compatible in reverse: remove the key and the bare-array contract
|
|
357
|
+
returns.
|
|
358
|
+
|
|
359
|
+
**On failure** — a non-zero exit, a timeout, or a spawn error — the request
|
|
360
|
+
returns `500 Internal Server Error` with the command's stderr tail in the JSON
|
|
361
|
+
`error` body. The command runs in the config file's directory and is bounded by
|
|
362
|
+
a fixed **30-second** timeout.
|
|
363
|
+
|
|
364
|
+
See [Command execution](#command-execution) for the full contract (argv
|
|
365
|
+
splitting, injection safety, cwd, environment, timeout, and output framing).
|
|
366
|
+
|
|
313
367
|
### Full Example
|
|
314
368
|
|
|
315
369
|
```toml
|
|
@@ -331,8 +385,8 @@ glob = "logs/*.csv"
|
|
|
331
385
|
|
|
332
386
|
## Command execution
|
|
333
387
|
|
|
334
|
-
Config keys that run an external command — today `on-file
|
|
335
|
-
with more events to follow — share one execution contract:
|
|
388
|
+
Config keys that run an external command — today `on-file`, `pre-query`, and
|
|
389
|
+
`post-query`, with more events to follow — share one execution contract:
|
|
336
390
|
|
|
337
391
|
- **argv, not a shell.** The command string is split into an argv with
|
|
338
392
|
shell-like quoting (spaces separate arguments; quotes group them), but **no
|
|
@@ -44,6 +44,15 @@ command's stderr tail. With no `pre-query` key, the `{"sql": …}` contract abov
|
|
|
44
44
|
applies.
|
|
45
45
|
:::
|
|
46
46
|
|
|
47
|
+
::: tip `post-query` changes the response body
|
|
48
|
+
When [`[dirsql].post-query`](./config.md#reshaping-responses-post-query) is
|
|
49
|
+
configured, the result rows are handed to the hook command (as a JSON array on
|
|
50
|
+
stdin, and as `{args}` for small sets) and the JSON body it prints is returned
|
|
51
|
+
instead of the bare array above. A hook that fails (non-zero exit, timeout,
|
|
52
|
+
spawn error) or prints invalid JSON returns `500`. With no `post-query` key, the
|
|
53
|
+
array-of-rows response above applies.
|
|
54
|
+
:::
|
|
55
|
+
|
|
47
56
|
```bash
|
|
48
57
|
curl -s http://localhost:7117/query \
|
|
49
58
|
-H 'content-type: application/json' \
|
|
@@ -8,7 +8,9 @@ use std::path::{Path, PathBuf};
|
|
|
8
8
|
use std::process::ExitCode;
|
|
9
9
|
|
|
10
10
|
use clap::{Args, Parser, Subcommand};
|
|
11
|
-
use dirsql::cli::{
|
|
11
|
+
use dirsql::cli::{
|
|
12
|
+
AppState, PostQuery, PreQuery, ServerConfig, init::InitOptions, serve_with_state,
|
|
13
|
+
};
|
|
12
14
|
use dirsql::{DirSQL, Extension, Row, Table};
|
|
13
15
|
|
|
14
16
|
#[derive(Debug, Parser)]
|
|
@@ -122,6 +124,9 @@ async fn run_server(cli: Cli) -> ExitCode {
|
|
|
122
124
|
if let Some(pre_query) = load_pre_query(&cli) {
|
|
123
125
|
server_config = server_config.with_pre_query(pre_query);
|
|
124
126
|
}
|
|
127
|
+
if let Some(post_query) = load_post_query(&cli) {
|
|
128
|
+
server_config = server_config.with_post_query(post_query);
|
|
129
|
+
}
|
|
125
130
|
|
|
126
131
|
let host = cli.host.clone();
|
|
127
132
|
let handle = match serve_with_state(server_config, state).await {
|
|
@@ -228,6 +233,23 @@ fn load_pre_query(cli: &Cli) -> Option<PreQuery> {
|
|
|
228
233
|
Some(PreQuery::new(command, config_dir))
|
|
229
234
|
}
|
|
230
235
|
|
|
236
|
+
/// Extract the server-wide `post-query` hook from the config, if any.
|
|
237
|
+
///
|
|
238
|
+
/// Returns `None` when the config is absent, unresolvable, unparsable, or
|
|
239
|
+
/// declares no `post-query` — the server then returns `POST /query` result rows
|
|
240
|
+
/// as-is (the degraded / zero-config paths never get a hook). The command's
|
|
241
|
+
/// working directory is the config file's parent, mirroring [`load_pre_query`].
|
|
242
|
+
fn load_post_query(cli: &Cli) -> Option<PostQuery> {
|
|
243
|
+
let config_path = &cli.config;
|
|
244
|
+
if !config_path.exists() {
|
|
245
|
+
return None;
|
|
246
|
+
}
|
|
247
|
+
let resolved = config_path.canonicalize().ok()?;
|
|
248
|
+
let command = dirsql::config::load_config(&resolved).ok()?.post_query?;
|
|
249
|
+
let config_dir = resolved.parent()?.to_path_buf();
|
|
250
|
+
Some(PostQuery::new(command, config_dir))
|
|
251
|
+
}
|
|
252
|
+
|
|
231
253
|
/// Zero-config fallback. When no `.dirsql.toml` is found, dirsql indexes the
|
|
232
254
|
/// directory that would have held the config with a single default `files`
|
|
233
255
|
/// table — one row per file, columns drawn entirely from filesystem facts —
|
|
@@ -63,6 +63,31 @@ impl PreQuery {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/// A server-wide `post-query` command hook, carrying the command template plus
|
|
67
|
+
/// the directory it runs in (the config file's parent). When set on a
|
|
68
|
+
/// [`ServerConfig`], the server hands each successful `POST /query` result set
|
|
69
|
+
/// (the rows serialized as a JSON array) to the command as `{args}` and on
|
|
70
|
+
/// stdin, and returns the JSON body the command prints instead of the rows
|
|
71
|
+
/// as-is. See [`crate::command`] for the execution contract.
|
|
72
|
+
#[derive(Debug, Clone)]
|
|
73
|
+
pub struct PostQuery {
|
|
74
|
+
/// The command template (argv-split, no shell). Receives the serialized
|
|
75
|
+
/// result rows as the `{args}` placeholder (and on stdin).
|
|
76
|
+
pub command: String,
|
|
77
|
+
/// The command's working directory — the config file's parent.
|
|
78
|
+
pub config_dir: PathBuf,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
impl PostQuery {
|
|
82
|
+
/// Build a [`PostQuery`] from a command template and its working directory.
|
|
83
|
+
pub fn new(command: impl Into<String>, config_dir: impl Into<PathBuf>) -> Self {
|
|
84
|
+
Self {
|
|
85
|
+
command: command.into(),
|
|
86
|
+
config_dir: config_dir.into(),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
66
91
|
/// Configure how the server binds. Defaults to `localhost:7117` with a
|
|
67
92
|
/// 30-second per-query timeout and no `pre-query` hook.
|
|
68
93
|
#[derive(Debug, Clone)]
|
|
@@ -73,6 +98,9 @@ pub struct ServerConfig {
|
|
|
73
98
|
/// Optional server-wide `pre-query` command. When `None` (the default),
|
|
74
99
|
/// `POST /query` parses its body as `{"sql": …}`.
|
|
75
100
|
pub pre_query: Option<PreQuery>,
|
|
101
|
+
/// Optional server-wide `post-query` command. When `None` (the default),
|
|
102
|
+
/// `POST /query` returns the result rows as-is.
|
|
103
|
+
pub post_query: Option<PostQuery>,
|
|
76
104
|
}
|
|
77
105
|
|
|
78
106
|
impl ServerConfig {
|
|
@@ -84,6 +112,7 @@ impl ServerConfig {
|
|
|
84
112
|
port: 0,
|
|
85
113
|
query_timeout: Duration::from_secs(30),
|
|
86
114
|
pre_query: None,
|
|
115
|
+
post_query: None,
|
|
87
116
|
}
|
|
88
117
|
}
|
|
89
118
|
|
|
@@ -94,6 +123,7 @@ impl ServerConfig {
|
|
|
94
123
|
port,
|
|
95
124
|
query_timeout: Duration::from_secs(30),
|
|
96
125
|
pre_query: None,
|
|
126
|
+
post_query: None,
|
|
97
127
|
}
|
|
98
128
|
}
|
|
99
129
|
|
|
@@ -111,6 +141,14 @@ impl ServerConfig {
|
|
|
111
141
|
self.pre_query = Some(pre_query);
|
|
112
142
|
self
|
|
113
143
|
}
|
|
144
|
+
|
|
145
|
+
/// Attach a server-wide [`PostQuery`] hook. With it set, `POST /query`
|
|
146
|
+
/// hands each successful result set to the command and returns the JSON
|
|
147
|
+
/// body it prints instead of returning the rows as-is.
|
|
148
|
+
pub fn with_post_query(mut self, post_query: PostQuery) -> Self {
|
|
149
|
+
self.post_query = Some(post_query);
|
|
150
|
+
self
|
|
151
|
+
}
|
|
114
152
|
}
|
|
115
153
|
|
|
116
154
|
impl Default for ServerConfig {
|
|
@@ -214,6 +252,7 @@ mod tests {
|
|
|
214
252
|
assert_eq!(cfg.port, 7117);
|
|
215
253
|
assert_eq!(cfg.query_timeout, Duration::from_secs(30));
|
|
216
254
|
assert!(cfg.pre_query.is_none());
|
|
255
|
+
assert!(cfg.post_query.is_none());
|
|
217
256
|
}
|
|
218
257
|
|
|
219
258
|
#[test]
|
|
@@ -232,4 +271,22 @@ mod tests {
|
|
|
232
271
|
assert_eq!(pq.command, "cmd {args}");
|
|
233
272
|
assert_eq!(pq.config_dir, PathBuf::from("/proj"));
|
|
234
273
|
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn post_query_constructor_carries_command_and_dir() {
|
|
277
|
+
// `PostQuery::new` is pure data plumbing: the command template and the
|
|
278
|
+
// working directory it will run in.
|
|
279
|
+
let pq = PostQuery::new("jq '{results: .}'", "/proj");
|
|
280
|
+
assert_eq!(pq.command, "jq '{results: .}'");
|
|
281
|
+
assert_eq!(pq.config_dir, PathBuf::from("/proj"));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[test]
|
|
285
|
+
fn with_post_query_sets_the_hook() {
|
|
286
|
+
let cfg =
|
|
287
|
+
ServerConfig::ephemeral().with_post_query(PostQuery::new("reshape {args}", "/proj"));
|
|
288
|
+
let pq = cfg.post_query.expect("hook must be set");
|
|
289
|
+
assert_eq!(pq.command, "reshape {args}");
|
|
290
|
+
assert_eq!(pq.config_dir, PathBuf::from("/proj"));
|
|
291
|
+
}
|
|
235
292
|
}
|
|
@@ -17,7 +17,7 @@ use tokio::sync::{broadcast, watch};
|
|
|
17
17
|
use tokio_stream::wrappers::BroadcastStream;
|
|
18
18
|
|
|
19
19
|
use super::serialize::rows_to_json;
|
|
20
|
-
use super::{AppState, PreQuery};
|
|
20
|
+
use super::{AppState, PostQuery, PreQuery};
|
|
21
21
|
use crate::command::{Placeholder, run_command};
|
|
22
22
|
use crate::{DirSQL, DirSqlError};
|
|
23
23
|
|
|
@@ -26,6 +26,16 @@ use crate::{DirSQL, DirSqlError};
|
|
|
26
26
|
/// `on-file`'s `ON_FILE_TIMEOUT`).
|
|
27
27
|
const PRE_QUERY_TIMEOUT: Duration = Duration::from_secs(30);
|
|
28
28
|
|
|
29
|
+
/// Fixed timeout for a server-wide `post-query` command (mirrors
|
|
30
|
+
/// `PRE_QUERY_TIMEOUT`).
|
|
31
|
+
const POST_QUERY_TIMEOUT: Duration = Duration::from_secs(30);
|
|
32
|
+
|
|
33
|
+
/// Cap on the serialized result payload passed as the `{args}` argv token.
|
|
34
|
+
/// Beyond this, `{args}` is emptied and the operator is directed to stdin
|
|
35
|
+
/// (which always carries the full payload) — comfortably under Linux's 128 KiB
|
|
36
|
+
/// single-arg `MAX_ARG_STRLEN`.
|
|
37
|
+
const POST_QUERY_ARGS_MAX: usize = 96 * 1024;
|
|
38
|
+
|
|
29
39
|
pub(super) struct AppContext {
|
|
30
40
|
pub state: AppState,
|
|
31
41
|
pub events: broadcast::Sender<String>,
|
|
@@ -35,6 +45,10 @@ pub(super) struct AppContext {
|
|
|
35
45
|
/// rewrites the request body through the command; when `None`, the body
|
|
36
46
|
/// is parsed as `{"sql": …}`.
|
|
37
47
|
pub pre_query: Option<PreQuery>,
|
|
48
|
+
/// Optional server-wide `post-query` hook. When `Some`, a successful
|
|
49
|
+
/// `POST /query` result set is reshaped by the command before responding;
|
|
50
|
+
/// when `None`, the rows are returned as-is.
|
|
51
|
+
pub post_query: Option<PostQuery>,
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
pub(super) type SharedCtx = Arc<AppContext>;
|
|
@@ -81,7 +95,16 @@ async fn handle_query(State(ctx): State<SharedCtx>, body: String) -> Response {
|
|
|
81
95
|
tokio::time::timeout(timeout, tokio::task::spawn_blocking(move || db.query(&sql))).await;
|
|
82
96
|
|
|
83
97
|
match join {
|
|
84
|
-
Ok(Ok(Ok(rows))) =>
|
|
98
|
+
Ok(Ok(Ok(rows))) => {
|
|
99
|
+
let rows_json = rows_to_json(&rows);
|
|
100
|
+
match &ctx.post_query {
|
|
101
|
+
Some(pq) => match run_post_query(pq, rows_json).await {
|
|
102
|
+
Ok(value) => Json(value).into_response(),
|
|
103
|
+
Err(resp) => resp,
|
|
104
|
+
},
|
|
105
|
+
None => Json(rows_json).into_response(),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
85
108
|
Ok(Ok(Err(err))) => {
|
|
86
109
|
let status = classify_query_error(&err);
|
|
87
110
|
error_response(status, err.to_string())
|
|
@@ -154,6 +177,66 @@ async fn run_pre_query(pq: &PreQuery, raw_body: String) -> Result<String, Respon
|
|
|
154
177
|
.map_err(|err| error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))
|
|
155
178
|
}
|
|
156
179
|
|
|
180
|
+
/// Run the server-wide `post-query` hook over a successful result set and
|
|
181
|
+
/// return the JSON body it prints. The rows are serialized to a JSON array and
|
|
182
|
+
/// delivered two ways: always on the child's stdin (unbounded, injection-safe),
|
|
183
|
+
/// and as the `{args}` placeholder when the payload is within
|
|
184
|
+
/// [`POST_QUERY_ARGS_MAX`] (beyond that `{args}` is emptied and a warning names
|
|
185
|
+
/// the size, directing the operator to stdin — never silent truncation). The
|
|
186
|
+
/// command's last non-empty stdout line is parsed as JSON and returned as the
|
|
187
|
+
/// `200` body; anything that isn't valid JSON, or any failure (non-zero exit,
|
|
188
|
+
/// timeout, spawn error), maps to `500`.
|
|
189
|
+
///
|
|
190
|
+
/// `Response` is large (see [`parse_sql_body`]); returned by value for the same
|
|
191
|
+
/// reason.
|
|
192
|
+
#[allow(clippy::result_large_err)]
|
|
193
|
+
async fn run_post_query(
|
|
194
|
+
pq: &PostQuery,
|
|
195
|
+
rows: Vec<serde_json::Value>,
|
|
196
|
+
) -> Result<serde_json::Value, Response> {
|
|
197
|
+
let payload = serde_json::to_string(&rows)
|
|
198
|
+
.map_err(|err| error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
|
199
|
+
let command = pq.command.clone();
|
|
200
|
+
let config_dir = pq.config_dir.clone();
|
|
201
|
+
// `run_command` is blocking — it spawns a child and joins drain threads —
|
|
202
|
+
// so run it off the async runtime. It enforces `POST_QUERY_TIMEOUT`
|
|
203
|
+
// internally, so no outer `tokio::time::timeout` is needed.
|
|
204
|
+
let outcome = tokio::task::spawn_blocking(move || {
|
|
205
|
+
let args_value = if payload.len() <= POST_QUERY_ARGS_MAX {
|
|
206
|
+
payload.clone()
|
|
207
|
+
} else {
|
|
208
|
+
eprintln!(
|
|
209
|
+
"dirsql: post-query result payload is {} bytes, exceeding the \
|
|
210
|
+
{POST_QUERY_ARGS_MAX}-byte argv threshold; `{{args}}` is emptied — \
|
|
211
|
+
read the rows from stdin instead",
|
|
212
|
+
payload.len()
|
|
213
|
+
);
|
|
214
|
+
String::new()
|
|
215
|
+
};
|
|
216
|
+
run_command(
|
|
217
|
+
&command,
|
|
218
|
+
&[Placeholder::new("args", &args_value)],
|
|
219
|
+
&config_dir,
|
|
220
|
+
POST_QUERY_TIMEOUT,
|
|
221
|
+
Some(payload.as_bytes()),
|
|
222
|
+
)
|
|
223
|
+
})
|
|
224
|
+
.await
|
|
225
|
+
.map_err(|join_err| error_response(StatusCode::INTERNAL_SERVER_ERROR, join_err.to_string()))?;
|
|
226
|
+
|
|
227
|
+
let out = outcome
|
|
228
|
+
.map_err(|err| error_response(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
|
229
|
+
|
|
230
|
+
// The command's payload (last non-empty stdout line) is the JSON response
|
|
231
|
+
// body; reject anything that doesn't parse as JSON.
|
|
232
|
+
serde_json::from_str(&out.payload).map_err(|err| {
|
|
233
|
+
error_response(
|
|
234
|
+
StatusCode::INTERNAL_SERVER_ERROR,
|
|
235
|
+
format!("post-query did not return valid JSON: {err}"),
|
|
236
|
+
)
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
157
240
|
async fn handle_events(State(ctx): State<SharedCtx>) -> Response {
|
|
158
241
|
if let Err(resp) = require_ready(&ctx.state) {
|
|
159
242
|
return resp;
|
|
@@ -51,6 +51,13 @@ pub struct Config {
|
|
|
51
51
|
/// execution contract. Only the CLI server consults this; the SDK ignores
|
|
52
52
|
/// it.
|
|
53
53
|
pub pre_query: Option<String>,
|
|
54
|
+
/// Optional server-wide `post-query` command (`[dirsql].post-query`). When
|
|
55
|
+
/// set, the HTTP server hands each successful `POST /query` result set (the
|
|
56
|
+
/// rows serialized as a JSON array) to this command as `{args}` and on
|
|
57
|
+
/// stdin, and returns the JSON body the command prints, instead of returning
|
|
58
|
+
/// the rows as-is. See `dirsql::command` for the execution contract. Only
|
|
59
|
+
/// the CLI server consults this; the SDK ignores it.
|
|
60
|
+
pub post_query: Option<String>,
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
/// A SQLite extension to load at startup.
|
|
@@ -109,6 +116,8 @@ struct RawDirsql {
|
|
|
109
116
|
extension: Option<Vec<RawExtension>>,
|
|
110
117
|
#[serde(rename = "pre-query")]
|
|
111
118
|
pre_query: Option<String>,
|
|
119
|
+
#[serde(rename = "post-query")]
|
|
120
|
+
post_query: Option<String>,
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
#[derive(Deserialize)]
|
|
@@ -136,17 +145,19 @@ pub fn load_config(path: &Path) -> Result<Config> {
|
|
|
136
145
|
pub fn load_config_str(content: &str) -> Result<Config> {
|
|
137
146
|
let raw: RawConfig = toml::from_str(content)?;
|
|
138
147
|
|
|
139
|
-
let (root, ignore, persist, persist_path, raw_extensions, raw_pre_query) =
|
|
140
|
-
|
|
141
|
-
d
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
let (root, ignore, persist, persist_path, raw_extensions, raw_pre_query, raw_post_query) =
|
|
149
|
+
match raw.dirsql {
|
|
150
|
+
Some(d) => (
|
|
151
|
+
d.root,
|
|
152
|
+
d.ignore.unwrap_or_default(),
|
|
153
|
+
d.persist.unwrap_or(false),
|
|
154
|
+
d.persist_path,
|
|
155
|
+
d.extension.unwrap_or_default(),
|
|
156
|
+
d.pre_query,
|
|
157
|
+
d.post_query,
|
|
158
|
+
),
|
|
159
|
+
None => (None, Vec::new(), false, None, Vec::new(), None, None),
|
|
160
|
+
};
|
|
150
161
|
|
|
151
162
|
// A present-but-empty `pre-query = ""` is as unusable as a missing key:
|
|
152
163
|
// reject it at parse time rather than spawning an empty command later
|
|
@@ -158,6 +169,16 @@ pub fn load_config_str(content: &str) -> Result<Config> {
|
|
|
158
169
|
other => other,
|
|
159
170
|
};
|
|
160
171
|
|
|
172
|
+
// A present-but-empty `post-query = ""` is as unusable as a missing key:
|
|
173
|
+
// reject it at parse time rather than spawning an empty command later
|
|
174
|
+
// (mirrors the `pre-query` handling above).
|
|
175
|
+
let post_query = match raw_post_query {
|
|
176
|
+
Some(cmd) if cmd.trim().is_empty() => {
|
|
177
|
+
return Err(ConfigError::EmptyField("post-query"));
|
|
178
|
+
}
|
|
179
|
+
other => other,
|
|
180
|
+
};
|
|
181
|
+
|
|
161
182
|
let mut extensions = Vec::with_capacity(raw_extensions.len());
|
|
162
183
|
for raw_ext in raw_extensions {
|
|
163
184
|
// An empty `path = ""` is as unusable as a missing key: reject it at
|
|
@@ -204,6 +225,7 @@ pub fn load_config_str(content: &str) -> Result<Config> {
|
|
|
204
225
|
persist_path,
|
|
205
226
|
extensions,
|
|
206
227
|
pre_query,
|
|
228
|
+
post_query,
|
|
207
229
|
})
|
|
208
230
|
}
|
|
209
231
|
|
|
@@ -568,6 +590,44 @@ pre-query = " "
|
|
|
568
590
|
);
|
|
569
591
|
}
|
|
570
592
|
|
|
593
|
+
#[test]
|
|
594
|
+
fn post_query_parses_when_present() {
|
|
595
|
+
let toml = r#"
|
|
596
|
+
[dirsql]
|
|
597
|
+
post-query = "jq '{results: .}'"
|
|
598
|
+
|
|
599
|
+
[[table]]
|
|
600
|
+
ddl = "CREATE TABLE t (_path TEXT)"
|
|
601
|
+
glob = "*.json"
|
|
602
|
+
"#;
|
|
603
|
+
let config = load_config_str(toml).unwrap();
|
|
604
|
+
assert_eq!(config.post_query.as_deref(), Some("jq '{results: .}'"));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
#[test]
|
|
608
|
+
fn post_query_absent_is_none() {
|
|
609
|
+
let toml = r#"
|
|
610
|
+
[[table]]
|
|
611
|
+
ddl = "CREATE TABLE t (_path TEXT)"
|
|
612
|
+
glob = "*.json"
|
|
613
|
+
"#;
|
|
614
|
+
let config = load_config_str(toml).unwrap();
|
|
615
|
+
assert!(config.post_query.is_none());
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
#[test]
|
|
619
|
+
fn post_query_empty_errors() {
|
|
620
|
+
let toml = r#"
|
|
621
|
+
[dirsql]
|
|
622
|
+
post-query = " "
|
|
623
|
+
"#;
|
|
624
|
+
let err = load_config_str(toml).unwrap_err();
|
|
625
|
+
assert!(
|
|
626
|
+
matches!(err, ConfigError::EmptyField("post-query")),
|
|
627
|
+
"got: {err:?}"
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
571
631
|
#[test]
|
|
572
632
|
fn extension_empty_path_errors() {
|
|
573
633
|
// An empty `path = ""` is as unusable as a missing key — it must be
|
|
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
|
|
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
|
|
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
|
{dirsql-0.3.48 → dirsql-0.3.50}/packages/python/docs/tests/integration/language-flag.spec.ts
RENAMED
|
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
|
|
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
|