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:
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user