dirsql 0.3.95__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.95 → dirsql-0.3.96}/Cargo.lock +1 -1
- {dirsql-0.3.95 → dirsql-0.3.96}/PKG-INFO +1 -1
- {dirsql-0.3.95/packages/python → dirsql-0.3.96}/docs/howto/define-tables.md +4 -3
- {dirsql-0.3.95/packages/rust → dirsql-0.3.96}/docs/reference/config.md +1 -1
- {dirsql-0.3.95/packages/rust → dirsql-0.3.96}/docs/reference/sdk.md +3 -1
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.95/packages/rust → dirsql-0.3.96/packages/python}/docs/howto/define-tables.md +4 -3
- {dirsql-0.3.95 → dirsql-0.3.96/packages/python}/docs/reference/config.md +1 -1
- {dirsql-0.3.95 → 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.95 → dirsql-0.3.96}/packages/rust/benches/matcher_bench.rs +2 -2
- {dirsql-0.3.95 → dirsql-0.3.96/packages/rust}/docs/howto/define-tables.md +4 -3
- {dirsql-0.3.95/packages/python → dirsql-0.3.96/packages/rust}/docs/reference/config.md +1 -1
- {dirsql-0.3.95/packages/python → dirsql-0.3.96/packages/rust}/docs/reference/sdk.md +3 -1
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/lib.rs +61 -43
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/matcher.rs +146 -96
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/persist.rs +84 -16
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/scanner.rs +4 -2
- {dirsql-0.3.95 → dirsql-0.3.96}/Cargo.toml +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/README.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/dirsql/__init__.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/dirsql/_async.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/dirsql/cli/main.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/dirsql/cli/resolve_config_extensions.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/dirsql/py.typed +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/dirsql/resolve_config_extensions.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/dirsql/resolve_extension.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/AGENTS.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/explanation.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/getting-started.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/howto/embed.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/howto/persist.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/index.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/migrations.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/package.json +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/reference/cli.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/reference/columns.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/reference/hooks.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/reference/http-api.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/README.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/conftest.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/explanation.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/howto/embed.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/howto/persist.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/reference/cli.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/reference/columns.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/reference/hooks.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/reference/http-api.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/tests/integration/sidebar.spec.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/e2e-attestations/claude-open-issues-review-le8hm5-red-546.json +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/e2e-attestations/claude-open-issues-review-le8hm5-red-553.json +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/src/lib.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/tests/integration/binding/__init__.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/python/tests/integration/hermetic/__init__.py +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/README.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/explanation.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/howto/columns-from-paths.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/howto/embed.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/howto/extract-from-contents.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/howto/load-extension.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/howto/persist.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/howto/react-to-changes.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/howto/search-by-meaning.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/howto/skip-files.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/reference/cli.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/reference/columns.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/reference/hooks.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/docs/reference/http-api.md +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/bin/dirsql.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/cli/execute.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/cli/mod.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/command.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/config.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/db.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/default_config.toml +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.95 → dirsql-0.3.96}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.95 → 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
|
|
@@ -454,18 +454,33 @@ impl DirSQL {
|
|
|
454
454
|
return Vec::new();
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
457
|
+
// Fan-out: dispatch the event to every table whose glob matches, and
|
|
458
|
+
// concatenate the resulting row events. Cross-table event order is
|
|
459
|
+
// unspecified. An `extract` failure produces an error event for that
|
|
460
|
+
// table only; the other matching tables still process the event.
|
|
461
|
+
let matches = self.inner.matcher.match_all(&rel_path_buf);
|
|
462
|
+
if matches.is_empty() {
|
|
463
|
+
return Vec::new();
|
|
464
|
+
}
|
|
461
465
|
let rel_path = rel_path_buf.to_string_lossy().to_string();
|
|
462
466
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
+
let mut events = Vec::new();
|
|
468
|
+
for m in matches {
|
|
469
|
+
match &event {
|
|
470
|
+
FileEvent::Deleted(_) => {
|
|
471
|
+
events.extend(self.handle_delete(&m.table_name, &rel_path));
|
|
472
|
+
}
|
|
473
|
+
FileEvent::Created(_) | FileEvent::Modified(_) => {
|
|
474
|
+
events.extend(self.handle_upsert(
|
|
475
|
+
&m.table_name,
|
|
476
|
+
&abs_path,
|
|
477
|
+
&rel_path,
|
|
478
|
+
&m.captures,
|
|
479
|
+
));
|
|
480
|
+
}
|
|
467
481
|
}
|
|
468
482
|
}
|
|
483
|
+
events
|
|
469
484
|
}
|
|
470
485
|
|
|
471
486
|
fn handle_delete(&self, table: &str, rel_path: &str) -> Vec<RowEvent> {
|
|
@@ -491,7 +506,13 @@ impl DirSQL {
|
|
|
491
506
|
differ::diff(table, Some(&old_rows), None, rel_path)
|
|
492
507
|
}
|
|
493
508
|
|
|
494
|
-
fn handle_upsert(
|
|
509
|
+
fn handle_upsert(
|
|
510
|
+
&self,
|
|
511
|
+
table: &str,
|
|
512
|
+
abs_path: &Path,
|
|
513
|
+
rel_path: &str,
|
|
514
|
+
captures: &HashMap<String, String>,
|
|
515
|
+
) -> Vec<RowEvent> {
|
|
495
516
|
// The path may have vanished between the watcher event and now, or be a
|
|
496
517
|
// directory (a `mkdir` under the root matches a `**/*` glob). Only
|
|
497
518
|
// regular files become rows — mirror the initial scan, which skips
|
|
@@ -513,12 +534,6 @@ impl DirSQL {
|
|
|
513
534
|
Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
|
|
514
535
|
};
|
|
515
536
|
|
|
516
|
-
let captures = self
|
|
517
|
-
.inner
|
|
518
|
-
.matcher
|
|
519
|
-
.match_file_with_captures(Path::new(rel_path))
|
|
520
|
-
.map(|m| m.captures)
|
|
521
|
-
.unwrap_or_default();
|
|
522
537
|
let stat = compute_stat_virtuals(rel_path, abs_path);
|
|
523
538
|
|
|
524
539
|
let strict = *self.inner.strict_map.get(table).unwrap_or(&false);
|
|
@@ -535,7 +550,7 @@ impl DirSQL {
|
|
|
535
550
|
Ok(cols) => cols,
|
|
536
551
|
Err(e) => return vec![error_event(Some(table), rel_path, e.to_string())],
|
|
537
552
|
};
|
|
538
|
-
let raw_rows = merge_filesystem_facts(raw_rows,
|
|
553
|
+
let raw_rows = merge_filesystem_facts(raw_rows, captures, &stat, &declared_columns);
|
|
539
554
|
let mut new_rows = Vec::with_capacity(raw_rows.len());
|
|
540
555
|
for raw in &raw_rows {
|
|
541
556
|
match db.normalize_row(table, raw, strict) {
|
|
@@ -742,7 +757,7 @@ impl DirSQL {
|
|
|
742
757
|
for (rel_path, table_name) in deleted {
|
|
743
758
|
db.delete_rows_by_file(table_name, rel_path)
|
|
744
759
|
.map_err(map_db_error)?;
|
|
745
|
-
cache_delete_file(db.conn(), rel_path).map_err(DirSqlError::sqlite)?;
|
|
760
|
+
cache_delete_file(db.conn(), rel_path, table_name).map_err(DirSqlError::sqlite)?;
|
|
746
761
|
}
|
|
747
762
|
}
|
|
748
763
|
|
|
@@ -764,10 +779,9 @@ impl DirSQL {
|
|
|
764
779
|
message: e.to_string(),
|
|
765
780
|
})?;
|
|
766
781
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
.unwrap_or_default();
|
|
782
|
+
// Captures are per-glob: use the captures belonging to THIS
|
|
783
|
+
// entry's table, not a first-match lookup.
|
|
784
|
+
let captures = matcher.captures_for(Path::new(&rel_path), &table_name);
|
|
771
785
|
let stat_virtuals = compute_stat_virtuals(&rel_path, &abs_path);
|
|
772
786
|
let declared_columns = db.get_table_columns(&table_name).map_err(map_db_error)?;
|
|
773
787
|
let raw_rows =
|
|
@@ -1144,7 +1158,9 @@ pub struct TrustedFile {
|
|
|
1144
1158
|
|
|
1145
1159
|
struct PersistContext {
|
|
1146
1160
|
db: Db,
|
|
1147
|
-
|
|
1161
|
+
/// Cached file bookkeeping keyed by `(rel_path, table_name)` — a file may
|
|
1162
|
+
/// be cached under several tables under fan-out.
|
|
1163
|
+
cached: HashMap<(String, String), CachedFile>,
|
|
1148
1164
|
expected_meta: HashMap<String, String>,
|
|
1149
1165
|
}
|
|
1150
1166
|
|
|
@@ -1259,18 +1275,20 @@ fn reconcile_scan(
|
|
|
1259
1275
|
) -> Result<(Vec<ScannedFile>, Vec<TrustedFile>, Vec<(String, String)>)> {
|
|
1260
1276
|
let mut to_parse = Vec::new();
|
|
1261
1277
|
let mut trusted = Vec::new();
|
|
1262
|
-
|
|
1278
|
+
// Keyed by (rel_path, table_name): under fan-out one file may be scanned
|
|
1279
|
+
// for several tables, and each pair is trusted/deleted independently.
|
|
1280
|
+
let mut seen: std::collections::HashSet<(String, String)> =
|
|
1263
1281
|
std::collections::HashSet::with_capacity(scanned.len());
|
|
1264
1282
|
|
|
1265
1283
|
for (path, table_name) in scanned {
|
|
1266
1284
|
let rel_path = relative_path(root, &path);
|
|
1267
|
-
|
|
1285
|
+
seen.insert((rel_path.clone(), table_name.clone()));
|
|
1268
1286
|
|
|
1269
1287
|
let stat = fs.stat(&path)?;
|
|
1270
1288
|
|
|
1271
|
-
let cached = ctx.cached.get(&rel_path);
|
|
1289
|
+
let cached = ctx.cached.get(&(rel_path.clone(), table_name.clone()));
|
|
1272
1290
|
let trust = match cached {
|
|
1273
|
-
Some(c) if c.
|
|
1291
|
+
Some(c) if c.stat == stat => {
|
|
1274
1292
|
// Stat matches. Outside the racy window? Trust the cache.
|
|
1275
1293
|
if c.snapshot_ns > stat.mtime_ns {
|
|
1276
1294
|
true
|
|
@@ -1300,9 +1318,9 @@ fn reconcile_scan(
|
|
|
1300
1318
|
}
|
|
1301
1319
|
|
|
1302
1320
|
let mut deleted = Vec::new();
|
|
1303
|
-
for (rel_path,
|
|
1304
|
-
if !
|
|
1305
|
-
deleted.push((rel_path.clone(),
|
|
1321
|
+
for (rel_path, table_name) in ctx.cached.keys() {
|
|
1322
|
+
if !seen.contains(&(rel_path.clone(), table_name.clone())) {
|
|
1323
|
+
deleted.push((rel_path.clone(), table_name.clone()));
|
|
1306
1324
|
}
|
|
1307
1325
|
}
|
|
1308
1326
|
|
|
@@ -2160,7 +2178,7 @@ mod internal_tests {
|
|
|
2160
2178
|
|
|
2161
2179
|
let mut cached = HashMap::new();
|
|
2162
2180
|
cached.insert(
|
|
2163
|
-
"a.txt".to_string(),
|
|
2181
|
+
("a.txt".to_string(), "t".to_string()),
|
|
2164
2182
|
CachedFile {
|
|
2165
2183
|
rel_path: "a.txt".into(),
|
|
2166
2184
|
table_name: "t".into(),
|
|
@@ -2196,7 +2214,7 @@ mod internal_tests {
|
|
|
2196
2214
|
|
|
2197
2215
|
let mut cached = HashMap::new();
|
|
2198
2216
|
cached.insert(
|
|
2199
|
-
"b.txt".to_string(),
|
|
2217
|
+
("b.txt".to_string(), "t".to_string()),
|
|
2200
2218
|
CachedFile {
|
|
2201
2219
|
rel_path: "b.txt".into(),
|
|
2202
2220
|
table_name: "t".into(),
|
|
@@ -2365,7 +2383,7 @@ mod internal_tests {
|
|
|
2365
2383
|
// The old-row snapshot succeeds but the delete itself fails: a
|
|
2366
2384
|
// trigger aborts every DELETE on items.
|
|
2367
2385
|
let (_dir, db, abs, rel) = upsert_fixture();
|
|
2368
|
-
let events = db.handle_upsert("items", &abs, &rel);
|
|
2386
|
+
let events = db.handle_upsert("items", &abs, &rel, &HashMap::new());
|
|
2369
2387
|
assert_eq!(events.len(), 1, "fixture insert failed: {events:?}");
|
|
2370
2388
|
{
|
|
2371
2389
|
let guard = db.inner.db.lock().unwrap();
|
|
@@ -2389,7 +2407,7 @@ mod internal_tests {
|
|
|
2389
2407
|
fn handle_upsert_surfaces_db_poison() {
|
|
2390
2408
|
let (_dir, db, abs, rel) = upsert_fixture();
|
|
2391
2409
|
poison(&db.inner.db);
|
|
2392
|
-
let events = db.handle_upsert("items", &abs, &rel);
|
|
2410
|
+
let events = db.handle_upsert("items", &abs, &rel, &HashMap::new());
|
|
2393
2411
|
assert_single_lock_error(&events);
|
|
2394
2412
|
}
|
|
2395
2413
|
|
|
@@ -2409,7 +2427,7 @@ mod internal_tests {
|
|
|
2409
2427
|
)
|
|
2410
2428
|
.unwrap();
|
|
2411
2429
|
}
|
|
2412
|
-
let events = db.handle_upsert("items", &abs, &rel);
|
|
2430
|
+
let events = db.handle_upsert("items", &abs, &rel, &HashMap::new());
|
|
2413
2431
|
assert_eq!(events.len(), 1, "expected one error event: {events:?}");
|
|
2414
2432
|
let dbg = format!("{:?}", events[0]);
|
|
2415
2433
|
assert!(dbg.contains("Error"), "expected an Error event: {dbg}");
|
|
@@ -2436,7 +2454,7 @@ mod internal_tests {
|
|
|
2436
2454
|
)
|
|
2437
2455
|
.unwrap();
|
|
2438
2456
|
|
|
2439
|
-
let events = db.handle_upsert("files", &subdir, "subdir");
|
|
2457
|
+
let events = db.handle_upsert("files", &subdir, "subdir", &HashMap::new());
|
|
2440
2458
|
assert!(events.is_empty(), "a directory must not produce row events");
|
|
2441
2459
|
assert!(
|
|
2442
2460
|
db.query("SELECT * FROM files").unwrap().is_empty(),
|
|
@@ -2448,14 +2466,14 @@ mod internal_tests {
|
|
|
2448
2466
|
fn handle_upsert_returns_empty_when_file_vanished() {
|
|
2449
2467
|
let (dir, db, _abs, _rel) = upsert_fixture();
|
|
2450
2468
|
let missing = dir.path().join("gone.txt");
|
|
2451
|
-
let events = db.handle_upsert("items", &missing, "gone.txt");
|
|
2469
|
+
let events = db.handle_upsert("items", &missing, "gone.txt", &HashMap::new());
|
|
2452
2470
|
assert!(events.is_empty(), "vanished file must produce no events");
|
|
2453
2471
|
}
|
|
2454
2472
|
|
|
2455
2473
|
#[test]
|
|
2456
2474
|
fn handle_upsert_returns_empty_for_unknown_table() {
|
|
2457
2475
|
let (_dir, db, abs, rel) = upsert_fixture();
|
|
2458
|
-
let events = db.handle_upsert("not_a_table", &abs, &rel);
|
|
2476
|
+
let events = db.handle_upsert("not_a_table", &abs, &rel, &HashMap::new());
|
|
2459
2477
|
assert!(events.is_empty(), "unknown table must produce no events");
|
|
2460
2478
|
}
|
|
2461
2479
|
|
|
@@ -2483,7 +2501,7 @@ mod internal_tests {
|
|
|
2483
2501
|
)
|
|
2484
2502
|
.unwrap();
|
|
2485
2503
|
|
|
2486
|
-
let events = db.handle_upsert("items", &abs, "a.txt");
|
|
2504
|
+
let events = db.handle_upsert("items", &abs, "a.txt", &HashMap::new());
|
|
2487
2505
|
assert_eq!(events.len(), 1, "expected one error event: {events:?}");
|
|
2488
2506
|
let dbg = format!("{:?}", events[0]);
|
|
2489
2507
|
assert!(dbg.contains("Error"), "expected an Error event: {dbg}");
|
|
@@ -2635,7 +2653,7 @@ mod internal_tests {
|
|
|
2635
2653
|
#[test]
|
|
2636
2654
|
fn handle_upsert_inserts_and_diffs_rows() {
|
|
2637
2655
|
let (_dir, db, abs, rel) = upsert_fixture();
|
|
2638
|
-
let events = db.handle_upsert("items", &abs, &rel);
|
|
2656
|
+
let events = db.handle_upsert("items", &abs, &rel, &HashMap::new());
|
|
2639
2657
|
assert_eq!(events.len(), 1, "got: {events:?}");
|
|
2640
2658
|
assert!(matches!(&events[0], RowEvent::Insert { .. }));
|
|
2641
2659
|
|
|
@@ -2660,7 +2678,7 @@ mod internal_tests {
|
|
|
2660
2678
|
Arc::new(fake),
|
|
2661
2679
|
)
|
|
2662
2680
|
.unwrap();
|
|
2663
|
-
let events = db.handle_upsert("items", &abs, "a.txt");
|
|
2681
|
+
let events = db.handle_upsert("items", &abs, "a.txt", &HashMap::new());
|
|
2664
2682
|
assert_eq!(events.len(), 1, "got: {events:?}");
|
|
2665
2683
|
let dbg = format!("{:?}", events[0]);
|
|
2666
2684
|
assert!(dbg.contains("Error"), "got: {dbg}");
|
|
@@ -2772,7 +2790,7 @@ mod internal_tests {
|
|
|
2772
2790
|
let fake = FakeFs::with_stat(abs.clone(), stat.clone());
|
|
2773
2791
|
let mut cached = HashMap::new();
|
|
2774
2792
|
cached.insert(
|
|
2775
|
-
"a.txt".to_string(),
|
|
2793
|
+
("a.txt".to_string(), "t".to_string()),
|
|
2776
2794
|
CachedFile {
|
|
2777
2795
|
rel_path: "a.txt".into(),
|
|
2778
2796
|
table_name: "t".into(),
|
|
@@ -2804,7 +2822,7 @@ mod internal_tests {
|
|
|
2804
2822
|
let fake = FakeFs::with_stat(abs.clone(), stat.clone());
|
|
2805
2823
|
let mut cached = HashMap::new();
|
|
2806
2824
|
cached.insert(
|
|
2807
|
-
"a.txt".to_string(),
|
|
2825
|
+
("a.txt".to_string(), "other".to_string()),
|
|
2808
2826
|
CachedFile {
|
|
2809
2827
|
rel_path: "a.txt".into(),
|
|
2810
2828
|
table_name: "other".into(),
|
|
@@ -2830,7 +2848,7 @@ mod internal_tests {
|
|
|
2830
2848
|
let dir = TempDir::new().unwrap();
|
|
2831
2849
|
let mut cached = HashMap::new();
|
|
2832
2850
|
cached.insert(
|
|
2833
|
-
"gone.txt".to_string(),
|
|
2851
|
+
("gone.txt".to_string(), "t".to_string()),
|
|
2834
2852
|
CachedFile {
|
|
2835
2853
|
rel_path: "gone.txt".into(),
|
|
2836
2854
|
table_name: "t".into(),
|