diff --git a/docs/reference/config.md b/docs/reference/config.md index bf9990403..d7edc0253 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -274,6 +274,10 @@ So `glob = "projects/*/*.jsonl"` in `~/.claude/.dirsql.toml` indexes writes `glob = "~/.claude/projects/*/*.jsonl"`. A table's `path` and `dir` columns, `{root}`, and `ignore` are all relative to its anchor. +A table whose glob matches no files prints +`dirsql: table 'sessions': glob 'projects/*/*.jsonl' matched no files under ` +to stderr; the exit code is unchanged. Path-tables and programmatic tables do not warn. + ### Batch `ddl` `ddl` is handed to SQLite whole, so a table declaration is not limited to one diff --git a/packages/python/changelog.d/2026-10-06-zero-match-config-table-warning.md b/packages/python/changelog.d/2026-10-06-zero-match-config-table-warning.md new file mode 100644 index 000000000..7f9b45fbd --- /dev/null +++ b/packages/python/changelog.d/2026-10-06-zero-match-config-table-warning.md @@ -0,0 +1,3 @@ +**Added** + +- **A config `[[table]]` whose glob matches no files now prints a warning to stderr.** `dirsql: table 'sessions': glob 'projects/*/*.jsonl' matched no files under /home/u/.claude` names the table, the glob and the directory it was matched under, so a glob that silently indexes nothing is visible. The exit code is unchanged, and path-tables and programmatic tables do not warn. (#1232) diff --git a/packages/python/e2e-attestations/claude-1232-zero-match.json b/packages/python/e2e-attestations/claude-1232-zero-match.json new file mode 100644 index 000000000..f2d73964a --- /dev/null +++ b/packages/python/e2e-attestations/claude-1232-zero-match.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/fanout_test.py tests/e2e/capture_collision_test.py tests/e2e/declared_table_name_test.py tests/e2e/extension_package_test.py tests/e2e/plugin_discovery_test.py tests/e2e/use_case_notebook_test.py tests/e2e/on_file_query_test.py tests/e2e/use_case_transcripts_test.py tests/e2e/plugin_extension_discovery_test.py tests/e2e/use_case_join_test.py tests/e2e/use_case_dossier_test.py tests/e2e/sigint_interrupt_test.py tests/e2e/query_contract_test.py -x -q", + "ran_at": 1791306078, + "exit_code": 0, + "commit": "8b587de39902adf33e4ebaa957a8fd0005875029", + "branch": "claude/1232-zero-match" +} diff --git a/packages/python/migrations.d/2026-10-06-zero-match-config-table-warning.md b/packages/python/migrations.d/2026-10-06-zero-match-config-table-warning.md new file mode 100644 index 000000000..dac9ce1d0 --- /dev/null +++ b/packages/python/migrations.d/2026-10-06-zero-match-config-table-warning.md @@ -0,0 +1,25 @@ +### Empty config tables warn on stderr (#1232) + +#### Summary + +A config `[[table]]` whose glob matches no files writes one line to stderr at startup. Nothing else changes: the query still runs and the exit code is the same. + +#### Required changes + +_None._ A script that treats any stderr output as failure should fix the glob or drop the table. + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- stderr gains `dirsql: table '': glob '' matched no files under ` for each config table that matched nothing. + +#### Verification + +```bash +mkdir -p /tmp/cfg /tmp/elsewhere +printf '[[table]]\nname = "t"\nddl = "CREATE TABLE t (n TEXT)"\nglob = "nothing/*.jsonl"\non-file = "printf []"\n' > /tmp/cfg/.dirsql.toml +cd /tmp/elsewhere && dirsql query "SELECT COUNT(*) AS n FROM t" -c /tmp/cfg/.dirsql.toml # stderr: dirsql: table 't': glob 'nothing/*.jsonl' matched no files under /tmp/cfg +``` diff --git a/packages/rust/changelog.d/2026-10-06-zero-match-config-table-warning.md b/packages/rust/changelog.d/2026-10-06-zero-match-config-table-warning.md new file mode 100644 index 000000000..7f9b45fbd --- /dev/null +++ b/packages/rust/changelog.d/2026-10-06-zero-match-config-table-warning.md @@ -0,0 +1,3 @@ +**Added** + +- **A config `[[table]]` whose glob matches no files now prints a warning to stderr.** `dirsql: table 'sessions': glob 'projects/*/*.jsonl' matched no files under /home/u/.claude` names the table, the glob and the directory it was matched under, so a glob that silently indexes nothing is visible. The exit code is unchanged, and path-tables and programmatic tables do not warn. (#1232) diff --git a/packages/rust/migrations.d/2026-10-06-zero-match-config-table-warning.md b/packages/rust/migrations.d/2026-10-06-zero-match-config-table-warning.md new file mode 100644 index 000000000..dac9ce1d0 --- /dev/null +++ b/packages/rust/migrations.d/2026-10-06-zero-match-config-table-warning.md @@ -0,0 +1,25 @@ +### Empty config tables warn on stderr (#1232) + +#### Summary + +A config `[[table]]` whose glob matches no files writes one line to stderr at startup. Nothing else changes: the query still runs and the exit code is the same. + +#### Required changes + +_None._ A script that treats any stderr output as failure should fix the glob or drop the table. + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- stderr gains `dirsql: table '': glob '' matched no files under ` for each config table that matched nothing. + +#### Verification + +```bash +mkdir -p /tmp/cfg /tmp/elsewhere +printf '[[table]]\nname = "t"\nddl = "CREATE TABLE t (n TEXT)"\nglob = "nothing/*.jsonl"\non-file = "printf []"\n' > /tmp/cfg/.dirsql.toml +cd /tmp/elsewhere && dirsql query "SELECT COUNT(*) AS n FROM t" -c /tmp/cfg/.dirsql.toml # stderr: dirsql: table 't': glob 'nothing/*.jsonl' matched no files under /tmp/cfg +``` diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index cb1fdbec8..7e9e0434c 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -285,6 +285,7 @@ pub struct Table { hook: Hook, anchor: Option, ignore: Vec, + warn_when_empty: bool, } impl Table { @@ -298,6 +299,11 @@ impl Table { &self.ignore } + pub(crate) fn warning_when_empty(mut self) -> Self { + self.warn_when_empty = true; + self + } + pub(crate) fn scoped_ignore(mut self, ignore: Vec) -> Self { self.ignore = ignore; self @@ -354,6 +360,7 @@ impl Table { strict: false, anchor: None, ignore: Vec::new(), + warn_when_empty: false, } } @@ -400,6 +407,7 @@ impl Table { strict: false, anchor: None, ignore: Vec::new(), + warn_when_empty: false, } } } @@ -1128,6 +1136,9 @@ impl DirSQL { let mut progress = Progress::scanning(); let (scanned, seen) = scan_groups(&groups, scan, &mut progress); progress.finish(seen); + for warning in empty_table_warnings(&tables, &anchors, &scanned) { + eprintln!("{warning}"); + } scanned }; @@ -2019,6 +2030,28 @@ fn compile_groups( Ok((groups, anchors, names)) } +/// One stderr line per config table that matched no file in the scan, naming +/// the table, its glob and the directory the glob was matched under. +fn empty_table_warnings( + tables: &[Table], + anchors: &TableAnchors, + scanned: &[(PathBuf, String)], +) -> Vec { + tables + .iter() + .filter(|table| table.warn_when_empty) + .filter(|table| !scanned.iter().any(|(_, name)| *name == table.name)) + .map(|table| { + format!( + "dirsql: table '{}': glob '{}' matched no files under {}", + table.name, + table.glob, + anchors.of(&table.name).display() + ) + }) + .collect() +} + /// Open (or create) the persistent SQLite cache and read its meta. If the /// meta is missing or incompatible with the current build, the resulting /// [`PersistContext`] carries an empty file index and asks for a sweep, so @@ -2423,7 +2456,8 @@ fn build_tables_from_config(cfg: &config::Config, config_dir: &Path) -> Result std::path::PathBuf { + let path = dir.join(".dirsql.toml"); + fs::write( + &path, + format!( + "[[table]]\nname = \"sessions\"\nddl = \"CREATE TABLE sessions (n TEXT)\"\nglob = \"{glob}\"\non-file = \"printf []\"\n" + ), + ) + .unwrap(); + path +} + +fn query(cwd: &Path, config: &Path) -> std::process::Output { + std::process::Command::cargo_bin("dirsql") + .expect("`dirsql` binary must be built by `cargo test` with --features cli") + .arg("query") + .arg("SELECT COUNT(*) AS n FROM sessions") + .arg("-c") + .arg(config) + .current_dir(cwd) + .output() + .unwrap() +} + +#[test] +fn an_empty_config_table_warns_on_stderr_and_still_exits_zero() { + let cfg_dir = TempDir::new().unwrap(); + let cfg_dir_path = fs::canonicalize(cfg_dir.path()).unwrap(); + let config = write_config(&cfg_dir_path, "projects/*/*.jsonl"); + let elsewhere = TempDir::new().unwrap(); + + let out = query(elsewhere.path(), &config); + + assert!(out.status.success(), "{out:?}"); + let stderr = String::from_utf8_lossy(&out.stderr); + let expected = format!( + "dirsql: table 'sessions': glob 'projects/*/*.jsonl' matched no files under {}", + cfg_dir_path.display() + ); + assert!( + stderr.lines().any(|line| line == expected), + "expected {expected:?} in stderr, got {stderr:?}" + ); +} + +#[test] +fn a_config_table_with_matches_does_not_warn() { + let cfg_dir = TempDir::new().unwrap(); + fs::create_dir_all(cfg_dir.path().join("projects/p")).unwrap(); + fs::write(cfg_dir.path().join("projects/p/a.jsonl"), "{}").unwrap(); + let config = write_config(cfg_dir.path(), "projects/*/*.jsonl"); + let elsewhere = TempDir::new().unwrap(); + + let out = query(elsewhere.path(), &config); + + assert!(out.status.success(), "{out:?}"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(!stderr.contains("matched no files"), "got {stderr:?}"); +} diff --git a/packages/ts/changelog.d/2026-10-06-zero-match-config-table-warning.md b/packages/ts/changelog.d/2026-10-06-zero-match-config-table-warning.md new file mode 100644 index 000000000..7f9b45fbd --- /dev/null +++ b/packages/ts/changelog.d/2026-10-06-zero-match-config-table-warning.md @@ -0,0 +1,3 @@ +**Added** + +- **A config `[[table]]` whose glob matches no files now prints a warning to stderr.** `dirsql: table 'sessions': glob 'projects/*/*.jsonl' matched no files under /home/u/.claude` names the table, the glob and the directory it was matched under, so a glob that silently indexes nothing is visible. The exit code is unchanged, and path-tables and programmatic tables do not warn. (#1232) diff --git a/packages/ts/e2e-attestations/claude-1232-zero-match.json b/packages/ts/e2e-attestations/claude-1232-zero-match.json new file mode 100644 index 000000000..b9e89b644 --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1232-zero-match.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1791306118, + "exit_code": 0, + "commit": "055602415dffd51c3aa8145b39f5d9b0e8988590", + "branch": "claude/1232-zero-match" +} diff --git a/packages/ts/migrations.d/2026-10-06-zero-match-config-table-warning.md b/packages/ts/migrations.d/2026-10-06-zero-match-config-table-warning.md new file mode 100644 index 000000000..dac9ce1d0 --- /dev/null +++ b/packages/ts/migrations.d/2026-10-06-zero-match-config-table-warning.md @@ -0,0 +1,25 @@ +### Empty config tables warn on stderr (#1232) + +#### Summary + +A config `[[table]]` whose glob matches no files writes one line to stderr at startup. Nothing else changes: the query still runs and the exit code is the same. + +#### Required changes + +_None._ A script that treats any stderr output as failure should fix the glob or drop the table. + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- stderr gains `dirsql: table '': glob '' matched no files under ` for each config table that matched nothing. + +#### Verification + +```bash +mkdir -p /tmp/cfg /tmp/elsewhere +printf '[[table]]\nname = "t"\nddl = "CREATE TABLE t (n TEXT)"\nglob = "nothing/*.jsonl"\non-file = "printf []"\n' > /tmp/cfg/.dirsql.toml +cd /tmp/elsewhere && dirsql query "SELECT COUNT(*) AS n FROM t" -c /tmp/cfg/.dirsql.toml # stderr: dirsql: table 't': glob 'nothing/*.jsonl' matched no files under /tmp/cfg +```