use rudb_common::{Field, LogicalType};
use crate::signature::{FunctionKind, FunctionRow, function_rows};
use crate::table::TableFunction;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FunctionEntry {
pub name: &'static str,
pub function_type: &'static str,
pub alias_of: Option<&'static str>,
pub return_type: Option<&'static str>,
pub parameters: Vec<String>,
pub parameter_types: Vec<String>,
pub varargs: Option<&'static str>,
pub has_side_effects: Option<bool>,
pub stability: Option<&'static str>,
}
pub const FUNCTION_CATALOG: &str = "system";
pub const FUNCTION_SCHEMA: &str = "main";
pub const CONSISTENT: &str = "CONSISTENT";
#[must_use]
pub fn function_fields() -> Vec<Field> {
vec![
Field::new("database_name", LogicalType::Varchar),
Field::new("database_oid", LogicalType::Varchar),
Field::new("schema_name", LogicalType::Varchar),
Field::new("function_name", LogicalType::Varchar),
Field::new("alias_of", LogicalType::Varchar),
Field::new("function_type", LogicalType::Varchar),
Field::new("description", LogicalType::Varchar),
Field::new("comment", LogicalType::Varchar),
Field::new("tags", LogicalType::map(LogicalType::Varchar, LogicalType::Varchar)),
Field::new("return_type", LogicalType::Varchar),
Field::new("parameters", LogicalType::list(LogicalType::Varchar)),
Field::new("parameter_types", LogicalType::list(LogicalType::Varchar)),
Field::new("varargs", LogicalType::Varchar),
Field::new("macro_definition", LogicalType::Varchar),
Field::new("has_side_effects", LogicalType::Boolean),
Field::new("internal", LogicalType::Boolean),
Field::new("extension_name", LogicalType::Varchar),
Field::new("function_oid", LogicalType::BigInt),
Field::new("examples", LogicalType::list(LogicalType::Varchar)),
Field::new("stability", LogicalType::Varchar),
Field::new("categories", LogicalType::list(LogicalType::Varchar)),
]
}
#[must_use]
pub fn function_entries() -> Vec<FunctionEntry> {
let mut entries: Vec<FunctionEntry> = function_rows().into_iter().map(scalar).collect();
entries.extend(tables());
entries.sort_by(|left, right| {
left.name.cmp(right.name).then(left.parameters.len().cmp(&right.parameters.len()))
});
entries
}
fn scalar(row: FunctionRow) -> FunctionEntry {
FunctionEntry {
name: row.name,
function_type: match row.kind {
FunctionKind::Scalar => "scalar",
FunctionKind::Aggregate => "aggregate",
},
alias_of: row.alias_of,
return_type: Some(row.returns),
parameters: named(row.alias_of.unwrap_or(row.name), row.types.len()),
parameter_types: row.types.iter().map(|name| (*name).to_string()).collect(),
varargs: row.varargs,
has_side_effects: Some(false),
stability: Some(CONSISTENT),
}
}
fn tables() -> Vec<FunctionEntry> {
let mut entries = Vec::new();
for function in TABLE_FUNCTIONS {
for count in positional_counts(*function) {
let mut parameters = positional(count);
let mut parameter_types = vec![positional_type(*function).to_string(); count];
for (name, ty) in function.parameters() {
parameters.push((*name).to_string());
parameter_types.push(ty.to_string());
}
entries.push(FunctionEntry {
name: function.name(),
function_type: "table",
alias_of: None,
return_type: None,
parameters,
parameter_types,
varargs: None,
has_side_effects: None,
stability: None,
});
}
}
for (alias, function) in TABLE_ALIASES {
let rows: Vec<FunctionEntry> = entries
.iter()
.filter(|entry| entry.name == function.name())
.map(|entry| FunctionEntry { name: alias, ..entry.clone() })
.collect();
entries.extend(rows);
}
entries
}
const TABLE_FUNCTIONS: &[TableFunction] = &[
TableFunction::Range,
TableFunction::GenerateSeries,
TableFunction::ReadParquet,
TableFunction::ReadCsv,
TableFunction::RudbStrategies,
TableFunction::DuckdbKeywords,
TableFunction::DuckdbTypes,
TableFunction::DuckdbFunctions,
TableFunction::DuckdbSettings,
TableFunction::DuckdbDatabases,
TableFunction::DuckdbSchemas,
TableFunction::DuckdbTables,
TableFunction::DuckdbViews,
TableFunction::DuckdbColumns,
TableFunction::DuckdbExtensions,
TableFunction::DuckdbOptimizers,
TableFunction::DuckdbDialects,
TableFunction::DuckdbGrammarExtensions,
TableFunction::PragmaTableInfo,
TableFunction::PragmaShow,
TableFunction::PragmaVersion,
TableFunction::PragmaPlatform,
TableFunction::PragmaUserAgent,
TableFunction::PragmaDatabaseSize,
];
const TABLE_ALIASES: &[(&str, TableFunction)] =
&[("parquet_scan", TableFunction::ReadParquet), ("read_csv_auto", TableFunction::ReadCsv)];
fn positional_counts(function: TableFunction) -> Vec<usize> {
match function {
TableFunction::Range | TableFunction::GenerateSeries => vec![1, 2, 3],
TableFunction::ReadParquet
| TableFunction::ReadCsv
| TableFunction::PragmaTableInfo
| TableFunction::PragmaShow => vec![1],
TableFunction::RudbStrategies
| TableFunction::DuckdbKeywords
| TableFunction::DuckdbTypes
| TableFunction::DuckdbFunctions
| TableFunction::DuckdbSettings
| TableFunction::DuckdbDatabases
| TableFunction::DuckdbSchemas
| TableFunction::DuckdbTables
| TableFunction::DuckdbViews
| TableFunction::DuckdbColumns
| TableFunction::DuckdbExtensions
| TableFunction::DuckdbOptimizers
| TableFunction::DuckdbDialects
| TableFunction::DuckdbGrammarExtensions
| TableFunction::PragmaVersion
| TableFunction::PragmaPlatform
| TableFunction::PragmaUserAgent
| TableFunction::PragmaDatabaseSize => vec![0],
}
}
const fn positional_type(function: TableFunction) -> &'static str {
match function {
TableFunction::ReadParquet
| TableFunction::ReadCsv
| TableFunction::PragmaTableInfo
| TableFunction::PragmaShow => "VARCHAR",
_ => "BIGINT",
}
}
fn positional(count: usize) -> Vec<String> {
(0..count).map(|at| format!("col{at}")).collect()
}
fn named(name: &str, count: usize) -> Vec<String> {
match PARAMETER_NAMES.iter().find(|(entry, _)| *entry == name) {
Some((_, names)) if names.len() == count => {
names.iter().map(|name| (*name).to_string()).collect()
}
_ => positional(count),
}
}
const PARAMETER_NAMES: &[(&str, &[&str])] = &[("current_setting", &["setting_name"])];
#[cfg(test)]
mod tests {
use super::{CONSISTENT, function_entries, function_fields};
#[test]
fn the_table_is_the_shape_the_pin_returns() {
assert_eq!(function_fields().len(), 21);
let entries = function_entries();
assert!(!entries.is_empty());
for entry in &entries {
assert_eq!(
entry.parameters.len(),
entry.parameter_types.len(),
"{} takes {} names and {} types",
entry.name,
entry.parameters.len(),
entry.parameter_types.len()
);
}
}
#[test]
fn a_name_with_two_arities_is_two_rows_and_a_name_with_one_is_one() {
let entries = function_entries();
let rows = |name: &str| entries.iter().filter(|entry| entry.name == name).count();
assert_eq!(rows("+"), 2);
assert_eq!(rows("*"), 1);
assert_eq!(rows("substring"), 2);
let substring: Vec<usize> = entries
.iter()
.filter(|entry| entry.name == "substring")
.map(|entry| entry.parameters.len())
.collect();
assert_eq!(substring, [2, 3]);
}
#[test]
fn an_alias_is_a_row_of_its_own_that_says_what_it_resolves_to() {
let entries = function_entries();
let len: Vec<&super::FunctionEntry> =
entries.iter().filter(|entry| entry.name == "len").collect();
assert_eq!(len.len(), 1);
assert_eq!(len[0].alias_of, Some("length"));
assert_eq!(len[0].return_type, Some("BIGINT"));
let scan: Vec<&super::FunctionEntry> =
entries.iter().filter(|entry| entry.name == "parquet_scan").collect();
assert_eq!(scan.len(), 1);
assert_eq!(scan[0].alias_of, None);
assert_eq!(scan[0].function_type, "table");
}
#[test]
fn a_shape_that_promotes_is_declared_with_the_type_variable() {
let entries = function_entries();
let row = |name: &str, count: usize| {
entries
.iter()
.find(|entry| entry.name == name && entry.parameters.len() == count)
.unwrap_or_else(|| panic!("{name} of {count}"))
};
assert_eq!(row("%", 2).parameter_types, ["T", "T"]);
assert_eq!(row("%", 2).return_type, Some("T"));
assert_eq!(row("+", 2).parameter_types, ["T", "T"]);
assert_eq!(row("+", 2).return_type, Some("ANY"));
assert_eq!(row("count", 1).parameter_types, ["ANY"]);
assert_eq!(row("count", 1).return_type, Some("BIGINT"));
assert_eq!(row("lower", 1).parameter_types, ["VARCHAR"]);
assert_eq!(row("lower", 1).return_type, Some("VARCHAR"));
assert_eq!(row("substring", 3).parameter_types, ["VARCHAR", "BIGINT", "BIGINT"]);
}
#[test]
fn a_table_function_has_no_return_type_and_no_stability() {
let entries = function_entries();
let range: Vec<&super::FunctionEntry> =
entries.iter().filter(|entry| entry.name == "range").collect();
assert_eq!(range.len(), 3);
for entry in &range {
assert_eq!(entry.function_type, "table");
assert_eq!(entry.return_type, None);
assert_eq!(entry.stability, None);
assert_eq!(entry.has_side_effects, None);
}
let csv = entries
.iter()
.find(|entry| entry.name == "read_csv")
.expect("the csv reader is a table function");
assert_eq!(csv.parameters[0], "col0");
assert_eq!(csv.parameter_types[0], "VARCHAR");
assert_eq!(
csv.parameters[1..],
["all_varchar", "delim", "escape", "header", "quote", "sep"]
);
}
#[test]
fn a_setting_is_read_by_an_argument_the_table_names() {
let entry = function_entries()
.into_iter()
.find(|entry| entry.name == "current_setting")
.expect("a row for it");
assert_eq!(entry.function_type, "scalar");
assert_eq!(entry.parameters, ["setting_name"]);
assert_eq!(entry.parameter_types, ["VARCHAR"]);
assert_eq!(entry.return_type, Some("ANY"));
let lower = function_entries()
.into_iter()
.find(|entry| entry.name == "lower")
.expect("a row for it");
assert_eq!(lower.parameters, ["col0"]);
}
#[test]
fn nothing_in_this_engine_is_volatile_yet_and_the_table_says_so() {
for entry in function_entries().iter().filter(|entry| entry.function_type != "table") {
assert_eq!(entry.stability, Some(CONSISTENT), "{}", entry.name);
assert_eq!(entry.has_side_effects, Some(false), "{}", entry.name);
}
}
}