Skip to content

Commit 2fe33db

Browse files
radimclaude
andcommitted
test(rust): cover attribute regex + plan walk
Ports the attribute_test.go cases: aliased equality (exact), bare column on scan node (exact), mismatched qualifier (heuristic), multi-param cond with function-wrapped param ignored, Function Scan with Alias-only attribution, and exact-over-heuristic preservation. Adds three for paths Go didn't cover directly: IN-list first-param attribution, reverse-form ($N op column), and walk_plan recursion into Plans[]. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a4dd645 commit 2fe33db

1 file changed

Lines changed: 161 additions & 0 deletions

File tree

crates/qshape-cli/src/attribute.rs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,164 @@ fn record_param(
305305
},
306306
);
307307
}
308+
309+
#[cfg(test)]
310+
mod tests {
311+
use super::*;
312+
313+
fn ctx() -> AttrCtx {
314+
AttrCtx { by_position: HashMap::new() }
315+
}
316+
317+
fn aliases<const N: usize>(items: [(&str, &str, &str); N]) -> HashMap<String, TableRef> {
318+
items
319+
.into_iter()
320+
.map(|(k, schema, table)| {
321+
(k.to_string(), TableRef { schema: schema.to_string(), table: table.to_string() })
322+
})
323+
.collect()
324+
}
325+
326+
#[test]
327+
fn aliased_equal() {
328+
let mut c = ctx();
329+
let a = aliases([("u", "auth", "user_account")]);
330+
attribute_cond("(u.user_id = $1)", &a, "auth", "user_account", &mut c);
331+
332+
let p = c.by_position.get(&1).expect("param 1 attributed");
333+
assert_eq!(p.schema, "auth");
334+
assert_eq!(p.table, "user_account");
335+
assert_eq!(p.column, "user_id");
336+
assert_eq!(p.confidence, "exact");
337+
}
338+
339+
// PG emits bare column names in plan text when scan is unambiguous.
340+
// Plan node pins column to its relation — exact, not a guess
341+
#[test]
342+
fn unqualified_on_scan_is_exact() {
343+
let mut c = ctx();
344+
let a = aliases([("session", "auth", "session")]);
345+
attribute_cond("(id = $1)", &a, "auth", "session", &mut c);
346+
347+
let p = c.by_position.get(&1).expect("param 1 attributed");
348+
assert_eq!(p.table, "session");
349+
assert_eq!(p.column, "id");
350+
assert_eq!(p.confidence, "exact");
351+
}
352+
353+
// Qualifier that doesn't resolve (outer-scope ref, schema-qualified
354+
// name, subplan name) — best-guess to current relation as heuristic
355+
#[test]
356+
fn mismatched_qualifier_is_heuristic() {
357+
let mut c = ctx();
358+
attribute_cond("(outer_alias.id = $1)", &HashMap::new(), "auth", "session", &mut c);
359+
360+
let p = c.by_position.get(&1).expect("param 1 attributed");
361+
assert_eq!(p.confidence, "heuristic");
362+
}
363+
364+
#[test]
365+
fn multiple_params() {
366+
let mut c = ctx();
367+
let a = aliases([("t", "auth", "oauth_token")]);
368+
attribute_cond(
369+
"((t.access_sha = $2) AND (t.access_hash = hashtext($1)))",
370+
&a,
371+
"auth",
372+
"oauth_token",
373+
&mut c,
374+
);
375+
376+
let p2 = c.by_position.get(&2).expect("param 2 attributed");
377+
assert_eq!(p2.column, "access_sha");
378+
// $1 wrapped in hashtext(...) — no direct column comparison, OK
379+
// either way that the regex misses it
380+
}
381+
382+
// PG plans system views like pg_catalog.pg_settings as Function Scan.
383+
// Plan node has no "Relation Name" but carries view name in Alias —
384+
// walk_plan must still attribute conds on that node
385+
#[test]
386+
fn function_scan_with_alias_only() {
387+
let plan: serde_json::Value = serde_json::from_str(
388+
r#"{
389+
"Node Type": "Function Scan",
390+
"Function Name": "pg_show_all_settings",
391+
"Alias": "pg_settings",
392+
"Filter": "(name = $1)"
393+
}"#,
394+
)
395+
.unwrap();
396+
let mut c = ctx();
397+
walk_plan(&plan, "", "", &mut c);
398+
399+
let p = c.by_position.get(&1).expect("param 1 attributed via Alias fallback");
400+
assert_eq!(p.table, "pg_settings");
401+
assert_eq!(p.column, "name");
402+
assert_eq!(p.confidence, "exact");
403+
}
404+
405+
#[test]
406+
fn preserves_exact_over_heuristic() {
407+
let mut c = ctx();
408+
let a = aliases([("u", "auth", "user_account")]);
409+
attribute_cond("(u.user_id = $1)", &a, "", "", &mut c);
410+
// second hit that would be heuristic on different relation
411+
attribute_cond("(user_id = $1)", &HashMap::new(), "public", "other_table", &mut c);
412+
413+
let p = c.by_position.get(&1).expect("param 1 attributed");
414+
assert_eq!(p.table, "user_account");
415+
assert_eq!(p.confidence, "exact");
416+
}
417+
418+
// IN-list pattern: column IN ($N, $M, ...) attributes only first param
419+
#[test]
420+
fn in_list_attributes_first_param() {
421+
let mut c = ctx();
422+
let a = aliases([("u", "auth", "user_account")]);
423+
attribute_cond("(u.id IN ($3, $4, $5))", &a, "auth", "user_account", &mut c);
424+
425+
let p = c.by_position.get(&3).expect("param 3 attributed");
426+
assert_eq!(p.column, "id");
427+
assert_eq!(p.confidence, "exact");
428+
assert!(c.by_position.get(&4).is_none());
429+
}
430+
431+
// Reverse form: $N op column instead of column op $N
432+
#[test]
433+
fn reverse_form_param_first() {
434+
let mut c = ctx();
435+
let a = aliases([("u", "auth", "user_account")]);
436+
attribute_cond("($1 = u.email)", &a, "auth", "user_account", &mut c);
437+
438+
let p = c.by_position.get(&1).expect("param 1 attributed");
439+
assert_eq!(p.column, "email");
440+
assert_eq!(p.confidence, "exact");
441+
}
442+
443+
// walk_plan recurses into nested Plans
444+
#[test]
445+
fn walk_plan_descends_into_children() {
446+
let plan: serde_json::Value = serde_json::from_str(
447+
r#"{
448+
"Node Type": "Hash Join",
449+
"Hash Cond": "(orders.user_id = users.id)",
450+
"Plans": [
451+
{
452+
"Node Type": "Seq Scan",
453+
"Relation Name": "users",
454+
"Alias": "users",
455+
"Filter": "(email = $1)"
456+
}
457+
]
458+
}"#,
459+
)
460+
.unwrap();
461+
let mut c = ctx();
462+
walk_plan(&plan, "", "", &mut c);
463+
464+
let p = c.by_position.get(&1).expect("param 1 from child Seq Scan");
465+
assert_eq!(p.table, "users");
466+
assert_eq!(p.column, "email");
467+
}
468+
}

0 commit comments

Comments
 (0)