docs: ADR-0032 Amendment 2 + §10.6 regression tests
Amendment 2 records the §10.6 fixup-pass mechanism choice. §10.6 prescribes "rewriting the highlight class" on projection-list idents at end-of-walk; the actual implementation uses a different mechanism that achieves the identical user-visible behavior: 1. 2d's two-pass schema-existence diagnostic collects every FROM binding from the matched path first, then resolves projection idents against the complete scope. The post-walk re-resolve §10.6 calls for, just embedded in the diagnostic emitter. 2. input_render.rs's diagnostic-overlay path colors each diagnostic span Error/Warning, achieving the visual change §10.6 describes without needing a new HighlightClass variant. The completion-mid-typing piece is improved by the §10.5 look-ahead probe (sub-phase 2e earlier). Four new regression tests in `projection_before_from_tests` pin the behavior so a future refactor can't silently regress it: correct ident resolves silently, unknown ident flags via diagnostic on its span, multi-projection only flags unknowns, projection-without-FROM is silent. ADR index entry updated to reference Amendment 2. Test totals: 1424 → 1428 passing (+4). Clippy clean.
This commit is contained in:
@@ -4178,3 +4178,128 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod projection_before_from_tests {
|
||||
//! ADR-0032 §10.6 — projection-before-FROM correctness
|
||||
//! after the full walk. The 2d schema-existence pass's
|
||||
//! two-pass binding collection (gather all FROM bindings
|
||||
//! first, then resolve column refs) means the diagnostic
|
||||
//! verdict is already correct at end-of-walk:
|
||||
//!
|
||||
//! - A projection ident that resolves under the eventual
|
||||
//! FROM scope produces no diagnostic.
|
||||
//! - A projection ident that does NOT resolve produces an
|
||||
//! `unknown_column` diagnostic on its span — the renderer
|
||||
//! then overlays this as an Error visual via the
|
||||
//! `input_render.rs` diagnostic-overlay path, achieving
|
||||
//! the user-visible effect §10.6 prescribes ("the
|
||||
//! highlight snaps to the column class … or to the
|
||||
//! unknown-identifier diagnostic").
|
||||
//!
|
||||
//! These tests pin the behavior so a future refactor can't
|
||||
//! silently regress it.
|
||||
|
||||
use super::*;
|
||||
use crate::completion::{SchemaCache, TableColumn};
|
||||
use crate::dsl::types::Type;
|
||||
|
||||
fn schema_with_table_and_columns() -> SchemaCache {
|
||||
let mut s = SchemaCache::default();
|
||||
s.tables.push("mytable".to_string());
|
||||
s.columns.push("real_col".to_string());
|
||||
s.columns.push("another_col".to_string());
|
||||
s.table_columns.insert(
|
||||
"mytable".to_string(),
|
||||
vec![
|
||||
TableColumn {
|
||||
name: "real_col".to_string(),
|
||||
user_type: Type::Text,
|
||||
},
|
||||
TableColumn {
|
||||
name: "another_col".to_string(),
|
||||
user_type: Type::Int,
|
||||
},
|
||||
],
|
||||
);
|
||||
s
|
||||
}
|
||||
|
||||
fn diagnostics_advanced(
|
||||
source: &str,
|
||||
schema: &SchemaCache,
|
||||
) -> Vec<outcome::Diagnostic> {
|
||||
let mut ctx = context::WalkContext::with_schema(schema);
|
||||
ctx.mode = crate::mode::Mode::Advanced;
|
||||
let (result, _) =
|
||||
walk(source, outcome::WalkBound::EndOfInput, &mut ctx);
|
||||
result.map_or_else(Vec::new, |r| r.diagnostics)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_before_from_resolves_via_eventual_from() {
|
||||
// `select real_col from mytable` — the projection
|
||||
// ident appears in the path BEFORE the FROM binding,
|
||||
// but the two-pass diagnostic resolves correctly
|
||||
// against the eventual scope. No diagnostic.
|
||||
let schema = schema_with_table_and_columns();
|
||||
let diags =
|
||||
diagnostics_advanced("select real_col from mytable", &schema);
|
||||
assert!(
|
||||
diags.is_empty(),
|
||||
"projection-before-FROM legit column must not be flagged; got {diags:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_before_from_flags_unknown_column() {
|
||||
// `select bogus_col from mytable` — bogus_col doesn't
|
||||
// belong to mytable. The diagnostic fires on the
|
||||
// projection ident's span; the renderer overlays this
|
||||
// as Error in `input_render.rs`.
|
||||
let schema = schema_with_table_and_columns();
|
||||
let diags =
|
||||
diagnostics_advanced("select bogus_col from mytable", &schema);
|
||||
assert_eq!(diags.len(), 1, "{diags:?}");
|
||||
assert_eq!(diags[0].severity, outcome::Severity::Error);
|
||||
// Span should cover `bogus_col` (offset 7..16).
|
||||
assert_eq!(diags[0].span, (7, 16));
|
||||
assert!(
|
||||
diags[0].message.contains("no such column"),
|
||||
"expected unknown_column wording; got {:?}",
|
||||
diags[0].message,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_projection_before_from_flags_only_unknowns() {
|
||||
// `select real_col, bogus_col, another_col from mytable`
|
||||
// — only bogus_col flags; the two real ones resolve.
|
||||
let schema = schema_with_table_and_columns();
|
||||
let diags = diagnostics_advanced(
|
||||
"select real_col, bogus_col, another_col from mytable",
|
||||
&schema,
|
||||
);
|
||||
assert_eq!(
|
||||
diags.len(),
|
||||
1,
|
||||
"expected exactly one diagnostic; got {diags:?}",
|
||||
);
|
||||
assert!(diags[0].message.contains("bogus_col"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_without_from_is_silent() {
|
||||
// `select c1, c2` — no FROM in scope at all. The
|
||||
// current behavior is to skip the bare-column check
|
||||
// entirely (avoid noise on `SELECT 1` style
|
||||
// expressions). This is documented in the
|
||||
// schema_existence pass.
|
||||
let schema = schema_with_table_and_columns();
|
||||
let diags = diagnostics_advanced("select c1, c2", &schema);
|
||||
assert!(
|
||||
diags.is_empty(),
|
||||
"no FROM → silent; got {diags:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user