dirsql 0.3.94__tar.gz → 0.3.96__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.94 → dirsql-0.3.96}/Cargo.lock +1 -1
- {dirsql-0.3.94 → dirsql-0.3.96}/PKG-INFO +1 -1
- {dirsql-0.3.94/packages/python → dirsql-0.3.96}/docs/howto/define-tables.md +4 -3
- {dirsql-0.3.94/packages/rust → dirsql-0.3.96}/docs/reference/config.md +1 -1
- {dirsql-0.3.94/packages/rust → dirsql-0.3.96}/docs/reference/sdk.md +3 -1
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.94/packages/rust → dirsql-0.3.96/packages/python}/docs/howto/define-tables.md +4 -3
- {dirsql-0.3.94 → dirsql-0.3.96/packages/python}/docs/reference/config.md +1 -1
- {dirsql-0.3.94 → dirsql-0.3.96/packages/python}/docs/reference/sdk.md +3 -1
- dirsql-0.3.96/packages/python/e2e-attestations/claude-github-issue-580-d0jd9u.json +7 -0
- dirsql-0.3.96/packages/python/e2e-attestations/claude-open-issues-review-le8hm5-red-546.json +7 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/benches/matcher_bench.rs +2 -2
- {dirsql-0.3.94 → dirsql-0.3.96/packages/rust}/docs/howto/define-tables.md +4 -3
- {dirsql-0.3.94/packages/python → dirsql-0.3.96/packages/rust}/docs/reference/config.md +1 -1
- {dirsql-0.3.94/packages/python → dirsql-0.3.96/packages/rust}/docs/reference/sdk.md +3 -1
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/bin/dirsql.rs +4 -4
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/cli/execute.rs +38 -21
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/cli/mod.rs +47 -31
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/cli/router.rs +10 -10
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/lib.rs +61 -43
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/matcher.rs +146 -96
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/persist.rs +84 -16
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/scanner.rs +4 -2
- {dirsql-0.3.94 → dirsql-0.3.96}/Cargo.toml +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/README.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/dirsql/__init__.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/dirsql/_async.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/dirsql/cli/resolve_config_extensions.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/dirsql/py.typed +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/dirsql/resolve_config_extensions.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/dirsql/resolve_extension.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/AGENTS.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/explanation.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/getting-started.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/howto/embed.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/howto/persist.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/index.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/migrations.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/package.json +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/reference/cli.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/reference/columns.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/reference/hooks.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/reference/http-api.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/README.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/conftest.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/explanation.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/howto/embed.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/howto/persist.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/reference/cli.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/reference/columns.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/reference/hooks.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/reference/http-api.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/e2e-attestations/claude-open-issues-review-le8hm5-red-553.json +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/tests/integration/binding/__init__.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/python/tests/integration/hermetic/__init__.py +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/README.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/explanation.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/howto/embed.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/howto/persist.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/reference/cli.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/reference/columns.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/reference/hooks.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/docs/reference/http-api.md +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/command.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/config.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/db.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/default_config.toml +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.94 → dirsql-0.3.96}/pyproject.toml +0 -0
|
@@ -42,9 +42,10 @@ zero-config default — only the tables you define are served.
|
|
|
42
42
|
|
|
43
43
|
## Multiple tables
|
|
44
44
|
|
|
45
|
-
Add one `[[table]]` entry per table. When a file matches several globs,
|
|
46
|
-
|
|
47
|
-
for that and the remaining keys
|
|
45
|
+
Add one `[[table]]` entry per table. When a file matches several globs, it
|
|
46
|
+
populates every matching table — each table is an independent view. See
|
|
47
|
+
[`[[table]]`](../reference/config.md#table) for that and the remaining keys
|
|
48
|
+
(`strict`, `on-file`).
|
|
48
49
|
|
|
49
50
|
## Going further
|
|
50
51
|
|
|
@@ -97,7 +97,7 @@ per-file command.
|
|
|
97
97
|
| Key | Required | Description |
|
|
98
98
|
|---|---|---|
|
|
99
99
|
| `ddl` | yes | A SQLite `CREATE TABLE` statement. The table name is parsed from it. Only columns declared here are populated; auto-injected facts not in the DDL are dropped. |
|
|
100
|
-
| `glob` | yes | Glob pattern matched against root-relative paths. May contain `{name}` [capture segments](./columns.md#glob-captures).
|
|
100
|
+
| `glob` | yes | Glob pattern matched against root-relative paths. May contain `{name}` [capture segments](./columns.md#glob-captures). Every table whose glob matches a file receives that file's rows — a file can populate multiple tables. |
|
|
101
101
|
| `strict` | no (default `false`) | When `true`, rows whose keys do not exactly match the declared columns are rejected with an error: extra keys error, and every declared column must be supplied (by the command/extract output, a glob capture, or a stat column). When `false`, extra keys are dropped and missing columns become `NULL`. |
|
|
102
102
|
| `on-file` | no | A command run once per matched file; its stdout (a JSON array of row objects) becomes the file's rows. Must be non-empty. See [Command hooks](./hooks.md#on-file). |
|
|
103
103
|
|
|
@@ -318,7 +318,9 @@ Maps files to table rows.
|
|
|
318
318
|
- `ddl` — A SQLite `CREATE TABLE` statement; the table name is parsed from
|
|
319
319
|
it. Table names must be unique across all tables.
|
|
320
320
|
- `glob` — Glob pattern matched against root-relative paths. May contain
|
|
321
|
-
`{name}` [captures](./columns.md#glob-captures).
|
|
321
|
+
`{name}` [captures](./columns.md#glob-captures). Every table whose glob
|
|
322
|
+
matches a file receives that file's rows — a file can populate multiple
|
|
323
|
+
tables.
|
|
322
324
|
- `extract` — Callback receiving the matched file's full path (the root
|
|
323
325
|
joined with the file's relative path — absolute when `root` is absolute)
|
|
324
326
|
and returning the rows that file contributes. `dirsql` never reads file
|
|
@@ -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.96"
|
|
8
8
|
# abi3 note: the `abi3-py311` pyo3 feature (below) builds ONE stable-ABI
|
|
9
9
|
# `cp311-abi3` wheel per platform that loads on every CPython >= 3.11
|
|
10
10
|
# (matching `pyproject.toml`'s `requires-python = ">=3.11"`), instead of
|
|
@@ -42,9 +42,10 @@ zero-config default — only the tables you define are served.
|
|
|
42
42
|
|
|
43
43
|
## Multiple tables
|
|
44
44
|
|
|
45
|
-
Add one `[[table]]` entry per table. When a file matches several globs,
|
|
46
|
-
|
|
47
|
-
for that and the remaining keys
|
|
45
|
+
Add one `[[table]]` entry per table. When a file matches several globs, it
|
|
46
|
+
populates every matching table — each table is an independent view. See
|
|
47
|
+
[`[[table]]`](../reference/config.md#table) for that and the remaining keys
|
|
48
|
+
(`strict`, `on-file`).
|
|
48
49
|
|
|
49
50
|
## Going further
|
|
50
51
|
|
|
@@ -97,7 +97,7 @@ per-file command.
|
|
|
97
97
|
| Key | Required | Description |
|
|
98
98
|
|---|---|---|
|
|
99
99
|
| `ddl` | yes | A SQLite `CREATE TABLE` statement. The table name is parsed from it. Only columns declared here are populated; auto-injected facts not in the DDL are dropped. |
|
|
100
|
-
| `glob` | yes | Glob pattern matched against root-relative paths. May contain `{name}` [capture segments](./columns.md#glob-captures).
|
|
100
|
+
| `glob` | yes | Glob pattern matched against root-relative paths. May contain `{name}` [capture segments](./columns.md#glob-captures). Every table whose glob matches a file receives that file's rows — a file can populate multiple tables. |
|
|
101
101
|
| `strict` | no (default `false`) | When `true`, rows whose keys do not exactly match the declared columns are rejected with an error: extra keys error, and every declared column must be supplied (by the command/extract output, a glob capture, or a stat column). When `false`, extra keys are dropped and missing columns become `NULL`. |
|
|
102
102
|
| `on-file` | no | A command run once per matched file; its stdout (a JSON array of row objects) becomes the file's rows. Must be non-empty. See [Command hooks](./hooks.md#on-file). |
|
|
103
103
|
|
|
@@ -318,7 +318,9 @@ Maps files to table rows.
|
|
|
318
318
|
- `ddl` — A SQLite `CREATE TABLE` statement; the table name is parsed from
|
|
319
319
|
it. Table names must be unique across all tables.
|
|
320
320
|
- `glob` — Glob pattern matched against root-relative paths. May contain
|
|
321
|
-
`{name}` [captures](./columns.md#glob-captures).
|
|
321
|
+
`{name}` [captures](./columns.md#glob-captures). Every table whose glob
|
|
322
|
+
matches a file receives that file's rows — a file can populate multiple
|
|
323
|
+
tables.
|
|
322
324
|
- `extract` — Callback receiving the matched file's full path (the root
|
|
323
325
|
joined with the file's relative path — absolute when `root` is absolute)
|
|
324
326
|
and returning the rows that file contributes. `dirsql` never reads file
|
|
@@ -20,7 +20,7 @@ fn make_test_paths(count: usize) -> Vec<String> {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
fn bench_match_file(c: &mut Criterion) {
|
|
23
|
-
let mut group = c.benchmark_group("matcher/
|
|
23
|
+
let mut group = c.benchmark_group("matcher/match_all");
|
|
24
24
|
for pattern_count in [1, 10, 50] {
|
|
25
25
|
let matcher = make_matcher(pattern_count);
|
|
26
26
|
let paths = make_test_paths(100);
|
|
@@ -30,7 +30,7 @@ fn bench_match_file(c: &mut Criterion) {
|
|
|
30
30
|
|b, _| {
|
|
31
31
|
b.iter(|| {
|
|
32
32
|
for p in &paths {
|
|
33
|
-
matcher.
|
|
33
|
+
matcher.match_all(Path::new(p));
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
36
|
},
|
|
@@ -42,9 +42,10 @@ zero-config default — only the tables you define are served.
|
|
|
42
42
|
|
|
43
43
|
## Multiple tables
|
|
44
44
|
|
|
45
|
-
Add one `[[table]]` entry per table. When a file matches several globs,
|
|
46
|
-
|
|
47
|
-
for that and the remaining keys
|
|
45
|
+
Add one `[[table]]` entry per table. When a file matches several globs, it
|
|
46
|
+
populates every matching table — each table is an independent view. See
|
|
47
|
+
[`[[table]]`](../reference/config.md#table) for that and the remaining keys
|
|
48
|
+
(`strict`, `on-file`).
|
|
48
49
|
|
|
49
50
|
## Going further
|
|
50
51
|
|
|
@@ -97,7 +97,7 @@ per-file command.
|
|
|
97
97
|
| Key | Required | Description |
|
|
98
98
|
|---|---|---|
|
|
99
99
|
| `ddl` | yes | A SQLite `CREATE TABLE` statement. The table name is parsed from it. Only columns declared here are populated; auto-injected facts not in the DDL are dropped. |
|
|
100
|
-
| `glob` | yes | Glob pattern matched against root-relative paths. May contain `{name}` [capture segments](./columns.md#glob-captures).
|
|
100
|
+
| `glob` | yes | Glob pattern matched against root-relative paths. May contain `{name}` [capture segments](./columns.md#glob-captures). Every table whose glob matches a file receives that file's rows — a file can populate multiple tables. |
|
|
101
101
|
| `strict` | no (default `false`) | When `true`, rows whose keys do not exactly match the declared columns are rejected with an error: extra keys error, and every declared column must be supplied (by the command/extract output, a glob capture, or a stat column). When `false`, extra keys are dropped and missing columns become `NULL`. |
|
|
102
102
|
| `on-file` | no | A command run once per matched file; its stdout (a JSON array of row objects) becomes the file's rows. Must be non-empty. See [Command hooks](./hooks.md#on-file). |
|
|
103
103
|
|
|
@@ -318,7 +318,9 @@ Maps files to table rows.
|
|
|
318
318
|
- `ddl` — A SQLite `CREATE TABLE` statement; the table name is parsed from
|
|
319
319
|
it. Table names must be unique across all tables.
|
|
320
320
|
- `glob` — Glob pattern matched against root-relative paths. May contain
|
|
321
|
-
`{name}` [captures](./columns.md#glob-captures).
|
|
321
|
+
`{name}` [captures](./columns.md#glob-captures). Every table whose glob
|
|
322
|
+
matches a file receives that file's rows — a file can populate multiple
|
|
323
|
+
tables.
|
|
322
324
|
- `extract` — Callback receiving the matched file's full path (the root
|
|
323
325
|
joined with the file's relative path — absolute when `root` is absolute)
|
|
324
326
|
and returning the rows that file contributes. `dirsql` never reads file
|
|
@@ -138,8 +138,8 @@ async fn main() -> ExitCode {
|
|
|
138
138
|
/// stderr with a non-zero exit.
|
|
139
139
|
async fn run_query(cli: &Cli, args: QueryArgs) -> ExitCode {
|
|
140
140
|
let state = load_state(cli);
|
|
141
|
-
let pre_query = load_pre_query(cli);
|
|
142
|
-
let post_query = load_post_query(cli);
|
|
141
|
+
let pre_query: Vec<PreQuery> = load_pre_query(cli).into_iter().collect();
|
|
142
|
+
let post_query: Vec<PostQuery> = load_post_query(cli).into_iter().collect();
|
|
143
143
|
// Same default the server binds with; the pipeline enforces it.
|
|
144
144
|
let timeout = ServerConfig::default().query_timeout;
|
|
145
145
|
|
|
@@ -147,8 +147,8 @@ async fn run_query(cli: &Cli, args: QueryArgs) -> ExitCode {
|
|
|
147
147
|
&state,
|
|
148
148
|
query_body(&args.sql),
|
|
149
149
|
timeout,
|
|
150
|
-
pre_query
|
|
151
|
-
post_query
|
|
150
|
+
&pre_query,
|
|
151
|
+
&post_query,
|
|
152
152
|
)
|
|
153
153
|
.await
|
|
154
154
|
{
|
|
@@ -68,14 +68,22 @@ pub async fn execute_query(
|
|
|
68
68
|
state: &AppState,
|
|
69
69
|
raw_body: String,
|
|
70
70
|
timeout: Duration,
|
|
71
|
-
pre_query:
|
|
72
|
-
post_query:
|
|
71
|
+
pre_query: &[PreQuery],
|
|
72
|
+
post_query: &[PostQuery],
|
|
73
73
|
) -> Result<Value, QueryFailure> {
|
|
74
|
-
// Resolve the SQL to run.
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
// Resolve the SQL to run. A `pre-query` chain pipes the raw body FIFO
|
|
75
|
+
// through each stage (body → stage₁ → … → SQL), each stage receiving the
|
|
76
|
+
// previous stage's output as its `{args}`; with no stage the body is parsed
|
|
77
|
+
// as `{"sql": …}`. A failing stage fails the request (its `?` short-circuits
|
|
78
|
+
// the chain).
|
|
79
|
+
let sql = if pre_query.is_empty() {
|
|
80
|
+
parse_sql_body(&raw_body)?
|
|
81
|
+
} else {
|
|
82
|
+
let mut payload = raw_body;
|
|
83
|
+
for pq in pre_query {
|
|
84
|
+
payload = run_pre_query(pq, payload).await?;
|
|
85
|
+
}
|
|
86
|
+
payload
|
|
79
87
|
};
|
|
80
88
|
|
|
81
89
|
let db = require_ready(state)?;
|
|
@@ -86,9 +94,17 @@ pub async fn execute_query(
|
|
|
86
94
|
match join {
|
|
87
95
|
Ok(Ok(Ok(rows))) => {
|
|
88
96
|
let rows_json = rows_to_json(&rows);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
// A `post-query` chain pipes the rows FIFO through each stage
|
|
98
|
+
// (rows → stage₁ → … → response), each stage receiving the previous
|
|
99
|
+
// stage's output; with no stage the rows return as-is.
|
|
100
|
+
if post_query.is_empty() {
|
|
101
|
+
Ok(Value::Array(rows_json))
|
|
102
|
+
} else {
|
|
103
|
+
let mut payload = Value::Array(rows_json);
|
|
104
|
+
for pq in post_query {
|
|
105
|
+
payload = run_post_query(pq, payload).await?;
|
|
106
|
+
}
|
|
107
|
+
Ok(payload)
|
|
92
108
|
}
|
|
93
109
|
}
|
|
94
110
|
Ok(Ok(Err(err))) => Err(classify_query_error(err)),
|
|
@@ -162,18 +178,19 @@ async fn run_pre_query(pq: &PreQuery, raw_body: String) -> Result<String, QueryF
|
|
|
162
178
|
.map_err(|err| QueryFailure::Internal(err.to_string()))
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
/// Run
|
|
166
|
-
///
|
|
167
|
-
///
|
|
168
|
-
///
|
|
169
|
-
///
|
|
170
|
-
///
|
|
171
|
-
///
|
|
172
|
-
///
|
|
173
|
-
/// spawn error), maps to
|
|
174
|
-
|
|
181
|
+
/// Run one `post-query` stage over its `input` JSON and return the JSON body it
|
|
182
|
+
/// prints. `input` is the serialized result rows for the first stage, or the
|
|
183
|
+
/// previous stage's output thereafter. It is serialized and delivered two ways:
|
|
184
|
+
/// always on the child's stdin (unbounded, injection-safe), and as the `{args}`
|
|
185
|
+
/// placeholder when the payload is within [`POST_QUERY_ARGS_MAX`] (beyond that
|
|
186
|
+
/// `{args}` is emptied and a warning names the size, directing the operator to
|
|
187
|
+
/// stdin — never silent truncation). The command's last non-empty stdout line
|
|
188
|
+
/// is parsed as JSON and returned as the result; anything that isn't valid
|
|
189
|
+
/// JSON, or any failure (non-zero exit, timeout, spawn error), maps to
|
|
190
|
+
/// `Internal`.
|
|
191
|
+
async fn run_post_query(pq: &PostQuery, input: Value) -> Result<Value, QueryFailure> {
|
|
175
192
|
let payload =
|
|
176
|
-
serde_json::to_string(&
|
|
193
|
+
serde_json::to_string(&input).map_err(|err| QueryFailure::Internal(err.to_string()))?;
|
|
177
194
|
let command = pq.command.clone();
|
|
178
195
|
let config_dir = pq.config_dir.clone();
|
|
179
196
|
let timeout = pq.timeout;
|
|
@@ -127,12 +127,17 @@ pub struct ServerConfig {
|
|
|
127
127
|
pub host: String,
|
|
128
128
|
pub port: u16,
|
|
129
129
|
pub query_timeout: Duration,
|
|
130
|
-
///
|
|
131
|
-
/// `POST /query` parses its body as `{"sql": …}
|
|
132
|
-
|
|
133
|
-
///
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
/// Ordered `pre-query` command chain. Empty (the default) means
|
|
131
|
+
/// `POST /query` parses its body as `{"sql": …}`; otherwise the raw body is
|
|
132
|
+
/// piped through each stage in registration order (body → stage₁ → … → SQL),
|
|
133
|
+
/// each stage receiving the previous stage's output as its `{args}`.
|
|
134
|
+
pub pre_query: Vec<PreQuery>,
|
|
135
|
+
/// Ordered `post-query` command chain. Empty (the default) means
|
|
136
|
+
/// `POST /query` returns the result rows as-is; otherwise the rows are piped
|
|
137
|
+
/// through each stage in registration order (rows → stage₁ → … → response),
|
|
138
|
+
/// each stage receiving the previous stage's output as its `{args}` (and on
|
|
139
|
+
/// stdin).
|
|
140
|
+
pub post_query: Vec<PostQuery>,
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
impl ServerConfig {
|
|
@@ -143,8 +148,8 @@ impl ServerConfig {
|
|
|
143
148
|
host: "localhost".into(),
|
|
144
149
|
port: 0,
|
|
145
150
|
query_timeout: Duration::from_secs(30),
|
|
146
|
-
pre_query:
|
|
147
|
-
post_query:
|
|
151
|
+
pre_query: Vec::new(),
|
|
152
|
+
post_query: Vec::new(),
|
|
148
153
|
}
|
|
149
154
|
}
|
|
150
155
|
|
|
@@ -154,8 +159,8 @@ impl ServerConfig {
|
|
|
154
159
|
host: host.into(),
|
|
155
160
|
port,
|
|
156
161
|
query_timeout: Duration::from_secs(30),
|
|
157
|
-
pre_query:
|
|
158
|
-
post_query:
|
|
162
|
+
pre_query: Vec::new(),
|
|
163
|
+
post_query: Vec::new(),
|
|
159
164
|
}
|
|
160
165
|
}
|
|
161
166
|
|
|
@@ -166,19 +171,23 @@ impl ServerConfig {
|
|
|
166
171
|
self
|
|
167
172
|
}
|
|
168
173
|
|
|
169
|
-
///
|
|
170
|
-
///
|
|
171
|
-
///
|
|
174
|
+
/// Append a [`PreQuery`] stage to the chain. Stages run in registration
|
|
175
|
+
/// order (body → stage₁ → … → SQL); the first stage receives the raw
|
|
176
|
+
/// request body and each subsequent stage receives the previous stage's
|
|
177
|
+
/// output, the last stage's output being the SQL to run. With no stage the
|
|
178
|
+
/// body is parsed as `{"sql": …}`.
|
|
172
179
|
pub fn with_pre_query(mut self, pre_query: PreQuery) -> Self {
|
|
173
|
-
self.pre_query
|
|
180
|
+
self.pre_query.push(pre_query);
|
|
174
181
|
self
|
|
175
182
|
}
|
|
176
183
|
|
|
177
|
-
///
|
|
178
|
-
///
|
|
179
|
-
///
|
|
184
|
+
/// Append a [`PostQuery`] stage to the chain. Stages run in registration
|
|
185
|
+
/// order (rows → stage₁ → … → response); the first stage receives the
|
|
186
|
+
/// serialized result rows and each subsequent stage receives the previous
|
|
187
|
+
/// stage's output, the last stage's output being the response body. With no
|
|
188
|
+
/// stage the rows are returned as-is.
|
|
180
189
|
pub fn with_post_query(mut self, post_query: PostQuery) -> Self {
|
|
181
|
-
self.post_query
|
|
190
|
+
self.post_query.push(post_query);
|
|
182
191
|
self
|
|
183
192
|
}
|
|
184
193
|
}
|
|
@@ -276,8 +285,8 @@ mod tests {
|
|
|
276
285
|
assert_eq!(cfg.host, "localhost");
|
|
277
286
|
assert_eq!(cfg.port, 7117);
|
|
278
287
|
assert_eq!(cfg.query_timeout, Duration::from_secs(30));
|
|
279
|
-
assert!(cfg.pre_query.
|
|
280
|
-
assert!(cfg.post_query.
|
|
288
|
+
assert!(cfg.pre_query.is_empty());
|
|
289
|
+
assert!(cfg.post_query.is_empty());
|
|
281
290
|
}
|
|
282
291
|
|
|
283
292
|
#[test]
|
|
@@ -295,11 +304,15 @@ mod tests {
|
|
|
295
304
|
}
|
|
296
305
|
|
|
297
306
|
#[test]
|
|
298
|
-
fn
|
|
299
|
-
let cfg = ServerConfig::ephemeral()
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
assert_eq!(
|
|
307
|
+
fn with_pre_query_appends_stages_in_order() {
|
|
308
|
+
let cfg = ServerConfig::ephemeral()
|
|
309
|
+
.with_pre_query(PreQuery::new("first {args}", "/a"))
|
|
310
|
+
.with_pre_query(PreQuery::new("second {args}", "/b"));
|
|
311
|
+
assert_eq!(cfg.pre_query.len(), 2);
|
|
312
|
+
assert_eq!(cfg.pre_query[0].command, "first {args}");
|
|
313
|
+
assert_eq!(cfg.pre_query[0].config_dir, PathBuf::from("/a"));
|
|
314
|
+
assert_eq!(cfg.pre_query[1].command, "second {args}");
|
|
315
|
+
assert_eq!(cfg.pre_query[1].config_dir, PathBuf::from("/b"));
|
|
303
316
|
}
|
|
304
317
|
|
|
305
318
|
#[test]
|
|
@@ -317,12 +330,15 @@ mod tests {
|
|
|
317
330
|
}
|
|
318
331
|
|
|
319
332
|
#[test]
|
|
320
|
-
fn
|
|
321
|
-
let cfg =
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
assert_eq!(
|
|
325
|
-
assert_eq!(
|
|
333
|
+
fn with_post_query_appends_stages_in_order() {
|
|
334
|
+
let cfg = ServerConfig::ephemeral()
|
|
335
|
+
.with_post_query(PostQuery::new("first {args}", "/a"))
|
|
336
|
+
.with_post_query(PostQuery::new("second {args}", "/b"));
|
|
337
|
+
assert_eq!(cfg.post_query.len(), 2);
|
|
338
|
+
assert_eq!(cfg.post_query[0].command, "first {args}");
|
|
339
|
+
assert_eq!(cfg.post_query[0].config_dir, PathBuf::from("/a"));
|
|
340
|
+
assert_eq!(cfg.post_query[1].command, "second {args}");
|
|
341
|
+
assert_eq!(cfg.post_query[1].config_dir, PathBuf::from("/b"));
|
|
326
342
|
}
|
|
327
343
|
|
|
328
344
|
#[test]
|
|
@@ -28,14 +28,14 @@ pub(super) struct AppContext {
|
|
|
28
28
|
pub events: broadcast::Sender<String>,
|
|
29
29
|
pub cancel: watch::Receiver<bool>,
|
|
30
30
|
pub query_timeout: Duration,
|
|
31
|
-
///
|
|
32
|
-
///
|
|
33
|
-
///
|
|
34
|
-
pub pre_query:
|
|
35
|
-
///
|
|
36
|
-
///
|
|
37
|
-
///
|
|
38
|
-
pub post_query:
|
|
31
|
+
/// Ordered `pre-query` command chain. Empty means `POST /query` parses the
|
|
32
|
+
/// body as `{"sql": …}`; otherwise the raw body is piped through each stage
|
|
33
|
+
/// in order (body → stage₁ → … → SQL).
|
|
34
|
+
pub pre_query: Vec<PreQuery>,
|
|
35
|
+
/// Ordered `post-query` command chain. Empty means the rows are returned
|
|
36
|
+
/// as-is; otherwise a successful result set is piped through each stage in
|
|
37
|
+
/// order (rows → stage₁ → … → response).
|
|
38
|
+
pub post_query: Vec<PostQuery>,
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
pub(super) type SharedCtx = Arc<AppContext>;
|
|
@@ -58,8 +58,8 @@ async fn handle_query(State(ctx): State<SharedCtx>, body: String) -> Response {
|
|
|
58
58
|
&ctx.state,
|
|
59
59
|
body,
|
|
60
60
|
ctx.query_timeout,
|
|
61
|
-
ctx.pre_query
|
|
62
|
-
ctx.post_query
|
|
61
|
+
&ctx.pre_query,
|
|
62
|
+
&ctx.post_query,
|
|
63
63
|
)
|
|
64
64
|
.await
|
|
65
65
|
{
|