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:
claude@clouddev1
2026-06-05 19:02:11 +00:00
parent 1d4923b15b
commit d6e229f0f5
5 changed files with 120 additions and 8 deletions
+42
View File
@@ -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 {
+1
View File
@@ -309,6 +309,7 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
("parse.usage.update", &[]),
("parse.usage.with", &[]),
("parse.expect.select_projection", &[]),
("parse.cross_join_no_on", &[]),
// ---- Project lifecycle event notes ----
("project.export_failed", &["error"]),
("project.export_ok", &["path"]),
+6
View File
@@ -500,6 +500,12 @@ parse:
# completion/hints still expand the full first-set.
expect:
select_projection: "a projection: `*`, a column, or an expression"
# ADR-0042 §3: a CROSS JOIN pairs every row and takes no ON
# clause. The grammar rejects a following `on`; this message
# (rendered in place of the generic structural error when the
# most recent join is a CROSS join and the failing token is `on`)
# teaches the distinction instead of just "expected end of input".
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`"
# Per-command usage templates (ADR-0021 §1). Rendered under a
# "usage:" prefix when a parse fails after consuming a
# known command-entry keyword. The bracket convention `[...]`