Files
rdbms-playground/src/dsl/sql_functions.rs
T
claude@clouddev1 41b7e9a049 style: format the whole tree with cargo fmt (stock defaults, #35)
One-time, mechanical reformat — no functional changes. The tree was not
rustfmt-clean (~1800 hunks across ~100 files); this brings it to stock
`cargo fmt` defaults so a `cargo fmt --check` CI gate can follow.
Behaviour-preserving: 2509 pass / 0 fail / 1 ignored (unchanged baseline),
clippy clean. A .git-blame-ignore-revs entry follows so `git blame`
skips this commit.
2026-06-17 21:39:19 +00:00

130 lines
5.1 KiB
Rust

//! 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"));
}
}