feat: curated SQL function list — Tab completion (#15) + typing-time typo hint (#16)

Add src/dsl/sql_functions.rs (KNOWN_SQL_FUNCTIONS) as the shared source
of truth at sql_expr_ident slots:

- #15: offer the functions as Tab candidates under a new
  CandidateKind::Function + ninth Theme colour tok_function (blue,
  distinct from keyword/identifier/type).
- #16: restore the column-typo flag the #6 fix had dropped wholesale —
  invalid_ident_at_cursor now bails only when the partial prefix-matches
  a known function, else falls through to the schema-column check.

A column named like a function (e.g. `count`) is deduped (column wins).
`cast` is excluded — CAST(x AS type) is not a plain-call shape.
The no-validation-allowlist posture stands: the list drives completion +
the typo hint only, never parse-time acceptance.

Docs: ADR-0022 Amendment 6, ADR-0031 status note, README index,
requirements I3/I4 + refreshed test baseline.
This commit is contained in:
claude@clouddev1
2026-05-31 11:49:10 +00:00
parent 01ec926ec8
commit 6d8c9eea36
10 changed files with 570 additions and 25 deletions
+1
View File
@@ -14,6 +14,7 @@ pub mod command;
pub mod grammar;
pub mod parser;
pub mod shortid;
pub mod sql_functions;
pub mod types;
pub mod value;
pub mod walker;
+151
View File
@@ -0,0 +1,151 @@
//! Curated set of SQL function names the playground recognises
//! (ADR-0022 Amendment 6, issues #15 / #16).
//!
//! This is the single source of truth for "what SQL function names
//! does this playground know about", shared by two consumers:
//!
//! - **Tab completion** (issue #15): at a `sql_expr_ident` slot the
//! completion engine offers these as `CandidateKind::Function`
//! candidates, so a learner can discover `sum` / `upper` / … without
//! already knowing them.
//! - **The typing-time column-typo hint** (issue #16): at the same
//! slot, `invalid_ident_at_cursor` flags a partial as "no such
//! column" only when it matches neither a schema column *nor* a
//! known function name — so a genuine typo still warns early while
//! a real function name like `sum` does not get mis-flagged.
//!
//! Scope (ADR-0031 §1): the SQL expression grammar admits any
//! function-call *shape* without knowing which names are real — the
//! walker is a structural matcher, not an evaluator. This list is a
//! deliberately *curated pedagogical set*, not "every SQLite
//! built-in". It covers the aggregates a learner meets first plus the
//! common scalar functions, and is the allow-list / discovery source
//! layered on top of that shape-only grammar.
//!
//! Deliberately excluded: `cast`. SQLite's `CAST` uses the
//! `CAST(expr AS type)` syntax, which the expression grammar does
//! **not** parse as a function call (function args are expressions,
//! and `expr AS type` is not one). Offering `cast` as a completion
//! candidate would surface a name that does not parse in the call
//! shape, so it stays out of the set until the grammar grows a
//! dedicated `CAST` form.
/// The curated SQL function names, lowercase and sorted.
///
/// Sorted + lowercase is an invariant (pinned by a unit test): the
/// completion engine relies on a stable order, and the prefix match
/// is case-insensitive against these canonical lowercase spellings.
///
/// Grouping (all plain `name(args)` call shapes):
/// - **Aggregates:** `avg`, `count`, `max`, `min`, `sum`.
/// - **Common scalars:** `abs`, `coalesce`, `length`, `lower`,
/// `round`, `substr`, `trim`, `upper`.
/// - **Broader scalars:** `date`, `datetime`, `hex`, `ifnull`,
/// `instr`, `nullif`, `random`, `replace`, `strftime`, `typeof`.
pub const KNOWN_SQL_FUNCTIONS: &[&str] = &[
"abs",
"avg",
"coalesce",
"count",
"date",
"datetime",
"hex",
"ifnull",
"instr",
"length",
"lower",
"max",
"min",
"nullif",
"random",
"replace",
"round",
"strftime",
"substr",
"sum",
"trim",
"typeof",
"upper",
];
/// Whether `partial` is a case-insensitive prefix of at least one
/// known function name.
///
/// An empty `partial` matches every function (it is a prefix of
/// all), mirroring the empty-prefix behaviour of the keyword /
/// identifier completion sources. Used by `invalid_ident_at_cursor`
/// to decide whether a partial at a `sql_expr_ident` slot might still
/// resolve to a function name (and so must not be flagged as an
/// unknown column).
#[must_use]
pub fn is_known_function_prefix(partial: &str) -> bool {
let lowered = partial.to_lowercase();
KNOWN_SQL_FUNCTIONS
.iter()
.any(|f| f.starts_with(&lowered))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_is_sorted_and_lowercase() {
// The completion engine and prefix matcher both rely on the
// canonical sorted-lowercase invariant.
for f in KNOWN_SQL_FUNCTIONS {
assert_eq!(
*f,
f.to_lowercase(),
"function name `{f}` must be lowercase",
);
}
let mut sorted = KNOWN_SQL_FUNCTIONS.to_vec();
sorted.sort_unstable();
assert_eq!(
sorted.as_slice(),
KNOWN_SQL_FUNCTIONS,
"KNOWN_SQL_FUNCTIONS must be declared in sorted order",
);
}
#[test]
fn list_has_no_duplicates() {
let mut seen = std::collections::HashSet::new();
for f in KNOWN_SQL_FUNCTIONS {
assert!(seen.insert(*f), "duplicate function name `{f}`");
}
}
#[test]
fn cast_is_excluded() {
// `CAST(expr AS type)` is not a plain call shape the grammar
// parses — it must not be offered as a candidate (faithfulness
// to ADR-0031's call-shape grammar).
assert!(
!KNOWN_SQL_FUNCTIONS.contains(&"cast"),
"`cast` is not a plain-call function and must stay out of the set",
);
}
#[test]
fn prefix_match_is_case_insensitive() {
assert!(is_known_function_prefix("su")); // sum, substr
assert!(is_known_function_prefix("SU")); // case-folded
assert!(is_known_function_prefix("sum")); // exact
assert!(is_known_function_prefix("UPP")); // upper
}
#[test]
fn empty_prefix_matches_all() {
assert!(is_known_function_prefix(""));
}
#[test]
fn unknown_prefix_does_not_match() {
assert!(!is_known_function_prefix("zzz"));
assert!(!is_known_function_prefix("xqz"));
// A genuine column-typo shape that prefixes no function.
assert!(!is_known_function_prefix("Agx"));
}
}