walker: 2e prereq — §10.3 stage-2 CTE harvest + cte_arity_mismatch
Implements the six ADR-0032 §10.3 output-column derivation rules at CTE body-frame exit, populating the placeholder CteBinding's columns. Unblocks `diagnostic.cte_arity_mismatch` (which compares declared col-list arity vs derived projection arity) and the upcoming qualified-prefix completion in 2e proper. - `WalkContext::pending_cte_harvest`: bookkeeping for an in-progress CTE harvest, armed by writes_cte_name + extended by cte_column idents, consumed by the next walk_scoped_subgrammar invocation (CTE syntax has no intervening ScopedSubgrammar, so timing is deterministic). Cleared on every walk_scoped_subgrammar entry to prevent stale state surviving a speculative walk rollback. - `run_cte_harvest`: post-walk path-scan classifier that reconstructs the body's first leg's projection-list and applies the six derivation rules. Compound bodies take columns from the first leg per spec; recursive CTE bodies take the non-recursive (first) leg. Optional (col-list) renames positionally with preserved types. - `expand_binding`: bridges a TableBinding to a CteColumn list, resolving CTE-source bindings (empty columns + table-name matches an in-scope CteBinding) through to the CTE's harvested columns. Enables sibling CTEs to project correctly: in \`WITH a AS (...), b AS (SELECT * FROM a) ...\`, b's harvest sees a's derived columns through the body's from_scope binding. - `WalkContext::pending_diagnostics`: accumulator for diagnostics emitted DURING the walk by node handlers with context the post-walk passes can't reconstruct. Drained by the top-level walk function on both match and non-match paths so a re-used context can't leak entries between walks. Test totals: 1399 → 1414 passing (+15: 10 derivation rules + 1 sibling CTE + 4 arity match/mismatch tests). Clippy clean.
This commit is contained in:
@@ -1797,8 +1797,16 @@ pub fn walk<'a>(
|
||||
// operator slot is highlighted rather than the engine
|
||||
// wording shown at execution time.
|
||||
d.extend(compound_arity_diagnostics(&path));
|
||||
// ADR-0032 §10.3 / §11.2 — diagnostics emitted during
|
||||
// the walk by node handlers with direct context the
|
||||
// post-walk passes can't reconstruct (primarily the
|
||||
// CTE harvest's arity-check at body-frame exit). Drain
|
||||
// unconditionally so accumulated entries don't leak
|
||||
// into a subsequent walk via a re-used WalkContext.
|
||||
d.extend(std::mem::take(&mut ctx.pending_diagnostics));
|
||||
d
|
||||
} else {
|
||||
ctx.pending_diagnostics.clear();
|
||||
Vec::new()
|
||||
};
|
||||
// Expression WARNING diagnostics — type-mismatched
|
||||
@@ -4038,6 +4046,76 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ---- ADR-0032 §11.2 — cte_arity_mismatch ----
|
||||
|
||||
#[test]
|
||||
fn cte_arity_mismatch_when_col_list_shorter() {
|
||||
// `WITH x(a, b) AS (SELECT 1, 2, 3)` — declared 2,
|
||||
// derived 3 → fires.
|
||||
let schema = schema_with("base", &[("id", Type::Int)]);
|
||||
let diags = diag_keys(
|
||||
"with x (a, b) as (select 1, 2, 3) select * from x",
|
||||
&schema,
|
||||
);
|
||||
assert!(
|
||||
diags.iter().any(|d| {
|
||||
d.contains("CTE `x`")
|
||||
&& d.contains("declares 2 columns")
|
||||
&& d.contains("body has 3")
|
||||
}),
|
||||
"expected cte_arity_mismatch (declared 2, actual 3); got {diags:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cte_arity_mismatch_when_col_list_longer() {
|
||||
// `WITH x(a, b, c) AS (SELECT 1)` — declared 3,
|
||||
// derived 1 → fires.
|
||||
let schema = schema_with("base", &[("id", Type::Int)]);
|
||||
let diags = diag_keys(
|
||||
"with x (a, b, c) as (select 1) select * from x",
|
||||
&schema,
|
||||
);
|
||||
assert!(
|
||||
diags.iter().any(|d| {
|
||||
d.contains("CTE `x`")
|
||||
&& d.contains("declares 3 columns")
|
||||
&& d.contains("body has 1")
|
||||
}),
|
||||
"expected cte_arity_mismatch (declared 3, actual 1); got {diags:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cte_arity_match_no_diagnostic() {
|
||||
// `WITH x(a, b) AS (SELECT 1, 2)` — matched arity, no
|
||||
// diagnostic.
|
||||
let schema = schema_with("base", &[("id", Type::Int)]);
|
||||
let diags = diag_keys(
|
||||
"with x (a, b) as (select 1, 2) select * from x",
|
||||
&schema,
|
||||
);
|
||||
assert!(
|
||||
!diags.iter().any(|d| d.contains("declares")),
|
||||
"matched arity should not fire; got {diags:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cte_arity_no_col_list_no_diagnostic() {
|
||||
// No explicit col-list → no arity check (derived
|
||||
// columns are the canonical view).
|
||||
let schema = schema_with("base", &[("id", Type::Int)]);
|
||||
let diags = diag_keys(
|
||||
"with x as (select 1, 2, 3) select * from x",
|
||||
&schema,
|
||||
);
|
||||
assert!(
|
||||
!diags.iter().any(|d| d.contains("declares")),
|
||||
"no col-list should suppress arity check; got {diags:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_in_inner_subquery_does_not_affect_outer_aliases() {
|
||||
// The inner `AS y` is inside parens (depth > 0) and
|
||||
|
||||
Reference in New Issue
Block a user