use rudb_common::{Error, LogicalType, MAX_DECIMAL_WIDTH, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FunctionKind {
Scalar,
Aggregate,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Resolved {
pub name: &'static str,
pub kind: FunctionKind,
pub arguments: Vec<LogicalType>,
pub returns: LogicalType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Shape {
Promoted,
Multiplied,
Divided,
PromotedWithCarry,
PromotedTo(Fixed),
FixedTo(Fixed, Fixed),
Text(Fixed),
AnyTo(Fixed),
LeadingFixedTo(usize, Fixed, Fixed),
LeadingFixedToLast(Fixed),
Accumulated,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Fixed {
Boolean,
Integer,
BigInt,
Double,
Varchar,
Date,
Timestamp,
}
impl Fixed {
fn ty(self) -> LogicalType {
match self {
Self::Boolean => LogicalType::Boolean,
Self::Integer => LogicalType::Integer,
Self::BigInt => LogicalType::BigInt,
Self::Double => LogicalType::Double,
Self::Varchar => LogicalType::Varchar,
Self::Date => LogicalType::Date,
Self::Timestamp => LogicalType::Timestamp,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Arity {
Exactly(usize),
Between(usize, Option<usize>),
OneOf(&'static [usize]),
}
impl Arity {
const fn exactly(count: usize) -> Self {
Self::Exactly(count)
}
const fn between(least: usize, most: usize) -> Self {
Self::Between(least, Some(most))
}
const fn at_least(least: usize) -> Self {
Self::Between(least, None)
}
const fn one_of(counts: &'static [usize]) -> Self {
Self::OneOf(counts)
}
fn accepts(self, count: usize) -> bool {
match self {
Self::Exactly(wanted) => count == wanted,
Self::Between(least, most) => count >= least && most.is_none_or(|most| count <= most),
Self::OneOf(counts) => counts.contains(&count),
}
}
#[cfg(test)]
fn counts(self) -> Vec<usize> {
match self {
Self::Exactly(count) => vec![count],
Self::Between(least, most) => (least..=most.unwrap_or(least + 1)).collect(),
Self::OneOf(counts) => counts.to_vec(),
}
}
#[cfg(test)]
fn least(self) -> usize {
match self {
Self::Exactly(count) | Self::Between(count, _) => count,
Self::OneOf(counts) => counts.iter().copied().min().unwrap_or(0),
}
}
}
struct Entry {
name: &'static str,
kind: FunctionKind,
arity: Arity,
shape: Shape,
numeric_only: bool,
}
const TABLE: &[Entry] = &[
number("+", Arity::between(1, 2), Shape::PromotedWithCarry),
number("-", Arity::between(1, 2), Shape::PromotedWithCarry),
number("*", Arity::exactly(2), Shape::Multiplied),
number("%", Arity::exactly(2), Shape::Promoted),
number("/", Arity::exactly(2), Shape::PromotedTo(Fixed::Double)),
number("//", Arity::exactly(2), Shape::Divided),
number("abs", Arity::exactly(1), Shape::Promoted),
Entry {
name: "||",
kind: FunctionKind::Scalar,
arity: Arity::exactly(2),
shape: Shape::FixedTo(Fixed::Varchar, Fixed::Varchar),
numeric_only: false,
},
text("lower", Arity::exactly(1), Fixed::Varchar),
text("upper", Arity::exactly(1), Fixed::Varchar),
text("length", Arity::exactly(1), Fixed::BigInt),
text("strlen", Arity::exactly(1), Fixed::BigInt),
text("~~", Arity::exactly(2), Fixed::Boolean),
text("!~~", Arity::exactly(2), Fixed::Boolean),
text("~~*", Arity::exactly(2), Fixed::Boolean),
text("!~~*", Arity::exactly(2), Fixed::Boolean),
Entry {
name: "not",
kind: FunctionKind::Scalar,
arity: Arity::exactly(1),
shape: Shape::FixedTo(Fixed::Boolean, Fixed::Boolean),
numeric_only: false,
},
Entry {
name: "coalesce",
kind: FunctionKind::Scalar,
arity: Arity::at_least(1),
shape: Shape::Promoted,
numeric_only: false,
},
Entry {
name: "date_part",
kind: FunctionKind::Scalar,
arity: Arity::exactly(2),
shape: Shape::LeadingFixedTo(1, Fixed::Varchar, Fixed::BigInt),
numeric_only: false,
},
Entry {
name: "date_trunc",
kind: FunctionKind::Scalar,
arity: Arity::exactly(2),
shape: Shape::LeadingFixedToLast(Fixed::Varchar),
numeric_only: false,
},
Entry {
name: "make_date",
kind: FunctionKind::Scalar,
arity: Arity::one_of(&[1, 3]),
shape: Shape::FixedTo(Fixed::Integer, Fixed::Date),
numeric_only: true,
},
Entry {
name: "epoch_ms",
kind: FunctionKind::Scalar,
arity: Arity::exactly(1),
shape: Shape::FixedTo(Fixed::BigInt, Fixed::Timestamp),
numeric_only: true,
},
text("regexp_replace", Arity::between(3, 4), Fixed::Varchar),
text("regexp_matches", Arity::between(2, 3), Fixed::Boolean),
text("regexp_full_match", Arity::between(2, 3), Fixed::Boolean),
Entry {
name: "regexp_extract",
kind: FunctionKind::Scalar,
arity: Arity::between(2, 4),
shape: Shape::LeadingFixedTo(2, Fixed::Varchar, Fixed::Varchar),
numeric_only: false,
},
aggregate("count_star", Arity::exactly(0), Shape::AnyTo(Fixed::BigInt), false),
aggregate("count", Arity::exactly(1), Shape::AnyTo(Fixed::BigInt), false),
aggregate("sum", Arity::exactly(1), Shape::Accumulated, true),
aggregate("avg", Arity::exactly(1), Shape::PromotedTo(Fixed::Double), true),
aggregate("min", Arity::exactly(1), Shape::Promoted, false),
aggregate("max", Arity::exactly(1), Shape::Promoted, false),
];
const fn number(name: &'static str, arity: Arity, shape: Shape) -> Entry {
Entry { name, kind: FunctionKind::Scalar, arity, shape, numeric_only: true }
}
const fn text(name: &'static str, arity: Arity, returns: Fixed) -> Entry {
Entry {
name,
kind: FunctionKind::Scalar,
arity,
shape: Shape::Text(returns),
numeric_only: false,
}
}
const fn aggregate(name: &'static str, arity: Arity, shape: Shape, numeric_only: bool) -> Entry {
Entry { name, kind: FunctionKind::Aggregate, arity, shape, numeric_only }
}
#[must_use]
pub fn kind_of(name: &str) -> Option<FunctionKind> {
find(name).map(|entry| entry.kind)
}
pub fn resolve(name: &str, arguments: &[LogicalType]) -> Result<Resolved> {
let entry = find(name).ok_or_else(|| {
Error::catalog(format!("Scalar Function with name {name} does not exist!"))
})?;
if !entry.arity.accepts(arguments.len()) {
return Err(no_match(entry.name, arguments));
}
if entry.numeric_only {
for ty in arguments {
if !ty.is_numeric() && *ty != LogicalType::Null {
return Err(Error::binder(format!(
"No function matches the given name and argument types '{name}({ty})'. You might need to add explicit type casts."
)));
}
}
}
let (cast_to, returns) = match entry.shape {
Shape::Promoted => {
let common = promote_all(name, arguments)?;
(vec![common.clone(); arguments.len()], common)
}
Shape::Multiplied => {
let common = promote_all(name, arguments)?;
match product(arguments)? {
Some(LogicalType::Decimal { width, scale }) => {
let cast_to = arguments
.iter()
.map(|ty| match ty.decimal_shape() {
Some((_, held)) => LogicalType::Decimal { width, scale: held },
None => ty.clone(),
})
.collect();
(cast_to, LogicalType::Decimal { width, scale })
}
_ => (vec![common.clone(); arguments.len()], common),
}
}
Shape::Divided => {
let common = promote_all(name, arguments)?;
let returns = match common {
LogicalType::Decimal { .. } => LogicalType::Double,
other => other,
};
(vec![returns.clone(); arguments.len()], returns)
}
Shape::PromotedWithCarry => {
let common = promote_all(name, arguments)?;
let returns = if arguments.len() > 1 { carrying(common) } else { common };
(vec![returns.clone(); arguments.len()], returns)
}
Shape::PromotedTo(fixed) => {
let common = promote_all(name, arguments)?;
(vec![common; arguments.len()], fixed.ty())
}
Shape::FixedTo(argument, result) => (vec![argument.ty(); arguments.len()], result.ty()),
Shape::Text(result) => {
for ty in arguments {
if *ty != LogicalType::Varchar && *ty != LogicalType::Null {
return Err(no_match(entry.name, arguments));
}
}
(vec![LogicalType::Varchar; arguments.len()], result.ty())
}
Shape::AnyTo(result) => (arguments.to_vec(), result.ty()),
Shape::LeadingFixedTo(count, first, result) => {
(leading(count, first, arguments), result.ty())
}
Shape::LeadingFixedToLast(first) => {
let last = match arguments.last() {
Some(LogicalType::Null) | None => LogicalType::Timestamp,
Some(ty) => ty.clone(),
};
(leading(1, first, arguments), last)
}
Shape::Accumulated => {
let common = promote_all(name, arguments)?;
let returns = accumulator(&common);
(vec![common; arguments.len()], returns)
}
};
Ok(Resolved { name: entry.name, kind: entry.kind, arguments: cast_to, returns })
}
fn no_match(name: &str, arguments: &[LogicalType]) -> Error {
let types = arguments.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ");
let mut message = format!(
"No function matches the given name and argument types '{name}({types})'. You might need to add explicit type casts."
);
if let Some((_, overloads)) = CANDIDATES.iter().find(|(entry, _)| *entry == name) {
message.push_str("\n\tCandidate functions:");
for overload in *overloads {
message.push_str("\n\t");
message.push_str(overload);
}
message.push('\n');
}
Error::binder(message)
}
const CANDIDATES: &[(&str, &[&str])] = &[
("lower", &["lower(col0 VARCHAR) -> VARCHAR"]),
("upper", &["upper(col0 VARCHAR) -> VARCHAR"]),
(
"length",
&[
"length(col0 VARCHAR) -> BIGINT",
"length(col0 BIT) -> BIGINT",
"length(col0 ANY[]) -> BIGINT",
],
),
("strlen", &["strlen(col0 VARCHAR) -> BIGINT"]),
("~~", &["\"~~\"(col0 VARCHAR, col1 VARCHAR) -> BOOLEAN"]),
("!~~", &["\"!~~\"(col0 VARCHAR, col1 VARCHAR) -> BOOLEAN"]),
("~~*", &["\"~~*\"(col0 VARCHAR, col1 VARCHAR) -> BOOLEAN"]),
("!~~*", &["\"!~~*\"(col0 VARCHAR, col1 VARCHAR) -> BOOLEAN"]),
(
"regexp_replace",
&[
"regexp_replace(string VARCHAR, regex VARCHAR, replacement VARCHAR) -> VARCHAR",
"regexp_replace(string VARCHAR, regex VARCHAR, replacement VARCHAR, \"options\" VARCHAR) -> VARCHAR",
],
),
(
"regexp_matches",
&[
"regexp_matches(string VARCHAR, regex VARCHAR) -> BOOLEAN",
"regexp_matches(string VARCHAR, regex VARCHAR, \"options\" VARCHAR) -> BOOLEAN",
],
),
(
"regexp_full_match",
&[
"regexp_full_match(string VARCHAR, regex VARCHAR) -> BOOLEAN",
"regexp_full_match(string VARCHAR, regex VARCHAR, \"options\" VARCHAR) -> BOOLEAN",
],
),
];
fn leading(count: usize, first: Fixed, arguments: &[LogicalType]) -> Vec<LogicalType> {
let mut cast_to = arguments.to_vec();
for head in cast_to.iter_mut().take(count) {
*head = first.ty();
}
cast_to
}
fn product(arguments: &[LogicalType]) -> Result<Option<LogicalType>> {
let mut decimals = false;
let (mut width, mut scale, mut widest) = (0u8, 0u8, 0u8);
for ty in arguments {
decimals |= matches!(ty, LogicalType::Decimal { .. });
let Some((one, held)) = ty.decimal_shape() else { return Ok(None) };
width = width.saturating_add(one);
scale = scale.saturating_add(held);
widest = widest.max(one);
}
if !decimals {
return Ok(None);
}
if scale > MAX_DECIMAL_WIDTH {
return Err(Error::out_of_range(format!(
"Needed scale {scale} to accurately represent the multiplication result, but this is out of range of the DECIMAL type. Max scale is {MAX_DECIMAL_WIDTH}; could not perform an accurate multiplication. Either add a cast to DOUBLE, or add an explicit cast to a decimal with a lower scale."
)));
}
if widest <= WIDEST_SIXTY_FOUR_BIT
&& width > WIDEST_SIXTY_FOUR_BIT
&& scale < WIDEST_SIXTY_FOUR_BIT
{
width = WIDEST_SIXTY_FOUR_BIT;
}
Ok(Some(LogicalType::Decimal { width: width.min(MAX_DECIMAL_WIDTH), scale }))
}
const WIDEST_SIXTY_FOUR_BIT: u8 = 18;
fn carrying(common: LogicalType) -> LogicalType {
match common {
LogicalType::Decimal { width, scale } if width < MAX_DECIMAL_WIDTH => {
LogicalType::Decimal { width: width + 1, scale }
}
other => other,
}
}
fn accumulator(ty: &LogicalType) -> LogicalType {
if ty.is_integer() {
LogicalType::HugeInt
} else if *ty == LogicalType::Float {
LogicalType::Double
} else {
ty.clone()
}
}
fn promote_all(name: &str, arguments: &[LogicalType]) -> Result<LogicalType> {
let mut common = match arguments.first() {
Some(first) => first.clone(),
None => {
return Err(Error::internal(format!("{name} promotes over no arguments")));
}
};
for ty in &arguments[1..] {
common = common.promote(ty).ok_or_else(|| {
Error::binder(format!(
"No function matches the given name and argument types '{name}({})'. You might need to add explicit type casts.",
arguments.iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
))
})?;
}
if common == LogicalType::Null {
common = LogicalType::Integer;
}
Ok(common)
}
fn find(name: &str) -> Option<&'static Entry> {
let name = canonical(name);
TABLE.iter().find(|entry| entry.name.eq_ignore_ascii_case(name))
}
fn canonical(name: &str) -> &str {
ALIASES
.iter()
.find(|(alias, _)| alias.eq_ignore_ascii_case(name))
.map_or(name, |(_, real)| *real)
}
const ALIASES: &[(&str, &str)] = &[
("len", "length"),
("char_length", "length"),
("character_length", "length"),
("lcase", "lower"),
("ucase", "upper"),
("mean", "avg"),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn arithmetic_returns_what_its_operands_promote_to() {
let resolved = resolve("+", &[LogicalType::Integer, LogicalType::BigInt])
.expect("an integer and a bigint add");
assert_eq!(resolved.returns, LogicalType::BigInt);
assert_eq!(resolved.arguments, vec![LogicalType::BigInt, LogicalType::BigInt]);
}
#[test]
fn a_decimal_sum_is_a_digit_wider_than_what_its_operands_promote_to() {
let decimal = |width, scale| LogicalType::Decimal { width, scale };
let sum = |left: LogicalType, right: LogicalType| {
resolve("+", &[left, right]).expect("adds").returns
};
assert_eq!(sum(decimal(18, 0), decimal(18, 0)), decimal(19, 0));
assert_eq!(sum(decimal(2, 1), LogicalType::Integer), decimal(12, 1));
assert_eq!(sum(decimal(18, 0), decimal(4, 2)), decimal(21, 2));
assert_eq!(sum(decimal(4, 2), LogicalType::BigInt), decimal(22, 2));
assert_eq!(sum(decimal(4, 2), LogicalType::UBigInt), decimal(23, 2));
assert_eq!(sum(decimal(4, 2), LogicalType::HugeInt), decimal(38, 2));
let resolved = resolve("-", &[decimal(18, 0), decimal(18, 0)]).expect("subtracts");
assert_eq!(resolved.arguments, vec![decimal(19, 0), decimal(19, 0)]);
}
#[test]
fn a_decimal_sum_at_the_widest_decimal_stays_there() {
let widest = LogicalType::Decimal { width: MAX_DECIMAL_WIDTH, scale: 0 };
let resolved = resolve("+", &[widest.clone(), widest.clone()]).expect("adds");
assert_eq!(resolved.returns, widest);
}
#[test]
fn nothing_but_a_two_sided_addition_gains_a_digit() {
let decimal = |width, scale| LogicalType::Decimal { width, scale };
assert_eq!(resolve("-", &[decimal(4, 2)]).expect("negates").returns, decimal(4, 2));
assert_eq!(resolve("+", &[decimal(4, 2)]).expect("is unary plus").returns, decimal(4, 2));
assert_eq!(resolve("abs", &[decimal(4, 2)]).expect("has a size").returns, decimal(4, 2));
assert_eq!(
resolve("%", &[decimal(4, 2), LogicalType::Integer]).expect("divides").returns,
decimal(12, 2)
);
}
#[test]
fn a_decimal_product_is_as_wide_as_both_of_its_operands_together() {
let decimal = |width, scale| LogicalType::Decimal { width, scale };
let times = |left: LogicalType, right: LogicalType| {
resolve("*", &[left, right]).expect("multiplies").returns
};
assert_eq!(times(decimal(4, 2), decimal(4, 2)), decimal(8, 4));
assert_eq!(times(decimal(4, 2), LogicalType::BigInt), decimal(23, 2));
assert_eq!(times(decimal(18, 3), LogicalType::Integer), decimal(18, 3));
assert_eq!(times(decimal(12, 6), decimal(12, 6)), decimal(18, 12));
assert_eq!(times(decimal(10, 9), decimal(10, 9)), decimal(20, 18));
assert_eq!(times(decimal(18, 17), decimal(18, 17)), decimal(36, 34));
assert_eq!(times(decimal(20, 10), decimal(20, 10)), decimal(38, 20));
assert_eq!(times(LogicalType::Integer, LogicalType::Integer), LogicalType::Integer);
}
#[test]
fn a_decimal_product_casts_its_operands_to_the_width_of_the_answer() {
let decimal = |width, scale| LogicalType::Decimal { width, scale };
let resolved = resolve("*", &[decimal(4, 2), LogicalType::BigInt]).expect("multiplies");
assert_eq!(resolved.arguments, vec![decimal(23, 2), decimal(23, 0)]);
}
#[test]
fn a_product_that_needs_more_than_thirty_eight_decimal_places_is_refused() {
let wide = LogicalType::Decimal { width: 30, scale: 30 };
let error = resolve("*", &[wide.clone(), wide]).expect_err("has nowhere to put the scale");
assert!(error.to_string().contains("Max scale is 38"), "{error}");
}
#[test]
fn division_gives_a_double_and_integer_division_does_not() {
let divide = resolve("/", &[LogicalType::Integer, LogicalType::Integer]).expect("divides");
assert_eq!(divide.returns, LogicalType::Double);
let integer =
resolve("//", &[LogicalType::Integer, LogicalType::Integer]).expect("divides");
assert_eq!(integer.returns, LogicalType::Integer);
}
#[test]
fn integer_division_of_anything_but_integers_is_ordinary_division() {
let decimal = LogicalType::Decimal { width: 4, scale: 2 };
let divides = |left: LogicalType, right: LogicalType| {
let resolved = resolve("//", &[left, right]).expect("divides");
(resolved.arguments, resolved.returns)
};
let double = || (vec![LogicalType::Double; 2], LogicalType::Double);
assert_eq!(divides(decimal.clone(), decimal.clone()), double());
assert_eq!(divides(decimal.clone(), LogicalType::Integer), double());
assert_eq!(divides(LogicalType::Integer, decimal), double());
assert_eq!(divides(LogicalType::Double, LogicalType::Double), double());
assert_eq!(
divides(LogicalType::Float, LogicalType::Float),
(vec![LogicalType::Float; 2], LogicalType::Float)
);
assert_eq!(
divides(LogicalType::Integer, LogicalType::BigInt),
(vec![LogicalType::BigInt; 2], LogicalType::BigInt)
);
assert_eq!(
divides(LogicalType::HugeInt, LogicalType::HugeInt),
(vec![LogicalType::HugeInt; 2], LogicalType::HugeInt)
);
}
#[test]
fn an_alias_resolves_to_the_function_it_is_an_alias_of() {
for (alias, real) in ALIASES {
assert_eq!(canonical(alias), *real);
assert_eq!(canonical(&alias.to_uppercase()), *real);
}
let resolved = resolve("LEN", &[LogicalType::Varchar]).expect("len resolves");
assert_eq!(resolved.name, "length");
assert_eq!(resolved.returns, LogicalType::BigInt);
}
#[test]
fn every_alias_points_at_a_real_function() {
for (alias, real) in ALIASES {
assert!(
TABLE.iter().any(|entry| entry.name == *real),
"{alias} points at {real}, which is not in the table"
);
}
}
#[test]
fn a_string_function_refuses_a_type_that_is_not_a_string() {
let error = resolve("lower", &[LogicalType::Date]).expect_err("lower takes strings");
assert_eq!(
error.to_string(),
"Binder Error: No function matches the given name and argument types 'lower(DATE)'. \
You might need to add explicit type casts.\n\tCandidate functions:\n\tlower(col0 \
VARCHAR) -> VARCHAR\n"
);
for name in ["upper", "length", "strlen"] {
assert!(resolve(name, &[LogicalType::Integer]).is_err(), "{name} took an integer");
}
for name in ["~~", "!~~", "~~*", "!~~*"] {
let types = [LogicalType::Integer, LogicalType::Varchar];
assert!(resolve(name, &types).is_err(), "{name} took an integer");
}
}
#[test]
fn length_of_something_that_is_not_a_string_is_refused_rather_than_stringified() {
let error = resolve("length", &[LogicalType::Blob]).expect_err("length takes strings");
assert!(error.to_string().contains("length(col0 ANY[]) -> BIGINT"), "{error}");
}
#[test]
fn concatenation_still_takes_anything_and_makes_a_string_of_it() {
let resolved = resolve("||", &[LogicalType::Integer, LogicalType::Varchar])
.expect("concatenation takes anything");
assert_eq!(resolved.returns, LogicalType::Varchar);
assert_eq!(resolved.arguments, vec![LogicalType::Varchar, LogicalType::Varchar]);
}
#[test]
fn a_string_function_takes_an_untyped_null() {
let resolved = resolve("length", &[LogicalType::Null]).expect("length of a null");
assert_eq!(resolved.returns, LogicalType::BigInt);
assert_eq!(resolved.arguments, vec![LogicalType::Varchar]);
}
#[test]
fn every_name_with_candidates_is_a_function_this_engine_has() {
for (name, overloads) in CANDIDATES {
assert!(TABLE.iter().any(|entry| entry.name == *name), "{name} has no entry");
assert!(!overloads.is_empty(), "{name} has an empty candidate list");
}
}
#[test]
fn strlen_is_its_own_function_and_not_an_alias_of_length() {
assert!(!ALIASES.iter().any(|(alias, _)| *alias == "strlen"));
let resolved = resolve("strlen", &[LogicalType::Varchar]).expect("strlen resolves");
assert_eq!(resolved.name, "strlen");
assert_eq!(resolved.returns, LogicalType::BigInt);
}
#[test]
fn a_sum_accumulates_wider_than_it_reads() {
assert_eq!(
resolve("sum", &[LogicalType::Integer]).expect("sums").returns,
LogicalType::HugeInt
);
assert_eq!(
resolve("sum", &[LogicalType::Double]).expect("sums").returns,
LogicalType::Double
);
assert_eq!(
resolve("sum", &[LogicalType::Float]).expect("sums").returns,
LogicalType::Double
);
}
#[test]
fn count_takes_anything_and_returns_a_bigint() {
let counted = resolve("count", &[LogicalType::Varchar]).expect("counts strings");
assert_eq!(counted.returns, LogicalType::BigInt);
assert_eq!(counted.arguments, vec![LogicalType::Varchar], "count does not cast its input");
assert_eq!(resolve("count_star", &[]).expect("counts rows").returns, LogicalType::BigInt);
}
#[test]
fn a_date_function_fixes_the_part_and_leaves_the_date_alone() {
let part = resolve("date_part", &[LogicalType::Varchar, LogicalType::Timestamp])
.expect("a part of a timestamp");
assert_eq!(part.returns, LogicalType::BigInt);
assert_eq!(part.arguments, vec![LogicalType::Varchar, LogicalType::Timestamp]);
let truncated = resolve("date_trunc", &[LogicalType::Varchar, LogicalType::Date])
.expect("a truncated date");
assert_eq!(truncated.returns, LogicalType::Date);
assert_eq!(truncated.arguments, vec![LogicalType::Varchar, LogicalType::Date]);
}
#[test]
fn a_date_is_made_from_one_number_or_from_three_and_never_from_two() {
let day = resolve("make_date", &[LogicalType::Integer]).expect("days since the epoch");
assert_eq!(day.returns, LogicalType::Date);
assert_eq!(day.arguments, vec![LogicalType::Integer]);
let civil =
resolve("make_date", &vec![LogicalType::BigInt; 3]).expect("a year, a month and a day");
assert_eq!(civil.returns, LogicalType::Date);
assert_eq!(civil.arguments, vec![LogicalType::Integer; 3]);
let error = resolve("make_date", &vec![LogicalType::Integer; 2]).unwrap_err();
assert_eq!(
error.message(),
"No function matches the given name and argument types 'make_date(INTEGER, INTEGER)'. You might need to add explicit type casts."
);
}
#[test]
fn milliseconds_since_the_epoch_are_a_timestamp() {
let stamp = resolve("epoch_ms", &[LogicalType::Integer]).expect("a timestamp");
assert_eq!(stamp.returns, LogicalType::Timestamp);
assert_eq!(stamp.arguments, vec![LogicalType::BigInt], "the argument widens to read it");
let error = resolve("epoch_ms", &[LogicalType::Varchar]).unwrap_err();
assert!(error.message().contains("'epoch_ms(VARCHAR)'"), "{error}");
}
#[test]
fn the_part_of_a_date_function_is_cast_to_a_string() {
let resolved = resolve("date_part", &[LogicalType::Integer, LogicalType::Date])
.expect("the part is cast rather than refused");
assert_eq!(resolved.arguments, vec![LogicalType::Varchar, LogicalType::Date]);
}
#[test]
fn an_extraction_casts_the_text_and_the_pattern_and_leaves_the_group_alone() {
let resolved = resolve(
"regexp_extract",
&[LogicalType::Varchar, LogicalType::Varchar, LogicalType::Integer],
)
.expect("an extraction");
assert_eq!(resolved.returns, LogicalType::Varchar);
assert_eq!(
resolved.arguments,
vec![LogicalType::Varchar, LogicalType::Varchar, LogicalType::Integer]
);
let matched = resolve("regexp_matches", &[LogicalType::Varchar, LogicalType::Varchar])
.expect("a match");
assert_eq!(matched.returns, LogicalType::Boolean);
}
#[test]
fn a_name_that_is_not_a_function_says_so_the_way_duckdb_does() {
let error = resolve("nope", &[]).expect_err("there is no function called nope");
assert_eq!(
error.to_string(),
"Catalog Error: Scalar Function with name nope does not exist!"
);
}
#[test]
fn the_wrong_number_of_arguments_is_caught() {
let error = resolve("abs", &[LogicalType::Integer, LogicalType::Integer])
.expect_err("abs takes one");
assert!(error.message().contains("No function matches"), "{error}");
}
#[test]
fn arithmetic_on_a_string_is_refused() {
let error =
resolve("*", &[LogicalType::Varchar, LogicalType::Integer]).expect_err("no multiply");
assert!(error.message().contains("No function matches"), "{error}");
}
#[test]
fn a_call_over_nothing_but_nulls_lands_on_a_type_an_executor_can_hold() {
let resolved =
resolve("+", &[LogicalType::Null, LogicalType::Null]).expect("null plus null");
assert_eq!(resolved.returns, LogicalType::Integer);
}
#[test]
fn an_aggregate_is_known_to_be_one() {
assert_eq!(kind_of("sum"), Some(FunctionKind::Aggregate));
assert_eq!(kind_of("SUM"), Some(FunctionKind::Aggregate), "names are case insensitive");
assert_eq!(kind_of("abs"), Some(FunctionKind::Scalar));
assert_eq!(kind_of("nope"), None);
}
#[test]
fn no_name_appears_twice() {
let mut names: Vec<&str> = TABLE.iter().map(|entry| entry.name).collect();
let count = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "a name is in the table twice");
}
#[test]
fn every_entry_resolves_at_every_count_it_accepts() {
for entry in TABLE {
for count in entry.arity.counts() {
let ty =
if entry.numeric_only { LogicalType::Integer } else { LogicalType::Varchar };
let arguments = vec![ty; count];
resolve(entry.name, &arguments).unwrap_or_else(|error| {
panic!("{} does not resolve at {count} arguments: {error}", entry.name)
});
}
}
}
#[test]
fn nothing_that_promotes_accepts_no_arguments() {
for entry in TABLE {
let promotes =
matches!(entry.shape, Shape::Promoted | Shape::PromotedTo(_) | Shape::Accumulated);
assert!(
!(promotes && entry.arity.least() == 0),
"{} promotes over its arguments and takes none",
entry.name
);
}
}
#[test]
fn minus_is_both_the_negation_and_the_subtraction() {
assert_eq!(
resolve("-", &[LogicalType::Integer]).expect("negates").returns,
LogicalType::Integer
);
assert_eq!(
resolve("-", &[LogicalType::Integer, LogicalType::BigInt]).expect("subtracts").returns,
LogicalType::BigInt
);
assert!(
resolve("-", &vec![LogicalType::Integer; 3]).is_err(),
"three is not an arity minus has"
);
}
}