Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"command": "uv run python -m pytest tests/e2e/on_file_query_test.py tests/e2e/declared_table_name_test.py tests/e2e/fanout_test.py tests/e2e/query_contract_test.py tests/e2e/use_case_transcripts_test.py -x -q",
"ran_at": 1793923372,
"exit_code": 0,
"commit": "c92cd88ffba0cc34e3508e16795be83e6547ae5a",
"branch": "bot/1627-config-dotdot-1105"
}
1 change: 1 addition & 0 deletions packages/rust/changelog.d/2026-11-05-config-glob-dotdot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**Fixed** A config `[[table]] glob` with an absolute, `~/`, or `../` prefix resolves `..` through the filesystem, as the shell does, instead of folding it out of the string first. After a symlinked directory, `glob = '/r/y/link/../*.md'` now matches the files `ls` lists (in the link target's parent). The `on-file` hook receives the paths, and `{root}`, with the `..` as written. Windows keeps its own lexical `..` handling.
38 changes: 38 additions & 0 deletions packages/rust/migrations.d/2026-11-05-config-glob-dotdot.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
### `..` in config `[[table]]` globs resolves physically

#### Summary

A config `[[table]] glob` with an absolute, `~/`, or `../` prefix that contains `..` now lets the filesystem resolve it, so a `..` after a symlinked directory climbs out of the link's target, as in the shell. No API changes; nothing breaks for globs without a symlink ahead of a `..`, except that the paths handed to the `on-file` hook, and `{root}`, keep the `..` as written.

#### Required changes

_None._

#### Deprecations removed

_None._

#### Behavior changes without code changes

| Before | After |
|---|---|
| `glob = '/a/b/../*.md'` hands the hook `/a/x.md`, `{root}` is `/a` | hands it `/a/b/../x.md`, `{root}` is `/a/b/..` |
| `glob = '/a/link/../*.md'` with `link -> /real/sub` matches `/a/*.md` | matches `/real/*.md`, as `ls` does |

Windows paths keep folding `..` lexically, as Windows itself does.

#### Verification

```sh
mkdir -p r/real/sub r/y && printf P > r/real/f.md && printf L > r/y/f.md && ln -s "$PWD/r/real/sub" r/y/link
cat > t.toml <<TOML
[[table]]
name = "t"
ddl = "CREATE TABLE t (content TEXT)"
glob = '$PWD/r/y/link/../*.md'
on-file = "jq -Rc '{content: .}'"
TOML
dirsql query "SELECT content FROM t" -c t.toml
```

Expected: `content` is `P`.
29 changes: 27 additions & 2 deletions packages/rust/src/path_table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,12 @@ fn config_anchor_as<S: Syntax>(
let Some(target) = absolute_target::<S>(glob, config_dir, home) else {
return Some((config_dir.to_path_buf(), glob.to_string()));
};
let target = normalize(&target?);
let target = target?;
let target = if S::is_verbatim(&target) {
normalize(&target)
} else {
target
};
let (literal, rest) = split_at_first_glob(&target);
if !rest.is_empty() {
return Some((PathBuf::from(S::literal(literal.as_str())), rest));
Expand Down Expand Up @@ -966,7 +971,15 @@ mod tests {
fn a_parent_config_glob_resolves_against_the_config_directory() {
assert_eq!(
anchor_of("../shared/*.md", None),
Some((PathBuf::from("/shared"), "*.md".to_string()))
Some((PathBuf::from("/cfg/../shared"), "*.md".to_string()))
);
}

#[test]
fn an_absolute_config_glob_keeps_a_parent_component_for_the_filesystem_to_resolve() {
assert_eq!(
anchor_of("/a/link/../*.md", None),
Some((PathBuf::from("/a/link/.."), "*.md".to_string()))
);
}

Expand Down Expand Up @@ -1087,6 +1100,18 @@ mod tests {
assert_eq!(t.path_prefix, r"\\?\C:\var\log");
}

#[test]
fn a_verbatim_config_glob_folds_a_parent_component_the_os_would_take_literally() {
assert_eq!(
config_anchor_as::<Utf8WindowsEncoding>(
r"\\?\C:\var\x\..\log\*.log",
Path::new(r"C:\cfg"),
None
),
Some((PathBuf::from(r"\\?\C:\var\log"), "*.log".to_string()))
);
}

#[test]
fn a_drive_letter_path_folds_a_parent_component_as_windows_does() {
let t = table(r"C:\var\x\..\log\*.log", &nothing_is_a_dir);
Expand Down
51 changes: 51 additions & 0 deletions packages/rust/tests/config_anchor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,54 @@ fn a_config_ignore_is_relative_to_the_config_anchor() {

assert_eq!(paths(&db), vec!["keep.txt".to_string()]);
}

/// `y/link -> real/sub`, so `y/link/..` is `real` to the filesystem and `y`
/// to a lexical fold. Each `f.md` says which one was read.
#[cfg(unix)]
fn contents_through_link(glob_under: impl Fn(&Path) -> String) -> Vec<String> {
let dir = tempfile::TempDir::new().unwrap();
let at = |rel: &str| dir.path().join(rel);
fs::create_dir_all(at("cfg")).unwrap();
fs::create_dir_all(at("real/sub")).unwrap();
fs::create_dir_all(at("y")).unwrap();
fs::write(at("real/f.md"), "PHYSICAL").unwrap();
fs::write(at("y/f.md"), "LEXICAL").unwrap();
std::os::unix::fs::symlink(at("real/sub"), at("y/link")).unwrap();
let cfg = at("cfg/.dirsql.toml");
fs::write(
&cfg,
format!(
"[[table]]\nname = \"files\"\nddl = \"CREATE TABLE files (content TEXT)\"\nglob = '{}'\non-file = '''sh -c 'for p; do printf \"{{\\\"content\\\":\\\"%s\\\"}}\\n\" \"$(cat \"$p\")\"; done' sh'''\n",
glob_under(dir.path())
),
)
.unwrap();

let db = DirSQL::builder().config(&cfg).build().unwrap();
db.query("SELECT content FROM files")
.unwrap()
.iter()
.map(|row| match &row["content"] {
Value::Text(text) => text.clone(),
other => panic!("unexpected {other:?}"),
})
.collect()
}

#[cfg(unix)]
#[test]
fn an_absolute_glob_resolves_a_parent_through_a_symlinked_directory_physically() {
assert_eq!(
contents_through_link(|d| format!("{}/y/link/../*.md", d.display())),
vec!["PHYSICAL".to_string()]
);
}

#[cfg(unix)]
#[test]
fn a_parent_relative_glob_resolves_a_parent_through_a_symlinked_directory_physically() {
assert_eq!(
contents_through_link(|_| "../y/link/../*.md".to_string()),
vec!["PHYSICAL".to_string()]
);
}
7 changes: 7 additions & 0 deletions packages/ts/e2e-attestations/bot-1627-config-dotdot-1105.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"command": "pnpm test:e2e",
"ran_at": 1793923464,
"exit_code": 0,
"commit": "9630c2187772695d5ff44c534f422a8e30c7f96f",
"branch": "bot/1627-config-dotdot-1105"
}
Loading