use crate::errors::ExecutorError;
pub(crate) mod coercion;
mod control;
mod conversion;
pub(crate) mod datetime;
mod null_handling;
mod numeric;
#[cfg(feature = "spatial")]
pub(crate) mod spatial;
mod sqlite_compat;
mod storage;
pub(crate) mod string;
mod system;
mod vector;
pub(super) fn eval_scalar_function(
name: &str,
args: &[vibesql_types::SqlValue],
character_unit: &Option<vibesql_ast::CharacterUnit>,
sql_mode: &vibesql_types::SqlMode,
schema_context: super::SchemaExprContext,
) -> Result<vibesql_types::SqlValue, ExecutorError> {
match name.to_uppercase().as_str() {
"COALESCE" => null_handling::coalesce(args),
"NULLIF" => null_handling::nullif(args),
"UPPER" => string::upper(args),
"LOWER" => string::lower(args),
"SUBSTRING" => string::substring(args),
"SUBSTR" => string::substring(args), "TRIM" => string::trim(args),
"LTRIM" => string::ltrim(args),
"RTRIM" => string::rtrim(args),
"CHAR_LENGTH" | "CHARACTER_LENGTH" => string::char_length(args, name, character_unit),
"OCTET_LENGTH" => string::octet_length(args),
"CONCAT" => string::concat(args, sql_mode),
"LENGTH" => string::length(args),
"POSITION" => string::position(args),
"REPLACE" => string::replace(args),
"REVERSE" => string::reverse(args),
"LEFT" => string::left(args),
"RIGHT" => string::right(args),
"INSTR" => string::instr(args),
"LOCATE" => string::locate(args),
"ABS" => numeric::abs(args),
"ROUND" => numeric::round(args),
"TRUNCATE" | "TRUNC" => numeric::truncate(args),
"FLOOR" => numeric::floor(args),
"CEIL" | "CEILING" => numeric::ceil(args),
"MOD" => numeric::mod_func(args),
"POWER" | "POW" => numeric::power(args),
"SQRT" => numeric::sqrt(args),
"EXP" => numeric::exp(args),
"LN" => numeric::ln(args),
"LOG" => numeric::log(args),
"LOG10" => numeric::log10(args),
"SIGN" => numeric::sign(args),
"PI" => numeric::pi(args),
"SIN" => numeric::sin(args),
"COS" => numeric::cos(args),
"TAN" => numeric::tan(args),
"ASIN" => numeric::asin(args),
"ACOS" => numeric::acos(args),
"ATAN" => numeric::atan(args),
"ATAN2" => numeric::atan2(args),
"RADIANS" => numeric::radians(args),
"DEGREES" => numeric::degrees(args),
"GREATEST" => numeric::greatest(args),
"LEAST" => numeric::least(args),
"MIN" => numeric::scalar_min(args),
"MAX" => numeric::scalar_max(args),
"FORMAT" => match args.first() {
Some(
vibesql_types::SqlValue::Varchar(_)
| vibesql_types::SqlValue::Character(_)
| vibesql_types::SqlValue::Null,
) => sqlite_compat::printf(args),
_ => numeric::format(args),
},
"COSINE_DISTANCE" => vector::cosine_distance(args),
"L2_DISTANCE" => vector::l2_distance(args),
"INNER_PRODUCT" => vector::inner_product(args),
"VECTOR_NORM" => vector::vector_norm(args),
"VECTOR_DIMS" => vector::vector_dims(args),
"CURRENT_DATE" | "CURDATE" => datetime::current_date(args),
"CURRENT_TIME" | "CURTIME" => datetime::current_time(args),
"CURRENT_TIMESTAMP" | "NOW" => datetime::current_timestamp(args),
"DATETIME" => datetime::datetime(args, schema_context),
"DATE" => datetime::date(args, schema_context),
"TIME" => datetime::time(args, schema_context),
"STRFTIME" => datetime::strftime(args, schema_context),
"JULIANDAY" => datetime::julianday(args, schema_context),
"UNIXEPOCH" => datetime::unixepoch(args, schema_context),
"TIMEDIFF" => datetime::timediff(args, schema_context),
"YEAR" => datetime::year(args),
"MONTH" => datetime::month(args),
"DAY" => datetime::day(args),
"HOUR" => datetime::hour(args),
"MINUTE" => datetime::minute(args),
"SECOND" => datetime::second(args),
"DATEDIFF" => datetime::datediff(args),
"DATE_ADD" | "ADDDATE" => datetime::date_add(args),
"DATE_SUB" | "SUBDATE" => datetime::date_sub(args),
"EXTRACT" => datetime::extract(args),
"AGE" => datetime::age(args),
"IF" => control::if_func(args),
"IIF" => sqlite_compat::iif(args),
"TYPEOF" => sqlite_compat::typeof_func(args),
"LIKELY" => sqlite_compat::likely(args),
"UNLIKELY" => sqlite_compat::unlikely(args),
"LIKELIHOOD" => sqlite_compat::likelihood(args),
"IFNULL" => sqlite_compat::ifnull(args),
"RANDOM" => sqlite_compat::random(args),
"RANDOMBLOB" => sqlite_compat::randomblob(args),
"HEX" => sqlite_compat::hex(args),
"UNHEX" => sqlite_compat::unhex(args),
"ZEROBLOB" => sqlite_compat::zeroblob(args),
"UNICODE" => sqlite_compat::unicode(args),
"CHAR" => sqlite_compat::char_func(args),
"PRINTF" => sqlite_compat::printf(args),
"UNISTR" => sqlite_compat::unistr(args),
"TOREAL" => sqlite_compat::toreal(args),
"TOINTEGER" => sqlite_compat::tointeger(args),
"CONCAT_WS" => sqlite_compat::concat_ws(args),
"QUOTE" => sqlite_compat::quote(args),
"INTREAL" => sqlite_compat::intreal(args),
"GLOB" => sqlite_compat::glob(args),
"LIKE" => sqlite_compat::like(args),
"JSON" => sqlite_compat::json(args),
"TO_NUMBER" => conversion::to_number(args),
"TO_DATE" => conversion::to_date(args),
"TO_TIMESTAMP" => conversion::to_timestamp(args),
"TO_CHAR" => conversion::to_char(args),
"CAST" => conversion::cast(args, sql_mode),
"VERSION" => system::version(args),
"DATABASE" | "SCHEMA" => system::database(args, name),
"USER" | "CURRENT_USER" => system::user(args, name),
"STORAGE_URL" => storage::eval_storage_url(args),
"STORAGE_SIZE" => storage::eval_storage_size(args),
#[cfg(feature = "spatial")]
"ST_GEOMFROMTEXT" | "ST_GEOM_FROM_TEXT" => spatial::constructors::st_geom_from_text(args),
#[cfg(feature = "spatial")]
"ST_POINTFROMTEXT" | "ST_POINT_FROM_TEXT" => {
spatial::constructors::st_point_from_text(args)
}
#[cfg(feature = "spatial")]
"ST_LINEFROMTEXT" | "ST_LINE_FROM_TEXT" => spatial::constructors::st_line_from_text(args),
#[cfg(feature = "spatial")]
"ST_POLYGONFROMTEXT" | "ST_POLYGON_FROM_TEXT" => {
spatial::constructors::st_polygon_from_text(args)
}
#[cfg(feature = "spatial")]
"ST_GEOMFROMWKB" | "ST_GEOM_FROM_WKB" => spatial::constructors::st_geom_from_wkb(args),
#[cfg(feature = "spatial")]
"ST_POINTFROMWKB" | "ST_POINT_FROM_WKB" => spatial::constructors::st_point_from_wkb(args),
#[cfg(feature = "spatial")]
"ST_LINEFROMWKB" | "ST_LINE_FROM_WKB" => spatial::constructors::st_line_from_wkb(args),
#[cfg(feature = "spatial")]
"ST_POLYGONFROMWKB" | "ST_POLYGON_FROM_WKB" => {
spatial::constructors::st_polygon_from_wkb(args)
}
#[cfg(feature = "spatial")]
"ST_X" => spatial::accessors::st_x(args),
#[cfg(feature = "spatial")]
"ST_Y" => spatial::accessors::st_y(args),
#[cfg(feature = "spatial")]
"ST_GEOMETRYTYPE" | "ST_GEOMETRY_TYPE" => spatial::accessors::st_geometry_type(args),
#[cfg(feature = "spatial")]
"ST_DIMENSION" => spatial::accessors::st_dimension(args),
#[cfg(feature = "spatial")]
"ST_ASTEXT" | "ST_AS_TEXT" => spatial::accessors::st_as_text(args),
#[cfg(feature = "spatial")]
"ST_ASBINARY" | "ST_AS_BINARY" => spatial::accessors::st_as_binary(args),
#[cfg(feature = "spatial")]
"ST_ASGEOJSON" | "ST_AS_GEOJSON" => spatial::accessors::st_as_geojson(args),
#[cfg(feature = "spatial")]
"ST_SETSRID" | "ST_SET_SRID" => spatial::srid::st_set_srid(args),
#[cfg(feature = "spatial")]
"ST_SRID" => spatial::srid::st_srid(args),
#[cfg(feature = "spatial")]
"ST_CONTAINS" => spatial::predicates::st_contains(args),
#[cfg(feature = "spatial")]
"ST_WITHIN" => spatial::predicates::st_within(args),
#[cfg(feature = "spatial")]
"ST_INTERSECTS" => spatial::predicates::st_intersects(args),
#[cfg(feature = "spatial")]
"ST_DISJOINT" => spatial::predicates::st_disjoint(args),
#[cfg(feature = "spatial")]
"ST_EQUALS" => spatial::predicates::st_equals(args),
#[cfg(feature = "spatial")]
"ST_TOUCHES" => spatial::predicates::st_touches(args),
#[cfg(feature = "spatial")]
"ST_CROSSES" => spatial::predicates::st_crosses(args),
#[cfg(feature = "spatial")]
"ST_OVERLAPS" => spatial::predicates::st_overlaps(args),
#[cfg(feature = "spatial")]
"ST_COVERS" => spatial::predicates::st_covers(args),
#[cfg(feature = "spatial")]
"ST_COVEREDBY" | "ST_COVERED_BY" => spatial::predicates::st_coveredby(args),
#[cfg(feature = "spatial")]
"ST_DWITHIN" | "ST_D_WITHIN" => spatial::predicates::st_dwithin(args),
#[cfg(feature = "spatial")]
"ST_RELATE" => spatial::predicates::st_relate(args),
#[cfg(feature = "spatial")]
"ST_DISTANCE" => spatial::measurements::st_distance(args),
#[cfg(feature = "spatial")]
"ST_LENGTH" => spatial::measurements::st_length(args),
#[cfg(feature = "spatial")]
"ST_PERIMETER" => spatial::measurements::st_perimeter(args),
#[cfg(feature = "spatial")]
"ST_AREA" => spatial::measurements::st_area(args),
#[cfg(feature = "spatial")]
"ST_CENTROID" => spatial::measurements::st_centroid(args),
#[cfg(feature = "spatial")]
"ST_ENVELOPE" => spatial::measurements::st_envelope(args),
#[cfg(feature = "spatial")]
"ST_CONVEXHULL" | "ST_CONVEX_HULL" => spatial::measurements::st_convex_hull(args),
#[cfg(feature = "spatial")]
"ST_POINTONSURFACE" | "ST_POINT_ON_SURFACE" => {
spatial::measurements::st_point_on_surface(args)
}
#[cfg(feature = "spatial")]
"ST_BOUNDARY" => spatial::measurements::st_boundary(args),
#[cfg(feature = "spatial")]
"ST_HAUSDORFFDISTANCE" | "ST_HAUSDORFF_DISTANCE" => {
spatial::measurements::st_hausdorff_distance(args)
}
#[cfg(feature = "spatial")]
"ST_SIMPLIFY" => spatial::operations::st_simplify(args),
#[cfg(feature = "spatial")]
"ST_UNION" => spatial::operations::st_union(args),
#[cfg(feature = "spatial")]
"ST_INTERSECTION" => spatial::operations::st_intersection(args),
#[cfg(feature = "spatial")]
"ST_DIFFERENCE" => spatial::operations::st_difference(args),
#[cfg(feature = "spatial")]
"ST_SYMDIFFERENCE" | "ST_SYM_DIFFERENCE" => spatial::operations::st_sym_difference(args),
_ => Err(ExecutorError::NoSuchFunction { function_name: name.to_string() }),
}
}
#[cfg(test)]
mod dispatch_volatility {
use vibesql_ast::volatility;
const DISPATCH_SRC: &str = include_str!("mod.rs");
const VOLATILE_HANDLERS: &[(&str, Category)] = &[
("datetime::current_date(", Category::Clock),
("datetime::current_time(", Category::Clock),
("datetime::current_timestamp(", Category::Clock),
("datetime::datetime(", Category::DatetimeFamily),
("datetime::date(", Category::DatetimeFamily),
("datetime::time(", Category::DatetimeFamily),
("datetime::strftime(", Category::DatetimeFamily),
("datetime::julianday(", Category::DatetimeFamily),
("datetime::unixepoch(", Category::DatetimeFamily),
("datetime::timediff(", Category::DatetimeFamily),
("datetime::age(", Category::DatetimeFamily),
("sqlite_compat::random(", Category::Random),
("sqlite_compat::randomblob(", Category::Random),
("system::version(", Category::Session),
("system::database(", Category::Session),
("system::user(", Category::Session),
];
#[derive(Debug, Clone, Copy, PartialEq)]
enum Category {
Clock,
DatetimeFamily,
Random,
Session,
}
impl Category {
fn classifies(self, canonical_name: &str) -> bool {
match self {
Category::Clock => volatility::is_clock_function(canonical_name),
Category::DatetimeFamily => volatility::is_datetime_family_function(canonical_name),
Category::Random => volatility::is_random_function(canonical_name),
Category::Session => volatility::is_session_state_function(canonical_name),
}
}
}
fn dispatch_arms() -> Vec<(Vec<String>, String)> {
let dispatch_only =
DISPATCH_SRC.split("#[cfg(test)]").next().expect("split always yields a first part");
let mut arms: Vec<(Vec<String>, String)> = Vec::new();
for line in dispatch_only.lines() {
if let Some(arrow) = line.find("=>") {
let names = quoted_strings(&line[..arrow]);
let handler = line[arrow + 2..].to_string();
arms.push((names, handler));
} else if let Some((_, handler)) = arms.last_mut() {
handler.push('\n');
handler.push_str(line);
}
}
arms
}
fn quoted_strings(text: &str) -> Vec<String> {
let mut names = Vec::new();
let mut rest = text;
while let Some(start) = rest.find('"') {
let Some(len) = rest[start + 1..].find('"') else { break };
names.push(rest[start + 1..start + 1 + len].to_string());
rest = &rest[start + 1 + len + 1..];
}
names
}
#[test]
fn every_volatile_dispatch_name_is_classified_in_vibesql_ast_volatility() {
let mut seen: Vec<(Category, String)> = Vec::new();
for (names, handler) in dispatch_arms() {
for &(marker, category) in VOLATILE_HANDLERS {
if !handler.contains(marker) {
continue;
}
assert!(
!names.is_empty(),
"volatile handler {marker} dispatched from an arm without name patterns"
);
for name in &names {
let canonical = name.to_lowercase();
assert!(
category.classifies(&canonical),
"executor dispatches '{name}' to volatile handler {marker}, but \
vibesql_ast::volatility does not classify '{canonical}' as \
{category:?} — add it to the classification (replication freeze \
and optimizer pushdown depend on it, #5377/#5382)"
);
assert!(
volatility::is_volatile_function(&canonical),
"'{canonical}' must be in the coarse volatile union"
);
seen.push((category, canonical));
}
}
}
for (category, name) in [
(Category::Clock, "current_date"),
(Category::Clock, "curdate"),
(Category::Clock, "current_time"),
(Category::Clock, "curtime"),
(Category::Clock, "current_timestamp"),
(Category::Clock, "now"),
(Category::DatetimeFamily, "age"),
(Category::Random, "random"),
(Category::Random, "randomblob"),
(Category::Session, "user"),
(Category::Session, "current_user"),
(Category::Session, "database"),
(Category::Session, "schema"),
(Category::Session, "version"),
] {
assert!(
seen.contains(&(category, name.to_string())),
"expected to find '{name}' dispatched to a {category:?} handler — did the \
dispatch-source scan break?"
);
}
}
}