feat: H1a CROSS JOIN ON teaching message; advanced-SQL gaps re-verified (ADR-0042)
Empirically re-checking ADR §3's advanced-SQL "gaps" reversed two of three — the code survey that produced the list was wrong: - INSERT…SELECT column-count: already handled (verdict=Error, "the column list names N column(s) but M value(s) are given"; insert_select_arity_mismatch_fires). - RETURNING scope: already handled (completion offers the table's columns; `returning <unknown>` → unknown_column diagnostic). The one genuine residual is fixed: `select … cross join b on …` rejected the ON with a bare "expected end of input". Add parse.cross_join_no_on — "a CROSS JOIN has no ON clause — it pairs every row; for a join condition use `JOIN … ON`, or filter with `WHERE`" — rendered when the failing token is `on` and the most recent consumed join is a CROSS join (a precise signature: every other join requires `on`, so `on` is expected there, not a failure). Render-only in format_walker_error; two misfire guards locked (plain join still asks for ON; a stray `on` with no join does not fire). ADR-0042 §3 corrected + Implementation-outcome records the advanced-SQL re-check and the user-confirmed low-priority residual (submit-time expression first-set at non-projection positions, where typing-time completion already offers the right candidates). Full suite green (lib 1578 / it 388 / typing_surface_matrix 192); clippy clean.
This commit is contained in:
@@ -315,12 +315,54 @@ fn is_select_projection_start(expected: &[crate::dsl::walker::outcome::Expectati
|
||||
has_word("distinct") && has_word("all")
|
||||
}
|
||||
|
||||
/// ADR-0042 §3: detect the `… CROSS JOIN <table> on …` mistake. A
|
||||
/// CROSS JOIN takes no `ON` clause; the grammar rejects the `on`,
|
||||
/// but the bare structural error ("expected end of input") doesn't
|
||||
/// teach why. `on` is unexpected at this position *only* when the
|
||||
/// most recent join is a CROSS join — every other join flavour
|
||||
/// requires `on`, so there `on` would be in the expected set, not a
|
||||
/// failure. Detection: the failing token is the keyword `on`, and
|
||||
/// the last `join` word in the consumed prefix is immediately
|
||||
/// preceded by `cross`. Render-only; no grammar change.
|
||||
fn is_cross_join_on(source: &str, position: usize) -> bool {
|
||||
let rest = source[position.min(source.len())..].trim_start();
|
||||
let next_is_on = {
|
||||
let mut chars = rest.chars();
|
||||
let starts_on = rest.len() >= 2 && rest[..2].eq_ignore_ascii_case("on");
|
||||
let boundary = chars
|
||||
.nth(2)
|
||||
.is_none_or(|c| !c.is_ascii_alphanumeric() && c != '_');
|
||||
starts_on && boundary
|
||||
};
|
||||
if !next_is_on {
|
||||
return false;
|
||||
}
|
||||
let consumed = &source[..position.min(source.len())];
|
||||
let words: Vec<&str> = consumed
|
||||
.split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
|
||||
.filter(|w| !w.is_empty())
|
||||
.collect();
|
||||
match words.iter().rposition(|w| w.eq_ignore_ascii_case("join")) {
|
||||
Some(i) if i > 0 => words[i - 1].eq_ignore_ascii_case("cross"),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn format_walker_error(
|
||||
source: &str,
|
||||
position: usize,
|
||||
at_eof: bool,
|
||||
expected: &[crate::dsl::walker::outcome::Expectation],
|
||||
) -> String {
|
||||
if is_cross_join_on(source, position) {
|
||||
let consumed = source[..position.min(source.len())].trim_end();
|
||||
let prefix = if consumed.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("after `{consumed}`, ")
|
||||
};
|
||||
return format!("{prefix}{}", crate::t!("parse.cross_join_no_on"));
|
||||
}
|
||||
let joined = if is_select_projection_start(expected) {
|
||||
crate::t!("parse.expect.select_projection")
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user