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.
130 lines
5.1 KiB
Rust
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"));
|
|
}
|
|
}
|