walker: schema-existence ERROR diagnostics (ADR-0027 step B)
`MatchedKind::Ident` now carries its `IdentSource`. A post-walk pass over a structurally-valid parse flags a matched `Tables` ident that is absent from the schema, or a `Columns` ident absent from the table in scope, as an ERROR diagnostic — the command parses but would fail at execution (ADR-0027 §2). New behaviour: an unknown table / column used to parse cleanly and fail only when run. Column scope is resolved by one left-to-right pass over the matched path (every command places its table ident before the columns that belong to it); an unknown table clears the scope, so its columns are not cascaded into a second diagnostic. New catalog keys `diagnostic.unknown_table` / `diagnostic.unknown_column`.
This commit is contained in:
+137
-3
@@ -330,6 +330,97 @@ pub fn input_verdict(
|
||||
outcome_severity.into_iter().chain(diag_severity).max()
|
||||
}
|
||||
|
||||
/// Schema-existence diagnostics (ADR-0027 §2).
|
||||
///
|
||||
/// A matched `IdentSource::Tables` token whose name is not in
|
||||
/// the schema — or a `Columns` token absent from the table in
|
||||
/// scope — is an ERROR: the command parses but would fail at
|
||||
/// execution. Runs only on a structural `Match`.
|
||||
///
|
||||
/// Column scope is resolved by a single left-to-right pass:
|
||||
/// every command places its table ident before the columns
|
||||
/// that belong to it (a qualified `T.c` puts `T` immediately
|
||||
/// before `c`), so the most recent valid `Tables` ident is the
|
||||
/// table a subsequent `Columns` ident is checked against. An
|
||||
/// unknown table clears the scope, so its columns are not
|
||||
/// cascaded into a second diagnostic.
|
||||
fn schema_existence_diagnostics(
|
||||
path: &MatchedPath,
|
||||
schema: Option<&crate::completion::SchemaCache>,
|
||||
) -> Vec<outcome::Diagnostic> {
|
||||
use crate::dsl::grammar::IdentSource;
|
||||
use outcome::{Diagnostic, MatchedKind, Severity};
|
||||
|
||||
let Some(schema) = schema else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut diagnostics = Vec::new();
|
||||
let mut current_table: Option<String> = None;
|
||||
for item in &path.items {
|
||||
let MatchedKind::Ident { source, .. } = item.kind else {
|
||||
continue;
|
||||
};
|
||||
match source {
|
||||
IdentSource::Tables => {
|
||||
if schema_has_table(schema, &item.text) {
|
||||
current_table = Some(item.text.clone());
|
||||
} else {
|
||||
current_table = None;
|
||||
diagnostics.push(Diagnostic {
|
||||
severity: Severity::Error,
|
||||
span: item.span,
|
||||
message: crate::friendly::translate(
|
||||
"diagnostic.unknown_table",
|
||||
&[("name", &item.text as &dyn std::fmt::Display)],
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
IdentSource::Columns => {
|
||||
if let Some(table) = current_table.as_deref()
|
||||
&& !schema_has_column(schema, table, &item.text)
|
||||
{
|
||||
diagnostics.push(Diagnostic {
|
||||
severity: Severity::Error,
|
||||
span: item.span,
|
||||
message: crate::friendly::translate(
|
||||
"diagnostic.unknown_column",
|
||||
&[
|
||||
("name", &item.text as &dyn std::fmt::Display),
|
||||
("table", &table as &dyn std::fmt::Display),
|
||||
],
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
// Invented names (`NewName`), closed sets (`Types`),
|
||||
// and the other entity kinds are not schema-checked
|
||||
// here (ADR-0027 §2 scopes the check to tables and
|
||||
// columns).
|
||||
IdentSource::NewName
|
||||
| IdentSource::Relationships
|
||||
| IdentSource::Indexes
|
||||
| IdentSource::Types
|
||||
| IdentSource::Free => {}
|
||||
}
|
||||
}
|
||||
diagnostics
|
||||
}
|
||||
|
||||
fn schema_has_table(schema: &crate::completion::SchemaCache, name: &str) -> bool {
|
||||
schema.tables.iter().any(|t| t.eq_ignore_ascii_case(name))
|
||||
}
|
||||
|
||||
fn schema_has_column(
|
||||
schema: &crate::completion::SchemaCache,
|
||||
table: &str,
|
||||
column: &str,
|
||||
) -> bool {
|
||||
schema
|
||||
.columns_for_table(table)
|
||||
.is_some_and(|cols| cols.iter().any(|c| c.name.eq_ignore_ascii_case(column)))
|
||||
}
|
||||
|
||||
/// What the grammar would accept at the end of `source`
|
||||
/// (ADR-0024 §architecture, Phase F walker-driven completion).
|
||||
///
|
||||
@@ -604,14 +695,20 @@ pub fn walk<'a>(
|
||||
other => (other, None),
|
||||
};
|
||||
|
||||
// Schema-existence diagnostics (ADR-0027 §2) layer on top
|
||||
// of a structurally-valid parse; a parse that already
|
||||
// failed gets its ERROR verdict from `outcome`.
|
||||
let diagnostics = if matches!(final_outcome, WalkOutcome::Match { .. }) {
|
||||
schema_existence_diagnostics(&path, ctx.schema)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let result = WalkResult {
|
||||
outcome: final_outcome,
|
||||
matched_path: path,
|
||||
per_byte_class: per_byte,
|
||||
tail_expected,
|
||||
// Filled by the schema-existence and expression passes
|
||||
// (ADR-0027 §2); empty here at the structural layer.
|
||||
diagnostics: Vec::new(),
|
||||
diagnostics,
|
||||
};
|
||||
(Some(result), cmd)
|
||||
}
|
||||
@@ -1317,6 +1414,43 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_verdict_unknown_table_is_error() {
|
||||
// The command parses, but the table does not exist —
|
||||
// an ERROR diagnostic (ADR-0027 §2).
|
||||
let schema = schema_with("Customers", &[("id", Type::Int)]);
|
||||
assert_eq!(
|
||||
super::input_verdict("show data NoSuchTable", Some(&schema)),
|
||||
Some(super::Severity::Error),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_verdict_unknown_column_is_error() {
|
||||
let schema =
|
||||
schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]);
|
||||
assert_eq!(
|
||||
super::input_verdict(
|
||||
"show data Customers where NoSuchCol = 1",
|
||||
Some(&schema),
|
||||
),
|
||||
Some(super::Severity::Error),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_verdict_known_table_and_column_is_clean() {
|
||||
let schema =
|
||||
schema_with("Customers", &[("id", Type::Int), ("Name", Type::Text)]);
|
||||
assert_eq!(
|
||||
super::input_verdict(
|
||||
"show data Customers where id = 1",
|
||||
Some(&schema),
|
||||
),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn walker_parses_insert_with_explicit_column_list() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user