walker: F5 — drop preceding-clause keywords from committed-child Incomplete sets
walk_seq's Incomplete arm unconditionally merged the accumulated skipped-Optional expectations (pending_skipped) into the child's expected set. When a child committed terminals before going Incomplete (e.g. `order by` consumed, now awaiting a sort item), this leaked ~13 clause keywords from clauses positioned *before* the committed child — WHERE/GROUP BY/HAVING, the FROM's JOIN options, set-ops — into the ORDER BY completion list, shoving the actual columns off-screen. Merge pending_skipped only when the Incomplete-producing child consumed nothing (path length unchanged): the cursor still sits at the optional boundary, so those optionals are genuine alternatives. A committed child means the cursor is past them. Tests: walker expected-set guard (+ over-correction guard) and a full-stack completion-layer regression test.
This commit is contained in:
@@ -1474,6 +1474,38 @@ mod tests {
|
|||||||
assert!(!cs.contains(&"Stock".to_string()), "got {cs:?}");
|
assert!(!cs.contains(&"Stock".to_string()), "got {cs:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn order_by_completion_omits_preceding_clause_keywords() {
|
||||||
|
use crate::dsl::types::Type;
|
||||||
|
// F5 (handoff 30 §3.3): at `… order by ` the candidate
|
||||||
|
// list is the start of a sort item — the table's columns
|
||||||
|
// plus expression-start keywords. It must NOT be padded
|
||||||
|
// with clause keywords belonging to clauses positioned
|
||||||
|
// *before* ORDER BY (the FROM's JOIN options, WHERE /
|
||||||
|
// GROUP BY / HAVING, set-ops). Those used to shove the
|
||||||
|
// columns off-screen.
|
||||||
|
let cache = schema_with_table(
|
||||||
|
"Things",
|
||||||
|
&[("Name", Type::Text), ("Qty", Type::Int)],
|
||||||
|
);
|
||||||
|
let input = "select Name from Things order by ";
|
||||||
|
let cs = cands_with(input, input.len(), &cache);
|
||||||
|
// The columns the user wants are offered:
|
||||||
|
assert!(cs.contains(&"Name".to_string()), "got {cs:?}");
|
||||||
|
assert!(cs.contains(&"Qty".to_string()), "got {cs:?}");
|
||||||
|
// Preceding-clause keywords must not leak in:
|
||||||
|
for kw in [
|
||||||
|
"where", "group", "having", "join", "union", "intersect",
|
||||||
|
"except", "left", "right", "full", "cross", "inner", "as",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
!cs.contains(&kw.to_string()),
|
||||||
|
"preceding-clause keyword `{kw}` leaked into ORDER BY \
|
||||||
|
completion; got {cs:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn update_where_offers_only_current_table_columns() {
|
fn update_where_offers_only_current_table_columns() {
|
||||||
use crate::dsl::types::Type;
|
use crate::dsl::types::Type;
|
||||||
|
|||||||
@@ -850,6 +850,7 @@ fn walk_seq(
|
|||||||
// engine see optional connectives that haven't been typed.
|
// engine see optional connectives that haven't been typed.
|
||||||
let mut pending_skipped: Vec<Expectation> = Vec::new();
|
let mut pending_skipped: Vec<Expectation> = Vec::new();
|
||||||
for child in children {
|
for child in children {
|
||||||
|
let path_before = path.items.len();
|
||||||
match walk_node(source, cur, child, ctx, path, per_byte) {
|
match walk_node(source, cur, child, ctx, path, per_byte) {
|
||||||
NodeWalkResult::Matched { end, skipped } => {
|
NodeWalkResult::Matched { end, skipped } => {
|
||||||
if end == cur {
|
if end == cur {
|
||||||
@@ -900,9 +901,23 @@ fn walk_seq(
|
|||||||
position,
|
position,
|
||||||
mut expected,
|
mut expected,
|
||||||
} => {
|
} => {
|
||||||
for e in std::mem::take(&mut pending_skipped) {
|
// Only merge the skipped-Optional expectations when
|
||||||
if !expected.contains(&e) {
|
// the Incomplete-producing child consumed nothing
|
||||||
expected.push(e);
|
// (path didn't grow): the cursor still sits at the
|
||||||
|
// optional boundary, so those optionals are genuine
|
||||||
|
// alternatives. If the child committed terminals
|
||||||
|
// (e.g. `order by` consumed, now awaiting a sort
|
||||||
|
// item) the cursor has moved *past* the skipped
|
||||||
|
// optionals — clauses positioned before this child
|
||||||
|
// are no longer valid continuations, so dropping
|
||||||
|
// `pending_skipped` keeps them out of the expected
|
||||||
|
// set (handoff 30 §3.3, F5).
|
||||||
|
let child_consumed = path.items.len() > path_before;
|
||||||
|
if !child_consumed {
|
||||||
|
for e in std::mem::take(&mut pending_skipped) {
|
||||||
|
if !expected.contains(&e) {
|
||||||
|
expected.push(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NodeWalkResult::Incomplete { position, expected };
|
return NodeWalkResult::Incomplete { position, expected };
|
||||||
|
|||||||
@@ -4801,3 +4801,64 @@ mod dispatch_3a_tests {
|
|||||||
assert!(matches!(outcome, WalkOutcome::Match { .. }), "got {outcome:?}");
|
assert!(matches!(outcome, WalkOutcome::Match { .. }), "got {outcome:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod order_by_expected_set_tests {
|
||||||
|
//! F5 (handoff 30 §3.3) — when ORDER BY has consumed `order
|
||||||
|
//! by` and is awaiting a sort item, the expected set must not
|
||||||
|
//! be padded with clause keywords belonging to clauses that
|
||||||
|
//! sit *before* ORDER BY (the FROM's JOIN options, WHERE /
|
||||||
|
//! GROUP BY / HAVING, set-ops). Those optionals were skipped
|
||||||
|
//! earlier in the seq; once ORDER BY commits past them they
|
||||||
|
//! are no longer valid continuations at the cursor.
|
||||||
|
use super::*;
|
||||||
|
use crate::dsl::walker::outcome::Expectation;
|
||||||
|
use crate::mode::Mode;
|
||||||
|
|
||||||
|
fn expected_words(source: &str) -> Vec<&'static str> {
|
||||||
|
expected_at_input_in_mode(source, Mode::Advanced)
|
||||||
|
.iter()
|
||||||
|
.filter_map(|e| match e {
|
||||||
|
Expectation::Word(w) => Some(*w),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn order_by_excludes_preceding_clause_keywords() {
|
||||||
|
let words = expected_words("select Name from T order by ");
|
||||||
|
let preceding_clause_kw = [
|
||||||
|
"where", "group", "having", "join", "union", "intersect",
|
||||||
|
"except", "left", "right", "full", "cross", "inner", "as",
|
||||||
|
];
|
||||||
|
let leaked: Vec<&str> = preceding_clause_kw
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|k| words.contains(k))
|
||||||
|
.collect();
|
||||||
|
assert!(
|
||||||
|
leaked.is_empty(),
|
||||||
|
"ORDER BY expected set leaked preceding-clause keywords \
|
||||||
|
{leaked:?}; full word set: {words:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn order_by_still_offers_a_sort_item() {
|
||||||
|
// Guard against over-correction: the legitimate sort-item
|
||||||
|
// continuation (a column identifier) must survive the
|
||||||
|
// pending-skipped suppression.
|
||||||
|
let expected = expected_at_input_in_mode(
|
||||||
|
"select Name from T order by ",
|
||||||
|
Mode::Advanced,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
expected.iter().any(|e| matches!(
|
||||||
|
e,
|
||||||
|
Expectation::Ident { .. } | Expectation::NumberLit
|
||||||
|
)),
|
||||||
|
"ORDER BY must still offer a sort item; got {expected:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user