From 18b8ebd3e0c9bab5dafd77116f87fdd79bbc732f Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:18:12 -0400 Subject: [PATCH 1/7] Test that config globs hide dot-named entries unless spelled --- packages/rust/tests/config_glob_parity.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/rust/tests/config_glob_parity.rs b/packages/rust/tests/config_glob_parity.rs index f592ed687..ba0a66837 100644 --- a/packages/rust/tests/config_glob_parity.rs +++ b/packages/rust/tests/config_glob_parity.rs @@ -131,3 +131,19 @@ fn a_brace_alternation_still_matches_each_option() { assert_eq!(path_table_paths(root.path(), "{a,b}.md"), expected); assert_eq!(config_paths(root.path(), "{a,b}.md"), expected); } + +#[test] +fn dot_named_entries_are_hidden_unless_the_glob_spells_them() { + let root = tree(&["a.md", ".env.md", ".hid/z.md", "sub/b.md"]); + let expected = vec!["a.md", "sub/b.md"]; + assert_eq!(path_table_paths(root.path(), "**/*.md"), expected); + assert_eq!(config_paths(root.path(), "**/*.md"), expected); +} + +#[test] +fn a_spelled_dot_directory_is_listed() { + let root = tree(&["a.md", ".hid/z.md"]); + let expected = vec![".hid/z.md"]; + assert_eq!(path_table_paths(root.path(), ".hid/*.md"), expected); + assert_eq!(config_paths(root.path(), ".hid/*.md"), expected); +} From 7fdb84503456d3f4cf64f8088222530d48bcf8d7 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:26:10 -0400 Subject: [PATCH 2/7] Hide dot-named entries from config globs unless spelled --- .../changelog.d/2026-10-06-config-dotnames.md | 3 ++ .../2026-10-06-config-dotnames.md | 41 ++++++++++++++++++ .../changelog.d/2026-10-06-config-dotnames.md | 3 ++ .../2026-10-06-config-dotnames.md | 41 ++++++++++++++++++ packages/rust/src/matcher.rs | 10 ++++- packages/rust/src/scanner.rs | 43 ++++++++++++++++--- .../changelog.d/2026-10-06-config-dotnames.md | 3 ++ .../2026-10-06-config-dotnames.md | 41 ++++++++++++++++++ 8 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 packages/python/changelog.d/2026-10-06-config-dotnames.md create mode 100644 packages/python/migrations.d/2026-10-06-config-dotnames.md create mode 100644 packages/rust/changelog.d/2026-10-06-config-dotnames.md create mode 100644 packages/rust/migrations.d/2026-10-06-config-dotnames.md create mode 100644 packages/ts/changelog.d/2026-10-06-config-dotnames.md create mode 100644 packages/ts/migrations.d/2026-10-06-config-dotnames.md diff --git a/packages/python/changelog.d/2026-10-06-config-dotnames.md b/packages/python/changelog.d/2026-10-06-config-dotnames.md new file mode 100644 index 000000000..8639ae843 --- /dev/null +++ b/packages/python/changelog.d/2026-10-06-config-dotnames.md @@ -0,0 +1,3 @@ +**Changed** A declared `[[table]] glob` no longer matches a dot-named file or +directory unless the glob spells it: `**/*.md` skips `.env.md` and +`.hid/z.md`, while `.hid/*.md` lists `.hid/z.md`. diff --git a/packages/python/migrations.d/2026-10-06-config-dotnames.md b/packages/python/migrations.d/2026-10-06-config-dotnames.md new file mode 100644 index 000000000..127bd6158 --- /dev/null +++ b/packages/python/migrations.d/2026-10-06-config-dotnames.md @@ -0,0 +1,41 @@ +### A config glob hides dot-named entries unless it spells them + +#### Summary + +`glob = "**/*.md"` used to match `.env.md` and everything under `.hid/`. It +now matches neither, as a path-table and bash (with `dotglob` off) do. A config +that read dot-named files without naming them loses those rows. + +#### Required changes + +| Before | After | +| --- | --- | +| `glob = "**/*.md"` (included `.hid/z.md`) | `glob = ".hid/*.md"` or `glob = "{*,.hid}/*.md"` | + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- A dot-named component matches only where the glob spells a dot (`.hid`, + `.*`). +- A glob that names the dot directory (`.hid/*.md`) is unchanged. + +#### Verification + +```bash +cd "$(mktemp -d)" +mkdir .hid +echo '[{"p":"a"}]' > a.json +echo '[{"p":"h"}]' > .hid/h.json +cat > .dirsql.toml <<'TOML' +[[table]] +name = "t" +glob = "**/*.json" +ddl = "CREATE TABLE t (p TEXT)" +on-file = "jq -sc add" +TOML +dirsql query "SELECT p FROM t" -c .dirsql.toml +# expected: [{"p":"a"}] +``` diff --git a/packages/rust/changelog.d/2026-10-06-config-dotnames.md b/packages/rust/changelog.d/2026-10-06-config-dotnames.md new file mode 100644 index 000000000..8639ae843 --- /dev/null +++ b/packages/rust/changelog.d/2026-10-06-config-dotnames.md @@ -0,0 +1,3 @@ +**Changed** A declared `[[table]] glob` no longer matches a dot-named file or +directory unless the glob spells it: `**/*.md` skips `.env.md` and +`.hid/z.md`, while `.hid/*.md` lists `.hid/z.md`. diff --git a/packages/rust/migrations.d/2026-10-06-config-dotnames.md b/packages/rust/migrations.d/2026-10-06-config-dotnames.md new file mode 100644 index 000000000..127bd6158 --- /dev/null +++ b/packages/rust/migrations.d/2026-10-06-config-dotnames.md @@ -0,0 +1,41 @@ +### A config glob hides dot-named entries unless it spells them + +#### Summary + +`glob = "**/*.md"` used to match `.env.md` and everything under `.hid/`. It +now matches neither, as a path-table and bash (with `dotglob` off) do. A config +that read dot-named files without naming them loses those rows. + +#### Required changes + +| Before | After | +| --- | --- | +| `glob = "**/*.md"` (included `.hid/z.md`) | `glob = ".hid/*.md"` or `glob = "{*,.hid}/*.md"` | + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- A dot-named component matches only where the glob spells a dot (`.hid`, + `.*`). +- A glob that names the dot directory (`.hid/*.md`) is unchanged. + +#### Verification + +```bash +cd "$(mktemp -d)" +mkdir .hid +echo '[{"p":"a"}]' > a.json +echo '[{"p":"h"}]' > .hid/h.json +cat > .dirsql.toml <<'TOML' +[[table]] +name = "t" +glob = "**/*.json" +ddl = "CREATE TABLE t (p TEXT)" +on-file = "jq -sc add" +TOML +dirsql query "SELECT p FROM t" -c .dirsql.toml +# expected: [{"p":"a"}] +``` diff --git a/packages/rust/src/matcher.rs b/packages/rust/src/matcher.rs index a80e5e1b8..129be062a 100644 --- a/packages/rust/src/matcher.rs +++ b/packages/rust/src/matcher.rs @@ -2,6 +2,7 @@ use crate::posix_class::expand_posix_classes; use crate::scanner::{PathGlob, compile_glob}; use globset::GlobBuilder; use regex::{Regex, RegexBuilder}; +use std::ffi::OsStr; use std::path::Path; #[derive(Debug, thiserror::Error)] @@ -140,13 +141,20 @@ impl TableMatcher { pub fn match_all(&self, path: &Path) -> Vec { self.entries .iter() - .filter(|entry| entry.pattern.is_match(path)) + .filter(|entry| entry.pattern.is_match_unhidden(path)) .map(|entry| MatchResult { table_name: entry.table_name.clone(), }) .collect() } + /// Whether any table's glob spells the dot-named `name`. + pub(crate) fn spells_dot_name(&self, name: &OsStr) -> bool { + self.entries + .iter() + .any(|entry| entry.pattern.spells_dot_name(name)) + } + /// Returns true if the path matches any ignore pattern. pub fn is_ignored(&self, path: &Path) -> bool { self.ignore_set.iter().any(|p| p.is_match(path)) diff --git a/packages/rust/src/scanner.rs b/packages/rust/src/scanner.rs index c38d66200..863ead84a 100644 --- a/packages/rust/src/scanner.rs +++ b/packages/rust/src/scanner.rs @@ -203,7 +203,7 @@ enum Kind { } impl PathGlob { - fn spells_dot_name(&self, name: &OsStr) -> bool { + pub(crate) fn spells_dot_name(&self, name: &OsStr) -> bool { self.spelled_dot_names .iter() .any(|p| p.is_match(Path::new(name))) @@ -213,6 +213,17 @@ impl PathGlob { self.files.iter().any(|p| p.is_match(rel_path)) } + /// [`is_match`](Self::is_match), and every dot-named component of the path + /// is one the glob spells. + pub(crate) fn is_match_unhidden(&self, rel_path: &Path) -> bool { + self.is_match(rel_path) + && rel_path + .components() + .map(|c| c.as_os_str()) + .filter(|name| is_dot_named(name)) + .all(|name| self.spells_dot_name(name)) + } + fn has_components(&self) -> bool { !self.components.is_empty() } @@ -440,10 +451,11 @@ impl Walk<'_> { } fn admits_name(&self, name: &OsStr) -> bool { - match self.glob { - Some(glob) => !is_dot_named(name) || glob.spells_dot_name(name), - None => true, - } + !is_dot_named(name) + || match self.glob { + Some(glob) => glob.spells_dot_name(name), + None => self.ignore.spells_dot_name(name), + } } fn next_states(&self, states: &[usize], name: &OsStr, kind: Kind) -> Vec { @@ -899,8 +911,8 @@ mod tests { } #[test] - fn admits_every_dot_named_entry_when_no_glob_governs_the_walk() { - let ignore = TableMatcher::new(&[], &[]).unwrap(); + fn a_table_walk_admits_a_dot_name_only_a_table_glob_spells() { + let ignore = TableMatcher::new(&[(".env", "t")], &[]).unwrap(); let walk = walk_with(&ignore, None, Vec::new()); assert!(walk.admits( 1, @@ -909,6 +921,23 @@ mod tests { Path::new("/r/.env"), Path::new(".env") )); + assert!(!walk.admits( + 1, + false, + OsStr::new(".other"), + Path::new("/r/.other"), + Path::new(".other") + )); + } + + #[test] + fn a_match_with_an_unspelled_dot_component_is_hidden() { + let glob = compile_glob("**/*.md").unwrap(); + assert!(glob.is_match_unhidden(Path::new("sub/b.md"))); + assert!(!glob.is_match_unhidden(Path::new(".env.md"))); + assert!(!glob.is_match_unhidden(Path::new(".hid/z.md"))); + let spelled = compile_glob(".hid/*.md").unwrap(); + assert!(spelled.is_match_unhidden(Path::new(".hid/z.md"))); } #[test] diff --git a/packages/ts/changelog.d/2026-10-06-config-dotnames.md b/packages/ts/changelog.d/2026-10-06-config-dotnames.md new file mode 100644 index 000000000..8639ae843 --- /dev/null +++ b/packages/ts/changelog.d/2026-10-06-config-dotnames.md @@ -0,0 +1,3 @@ +**Changed** A declared `[[table]] glob` no longer matches a dot-named file or +directory unless the glob spells it: `**/*.md` skips `.env.md` and +`.hid/z.md`, while `.hid/*.md` lists `.hid/z.md`. diff --git a/packages/ts/migrations.d/2026-10-06-config-dotnames.md b/packages/ts/migrations.d/2026-10-06-config-dotnames.md new file mode 100644 index 000000000..127bd6158 --- /dev/null +++ b/packages/ts/migrations.d/2026-10-06-config-dotnames.md @@ -0,0 +1,41 @@ +### A config glob hides dot-named entries unless it spells them + +#### Summary + +`glob = "**/*.md"` used to match `.env.md` and everything under `.hid/`. It +now matches neither, as a path-table and bash (with `dotglob` off) do. A config +that read dot-named files without naming them loses those rows. + +#### Required changes + +| Before | After | +| --- | --- | +| `glob = "**/*.md"` (included `.hid/z.md`) | `glob = ".hid/*.md"` or `glob = "{*,.hid}/*.md"` | + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- A dot-named component matches only where the glob spells a dot (`.hid`, + `.*`). +- A glob that names the dot directory (`.hid/*.md`) is unchanged. + +#### Verification + +```bash +cd "$(mktemp -d)" +mkdir .hid +echo '[{"p":"a"}]' > a.json +echo '[{"p":"h"}]' > .hid/h.json +cat > .dirsql.toml <<'TOML' +[[table]] +name = "t" +glob = "**/*.json" +ddl = "CREATE TABLE t (p TEXT)" +on-file = "jq -sc add" +TOML +dirsql query "SELECT p FROM t" -c .dirsql.toml +# expected: [{"p":"a"}] +``` From 80e9cf4387d741ddf11b9311b6b14f7dac1b2982 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:27:06 -0400 Subject: [PATCH 3/7] e2e attestation for claude/1258-config-dotnames --- .../ts/e2e-attestations/claude-1258-config-dotnames.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/ts/e2e-attestations/claude-1258-config-dotnames.json diff --git a/packages/ts/e2e-attestations/claude-1258-config-dotnames.json b/packages/ts/e2e-attestations/claude-1258-config-dotnames.json new file mode 100644 index 000000000..df6b4f4ac --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1258-config-dotnames.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1791257226, + "exit_code": 0, + "commit": "7fdb84503456d3f4cf64f8088222530d48bcf8d7", + "branch": "claude/1258-config-dotnames" +} From e5cb1b6d850f1c3eac48c469229d617e3c4c8153 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:39:58 -0400 Subject: [PATCH 4/7] Update the walk count for the skipped dot-named config file --- packages/rust/tests/scan_progress_e2e.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/rust/tests/scan_progress_e2e.rs b/packages/rust/tests/scan_progress_e2e.rs index f4bd94cbf..fa2d94b9d 100644 --- a/packages/rust/tests/scan_progress_e2e.rs +++ b/packages/rust/tests/scan_progress_e2e.rs @@ -97,10 +97,11 @@ fn forced_progress_reports_the_walk_phase_on_stderr() { err.contains("dirsql: scanning"), "the directory walk draws a labeled progress line: {err:?}" ); - // Four: the three fixture files plus the `.dirsql.toml` the walk also - // visits. The walk counts files it reached, not rows it matched. + // Three: the fixture files. The dot-named `.dirsql.toml` is not spelled by + // the glob, so the walk skips it. The walk counts files it reached, not + // rows it matched. assert!( - err.contains("dirsql: scanned 4 files in "), + err.contains("dirsql: scanned 3 files in "), "the walk summarizes the files it actually reached: {err:?}" ); } From dd146cf1e0f60982d186b8393e96ca484fb4b0b8 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:57:41 -0400 Subject: [PATCH 5/7] Make the merged scan_dirs tests spell the dot directory they enter --- packages/rust/src/scanner.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rust/src/scanner.rs b/packages/rust/src/scanner.rs index b36ee3a1a..5abf7e626 100644 --- a/packages/rust/src/scanner.rs +++ b/packages/rust/src/scanner.rs @@ -1030,7 +1030,7 @@ mod tests { fs::create_dir_all(root.path().join("a").join("node_modules").join("pkg")).unwrap(); fs::create_dir_all(root.path().join("a").join(RESERVED_DIR)).unwrap(); fs::write(root.path().join("a").join("f.md"), "").unwrap(); - let ignore = TableMatcher::new(&[], &["**/node_modules/**"]).unwrap(); + let ignore = TableMatcher::new(&[(".dirsql/*", "t")], &["**/node_modules/**"]).unwrap(); let a = root.path().join("a"); let mut dirs = scan_dirs(root.path(), &a, &ignore); dirs.sort(); @@ -1052,7 +1052,7 @@ mod tests { fn explore_enters_a_dirsql_directory_below_the_top_level() { let root = tempfile::tempdir().unwrap(); fs::create_dir_all(root.path().join("a").join(RESERVED_DIR)).unwrap(); - let ignore = TableMatcher::new(&[], &[]).unwrap(); + let ignore = TableMatcher::new(&[("**/.dirsql/*", "t")], &[]).unwrap(); let top = Place { walk: walk_with(&ignore, None, Vec::new()), dir: root.path().to_path_buf(), From f1dac5535ee37b35fc9f204f8a64ba0927c21f05 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Tue, 6 Oct 2026 21:18:24 -0400 Subject: [PATCH 6/7] e2e attestation for claude/1258-config-dotnames --- .../e2e-attestations/claude-1258-config-dotnames.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/python/e2e-attestations/claude-1258-config-dotnames.json diff --git a/packages/python/e2e-attestations/claude-1258-config-dotnames.json b/packages/python/e2e-attestations/claude-1258-config-dotnames.json new file mode 100644 index 000000000..e4ce2a3c4 --- /dev/null +++ b/packages/python/e2e-attestations/claude-1258-config-dotnames.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/glob_speed_dotfiles_test.py tests/e2e/glob_speed_recursive_test.py tests/e2e/glob_speed_matchers_test.py tests/e2e/query_contract_test.py -x -q", + "ran_at": 1791335904, + "exit_code": 0, + "commit": "dd146cf1e0f60982d186b8393e96ca484fb4b0b8", + "branch": "claude/1258-config-dotnames" +} From 668d2daa0c370ba7a7c85baa4485c5f622ebf877 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Tue, 6 Oct 2026 21:18:59 -0400 Subject: [PATCH 7/7] e2e attestation for claude/1258-config-dotnames --- packages/ts/e2e-attestations/claude-1258-config-dotnames.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1258-config-dotnames.json b/packages/ts/e2e-attestations/claude-1258-config-dotnames.json index df6b4f4ac..49a003fb4 100644 --- a/packages/ts/e2e-attestations/claude-1258-config-dotnames.json +++ b/packages/ts/e2e-attestations/claude-1258-config-dotnames.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791257226, + "ran_at": 1791335939, "exit_code": 0, - "commit": "7fdb84503456d3f4cf64f8088222530d48bcf8d7", + "commit": "f1dac5535ee37b35fc9f204f8a64ba0927c21f05", "branch": "claude/1258-config-dotnames" }