use rudb_common::{Error, Field, LogicalType, Result};
use crate::entrycatalog::{
column_fields, database_fields, schema_fields, table_fields, view_fields,
};
use crate::functioncatalog::function_fields;
use crate::settingcatalog::setting_fields;
use crate::typecatalog::type_fields;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TableFunction {
Range,
GenerateSeries,
ReadParquet,
ReadCsv,
RudbStrategies,
DuckdbKeywords,
DuckdbTypes,
DuckdbFunctions,
DuckdbSettings,
DuckdbDatabases,
DuckdbSchemas,
DuckdbTables,
DuckdbViews,
DuckdbColumns,
DuckdbExtensions,
DuckdbOptimizers,
DuckdbDialects,
DuckdbGrammarExtensions,
PragmaTableInfo,
PragmaShow,
PragmaVersion,
PragmaPlatform,
PragmaUserAgent,
PragmaDatabaseSize,
}
pub const FILE_ROW_NUMBER: &str = "file_row_number";
impl TableFunction {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Range => "range",
Self::GenerateSeries => "generate_series",
Self::ReadParquet => "read_parquet",
Self::ReadCsv => "read_csv",
Self::RudbStrategies => "rudb_strategies",
Self::DuckdbKeywords => "duckdb_keywords",
Self::DuckdbTypes => "duckdb_types",
Self::DuckdbFunctions => "duckdb_functions",
Self::DuckdbSettings => "duckdb_settings",
Self::DuckdbDatabases => "duckdb_databases",
Self::DuckdbSchemas => "duckdb_schemas",
Self::DuckdbTables => "duckdb_tables",
Self::DuckdbViews => "duckdb_views",
Self::DuckdbColumns => "duckdb_columns",
Self::DuckdbExtensions => "duckdb_extensions",
Self::DuckdbOptimizers => "duckdb_optimizers",
Self::DuckdbDialects => "duckdb_dialects",
Self::DuckdbGrammarExtensions => "duckdb_grammar_extensions",
Self::PragmaTableInfo => "pragma_table_info",
Self::PragmaShow => "pragma_show",
Self::PragmaVersion => "pragma_version",
Self::PragmaPlatform => "pragma_platform",
Self::PragmaUserAgent => "pragma_user_agent",
Self::PragmaDatabaseSize => "pragma_database_size",
}
}
#[must_use]
pub const fn takes_a_name(self) -> bool {
matches!(self, Self::PragmaTableInfo | Self::PragmaShow)
}
#[must_use]
pub const fn inclusive(self) -> bool {
matches!(self, Self::GenerateSeries)
}
#[must_use]
pub fn parameters(self) -> &'static [(&'static str, LogicalType)] {
static READ_PARQUET: &[(&str, LogicalType)] = &[
("binary_as_string", LogicalType::Boolean),
("file_row_number", LogicalType::Boolean),
];
static READ_CSV: &[(&str, LogicalType)] = &[
("all_varchar", LogicalType::Boolean),
("delim", LogicalType::Varchar),
("escape", LogicalType::Varchar),
("header", LogicalType::Boolean),
("quote", LogicalType::Varchar),
("sep", LogicalType::Varchar),
];
match self {
Self::ReadParquet => READ_PARQUET,
Self::ReadCsv => READ_CSV,
_ => &[],
}
}
#[must_use]
pub fn lookup(name: &str) -> Option<Self> {
if name.eq_ignore_ascii_case("range") {
return Some(Self::Range);
}
if name.eq_ignore_ascii_case("generate_series") {
return Some(Self::GenerateSeries);
}
if name.eq_ignore_ascii_case("read_parquet") || name.eq_ignore_ascii_case("parquet_scan") {
return Some(Self::ReadParquet);
}
if name.eq_ignore_ascii_case("read_csv") || name.eq_ignore_ascii_case("read_csv_auto") {
return Some(Self::ReadCsv);
}
if name.eq_ignore_ascii_case("rudb_strategies") {
return Some(Self::RudbStrategies);
}
if name.eq_ignore_ascii_case("duckdb_keywords") {
return Some(Self::DuckdbKeywords);
}
if name.eq_ignore_ascii_case("duckdb_types") {
return Some(Self::DuckdbTypes);
}
if name.eq_ignore_ascii_case("duckdb_functions") {
return Some(Self::DuckdbFunctions);
}
if name.eq_ignore_ascii_case("duckdb_settings") {
return Some(Self::DuckdbSettings);
}
if name.eq_ignore_ascii_case("duckdb_databases") {
return Some(Self::DuckdbDatabases);
}
if name.eq_ignore_ascii_case("duckdb_schemas") {
return Some(Self::DuckdbSchemas);
}
if name.eq_ignore_ascii_case("duckdb_tables") {
return Some(Self::DuckdbTables);
}
if name.eq_ignore_ascii_case("duckdb_views") {
return Some(Self::DuckdbViews);
}
if name.eq_ignore_ascii_case("duckdb_columns") {
return Some(Self::DuckdbColumns);
}
if name.eq_ignore_ascii_case("duckdb_extensions") {
return Some(Self::DuckdbExtensions);
}
if name.eq_ignore_ascii_case("duckdb_optimizers") {
return Some(Self::DuckdbOptimizers);
}
if name.eq_ignore_ascii_case("duckdb_dialects") {
return Some(Self::DuckdbDialects);
}
if name.eq_ignore_ascii_case("duckdb_grammar_extensions") {
return Some(Self::DuckdbGrammarExtensions);
}
if name.eq_ignore_ascii_case("pragma_table_info") {
return Some(Self::PragmaTableInfo);
}
if name.eq_ignore_ascii_case("pragma_show") {
return Some(Self::PragmaShow);
}
if name.eq_ignore_ascii_case("pragma_version") {
return Some(Self::PragmaVersion);
}
if name.eq_ignore_ascii_case("pragma_platform") {
return Some(Self::PragmaPlatform);
}
if name.eq_ignore_ascii_case("pragma_user_agent") {
return Some(Self::PragmaUserAgent);
}
if name.eq_ignore_ascii_case("pragma_database_size") {
return Some(Self::PragmaDatabaseSize);
}
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Columns {
Fixed(Vec<Field>),
Parquet,
Csv,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedTable {
pub function: TableFunction,
pub arguments: Vec<LogicalType>,
pub columns: Columns,
}
pub fn resolve_table(name: &str, arguments: &[LogicalType]) -> Result<ResolvedTable> {
let Some(function) = TableFunction::lookup(name) else {
return Err(Error::catalog(format!("Table Function with name {name} does not exist!")));
};
if let Some(columns) = file_columns(function) {
let list = LogicalType::list(LogicalType::Varchar);
let single = arguments.len() == 1 && arguments[0] == LogicalType::Varchar;
let many = arguments.len() == 1 && arguments[0] == list;
let nothing = arguments.len() == 1 && arguments[0] == LogicalType::Null;
if !single && !many && !nothing {
return Err(no_overload(function, arguments));
}
let wanted = if many {
list
} else if nothing {
LogicalType::Null
} else {
LogicalType::Varchar
};
return Ok(ResolvedTable { function, arguments: vec![wanted], columns });
}
if function.takes_a_name() {
let single = arguments.len() == 1
&& matches!(arguments[0], LogicalType::Varchar | LogicalType::Null);
if !single {
return Err(one_name(function, arguments));
}
return Ok(ResolvedTable {
function,
arguments: vec![arguments[0].clone()],
columns: Columns::Fixed(name_columns(function)),
});
}
let arity = arguments.len();
if let Some(columns) = fixed_columns(function) {
if arity != 0 {
return Err(nothing_at_all(function, arguments));
}
return Ok(ResolvedTable {
function,
arguments: Vec::new(),
columns: Columns::Fixed(columns),
});
}
if !(1..=3).contains(&arity) {
return Err(Error::binder(format!(
"Table function {}() takes between 1 and 3 arguments, {arity} were given",
function.name()
)));
}
Ok(ResolvedTable {
function,
arguments: vec![LogicalType::BigInt; arity],
columns: Columns::Fixed(vec![Field::new(function.name(), LogicalType::BigInt)]),
})
}
pub fn resolve_pragma(name: &str, arguments: &[LogicalType]) -> Result<ResolvedTable> {
let error = match resolve_table(name, arguments) {
Ok(resolved) => return Ok(resolved),
Err(error) => error,
};
let Some(function) = TableFunction::lookup(name) else {
return Err(error);
};
let spelled = name.strip_prefix("pragma_").unwrap_or(name);
let takes = if function.takes_a_name() { "(VARCHAR)" } else { "" };
let written: Vec<String> = arguments.iter().map(ToString::to_string).collect();
Err(Error::binder(format!(
"No function matches the given name and argument types '{spelled}({})'. You might need to \
add explicit type casts.\n\tCandidate functions:\n\tPRAGMA \"{spelled}\"{takes}\n",
written.join(", ")
)))
}
fn file_columns(function: TableFunction) -> Option<Columns> {
match function {
TableFunction::ReadParquet => Some(Columns::Parquet),
TableFunction::ReadCsv => Some(Columns::Csv),
TableFunction::Range
| TableFunction::GenerateSeries
| 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 => None,
}
}
fn fixed_columns(function: TableFunction) -> Option<Vec<Field>> {
match function {
TableFunction::RudbStrategies => Some(strategy_fields()),
TableFunction::DuckdbKeywords => Some(keyword_fields()),
TableFunction::DuckdbTypes => Some(type_fields()),
TableFunction::DuckdbFunctions => Some(function_fields()),
TableFunction::DuckdbSettings => Some(setting_fields()),
TableFunction::DuckdbDatabases => Some(database_fields()),
TableFunction::DuckdbSchemas => Some(schema_fields()),
TableFunction::DuckdbTables => Some(table_fields()),
TableFunction::DuckdbViews => Some(view_fields()),
TableFunction::DuckdbColumns => Some(column_fields()),
TableFunction::DuckdbExtensions => Some(extension_fields()),
TableFunction::DuckdbOptimizers => Some(optimizer_fields()),
TableFunction::DuckdbDialects => Some(dialect_fields()),
TableFunction::DuckdbGrammarExtensions => Some(grammar_extension_fields()),
TableFunction::PragmaVersion => Some(version_fields()),
TableFunction::PragmaPlatform => Some(platform_fields()),
TableFunction::PragmaUserAgent => Some(user_agent_fields()),
TableFunction::PragmaDatabaseSize => Some(database_size_fields()),
TableFunction::Range
| TableFunction::GenerateSeries
| TableFunction::ReadParquet
| TableFunction::ReadCsv
| TableFunction::PragmaTableInfo
| TableFunction::PragmaShow => None,
}
}
fn name_columns(function: TableFunction) -> Vec<Field> {
match function {
TableFunction::PragmaShow => describe_fields(),
_ => table_info_fields(),
}
}
#[must_use]
pub fn table_info_fields() -> Vec<Field> {
vec![
Field::new("cid", LogicalType::Integer),
Field::new("name", LogicalType::Varchar),
Field::new("type", LogicalType::Varchar),
Field::new("notnull", LogicalType::Boolean),
Field::new("dflt_value", LogicalType::Varchar),
Field::new("pk", LogicalType::Boolean),
]
}
#[must_use]
pub fn describe_fields() -> Vec<Field> {
["column_name", "column_type", "null", "key", "default", "extra"]
.iter()
.map(|name| Field::new(*name, LogicalType::Varchar))
.collect()
}
#[must_use]
pub fn version_fields() -> Vec<Field> {
["library_version", "source_id", "codename"]
.iter()
.map(|name| Field::new(*name, LogicalType::Varchar))
.collect()
}
#[must_use]
pub fn platform_fields() -> Vec<Field> {
vec![Field::new("platform", LogicalType::Varchar)]
}
#[must_use]
pub fn user_agent_fields() -> Vec<Field> {
vec![Field::new("user_agent", LogicalType::Varchar)]
}
#[must_use]
pub fn database_size_fields() -> Vec<Field> {
vec![
Field::new("database_name", LogicalType::Varchar),
Field::new("database_size", LogicalType::Varchar),
Field::new("block_size", LogicalType::BigInt),
Field::new("total_blocks", LogicalType::BigInt),
Field::new("used_blocks", LogicalType::BigInt),
Field::new("free_blocks", LogicalType::BigInt),
Field::new("wal_size", LogicalType::Varchar),
Field::new("memory_usage", LogicalType::Varchar),
Field::new("memory_limit", LogicalType::Varchar),
]
}
#[must_use]
pub fn strategy_fields() -> Vec<Field> {
vec![
Field::new("seam", LogicalType::Varchar),
Field::new("milestone", LogicalType::Varchar),
Field::new("seam_description", LogicalType::Varchar),
Field::new("implementation", LogicalType::Varchar),
Field::new("implementation_description", LogicalType::Varchar),
Field::new("provenance", LogicalType::Varchar),
Field::new("determinism", LogicalType::Varchar),
Field::new("is_reference", LogicalType::Boolean),
Field::new("is_default", LogicalType::Boolean),
]
}
#[must_use]
pub fn keyword_fields() -> Vec<Field> {
vec![
Field::new("keyword_name", LogicalType::Varchar),
Field::new("keyword_category", LogicalType::Varchar),
]
}
#[must_use]
pub fn extension_fields() -> Vec<Field> {
vec![
Field::new("extension_name", LogicalType::Varchar),
Field::new("loaded", LogicalType::Boolean),
Field::new("installed", LogicalType::Boolean),
Field::new("install_path", LogicalType::Varchar),
Field::new("description", LogicalType::Varchar),
Field::new("aliases", LogicalType::list(LogicalType::Varchar)),
Field::new("extension_version", LogicalType::Varchar),
Field::new("install_mode", LogicalType::Varchar),
Field::new("installed_from", LogicalType::Varchar),
Field::new("signature_key_fingerprint", LogicalType::Varchar),
]
}
#[must_use]
pub fn optimizer_fields() -> Vec<Field> {
vec![Field::new("name", LogicalType::Varchar)]
}
#[must_use]
pub fn dialect_fields() -> Vec<Field> {
vec![Field::new("dialect_name", LogicalType::Varchar)]
}
#[must_use]
pub fn grammar_extension_fields() -> Vec<Field> {
vec![Field::new("name", LogicalType::Varchar), Field::new("description", LogicalType::Varchar)]
}
#[must_use]
pub fn keyword_categories(classes: u8) -> Vec<&'static str> {
use rudb_parse::{COLUMN_NAME, FUNC_NAME, RESERVED, TYPE_NAME, UNRESERVED};
let mut out = Vec::new();
if classes & RESERVED != 0 {
out.push("reserved");
}
if classes & UNRESERVED != 0 {
out.push("unreserved");
}
if classes & COLUMN_NAME != 0 {
out.push("column_name");
}
if classes & (FUNC_NAME | TYPE_NAME) != 0 {
out.push("type_function");
}
out
}
fn no_overload(function: TableFunction, arguments: &[LogicalType]) -> Error {
let written: Vec<String> = arguments.iter().map(ToString::to_string).collect();
let name = function.name();
Error::binder(format!(
"No function matches the given name and argument types '{name}({})'. You might need to \
add explicit type casts.\n\tCandidate functions:\n\t{name}(VARCHAR)\n\t{name}(VARCHAR[])\n",
written.join(", ")
))
}
fn one_name(function: TableFunction, arguments: &[LogicalType]) -> Error {
let written: Vec<String> = arguments.iter().map(ToString::to_string).collect();
let name = function.name();
Error::binder(format!(
"No function matches the given name and argument types '{name}({})'. You might need to \
add explicit type casts.\n\tCandidate functions:\n\t\"{name}\"(VARCHAR)\n",
written.join(", ")
))
}
fn nothing_at_all(function: TableFunction, arguments: &[LogicalType]) -> Error {
let written: Vec<String> = arguments.iter().map(ToString::to_string).collect();
let name = function.name();
Error::binder(format!(
"No function matches the given name and argument types '{name}({})'. You might need to \
add explicit type casts.\n\tCandidate functions:\n\t\"{name}\"()\n",
written.join(", ")
))
}
pub fn series(function: TableFunction, start: i64, stop: i64, step: i64) -> Result<Vec<i64>> {
let count = series_length(function, start, stop, step)?;
let mut out = Vec::with_capacity(count);
let mut at = start;
for _ in 0..count {
out.push(at);
at = at.saturating_add(step);
}
Ok(out)
}
pub fn series_length(function: TableFunction, start: i64, stop: i64, step: i64) -> Result<usize> {
if step == 0 {
return Err(Error::binder("interval cannot be 0!"));
}
Ok(length(function, start, stop, step))
}
fn length(function: TableFunction, start: i64, stop: i64, step: i64) -> usize {
let start = i128::from(start);
let stop = i128::from(stop);
let step = i128::from(step);
let span = if function.inclusive() {
if step > 0 { stop - start + 1 } else { stop - start - 1 }
} else {
stop - start
};
if (span > 0) != (step > 0) {
return 0;
}
let count = (span + step - step.signum()) / step;
usize::try_from(count).unwrap_or(usize::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
fn fixed(resolved: &ResolvedTable) -> &[Field] {
match &resolved.columns {
Columns::Fixed(fields) => fields,
Columns::Parquet | Columns::Csv => {
panic!("{} resolves to a file", resolved.function.name())
}
}
}
fn integers(count: usize) -> Vec<LogicalType> {
vec![LogicalType::BigInt; count]
}
#[test]
fn a_name_that_is_not_a_table_function_says_so_rather_than_binding() {
let error = resolve_table("read_csv", &integers(1)).unwrap_err();
assert!(error.to_string().contains("read_csv"), "{error}");
}
#[test]
fn both_names_resolve_and_each_one_names_its_own_column() {
let range = resolve_table("range", &integers(1)).unwrap();
assert_eq!(fixed(&range)[0].name, "range");
let series = resolve_table("GENERATE_SERIES", &integers(3)).unwrap();
assert_eq!(fixed(&series)[0].name, "generate_series");
assert_eq!(series.arguments.len(), 3);
}
#[test]
fn no_arguments_and_four_arguments_are_both_the_arity_error() {
assert!(resolve_table("range", &integers(0)).is_err());
assert!(resolve_table("range", &integers(4)).is_err());
}
#[test]
fn a_series_call_ignores_the_types_it_was_given_and_casts_them_all_to_bigint() {
let resolved =
resolve_table("range", &[LogicalType::Varchar, LogicalType::Double]).unwrap();
assert_eq!(resolved.arguments, integers(2));
}
#[test]
fn read_parquet_takes_one_string_and_says_its_columns_are_in_the_file() {
let resolved = resolve_table("read_parquet", &[LogicalType::Varchar]).unwrap();
assert_eq!(resolved.function, TableFunction::ReadParquet);
assert_eq!(resolved.arguments, vec![LogicalType::Varchar]);
assert_eq!(resolved.columns, Columns::Parquet);
}
#[test]
fn parquet_scan_is_the_same_function_under_duckdbs_other_name_for_it() {
assert_eq!(TableFunction::lookup("parquet_scan"), Some(TableFunction::ReadParquet));
let resolved = resolve_table("parquet_scan", &[LogicalType::Varchar]).unwrap();
assert_eq!(resolved.function.name(), "read_parquet");
}
#[test]
fn a_path_that_is_not_a_string_is_the_message_duckdb_gives_for_it() {
let error = resolve_table("read_parquet", &[LogicalType::Integer]).unwrap_err();
assert!(
error.message().starts_with(
"No function matches the given name and argument types 'read_parquet(INTEGER)'."
),
"{error}"
);
assert!(error.message().contains("read_parquet(VARCHAR)"), "{error}");
}
#[test]
fn read_parquet_of_no_arguments_or_two_is_the_same_no_overload_message() {
let two = resolve_table("read_parquet", &[LogicalType::Varchar, LogicalType::Varchar]);
assert!(two.unwrap_err().message().contains("read_parquet(VARCHAR, VARCHAR)"));
let none = resolve_table("read_parquet", &[]);
assert!(none.unwrap_err().message().contains("read_parquet()"));
}
#[test]
fn range_stops_before_the_end_and_generate_series_stops_on_it() {
assert_eq!(series(TableFunction::Range, 0, 3, 1).unwrap(), vec![0, 1, 2]);
assert_eq!(series(TableFunction::GenerateSeries, 0, 3, 1).unwrap(), vec![0, 1, 2, 3]);
}
#[test]
fn a_step_that_does_not_divide_the_span_stops_before_the_end_of_it() {
assert_eq!(series(TableFunction::Range, 2, 7, 2).unwrap(), vec![2, 4, 6]);
assert_eq!(series(TableFunction::GenerateSeries, 2, 7, 2).unwrap(), vec![2, 4, 6]);
}
#[test]
fn the_four_categories_come_out_of_the_grammars_five_rules() {
use rudb_parse::{COLUMN_NAME, FUNC_NAME, RESERVED, TYPE_NAME, UNRESERVED};
assert_eq!(keyword_categories(RESERVED), ["reserved"]);
assert_eq!(keyword_categories(UNRESERVED), ["unreserved"]);
assert_eq!(keyword_categories(COLUMN_NAME), ["column_name"]);
assert_eq!(keyword_categories(FUNC_NAME | TYPE_NAME), ["type_function"]);
assert_eq!(keyword_categories(TYPE_NAME), ["type_function"]);
assert_eq!(keyword_categories(FUNC_NAME), ["type_function"]);
assert_eq!(keyword_categories(COLUMN_NAME | FUNC_NAME), ["column_name", "type_function"]);
assert!(keyword_categories(0).is_empty());
}
#[test]
fn a_metadata_table_given_an_argument_says_it_takes_none() {
for name in [
"rudb_strategies",
"duckdb_keywords",
"duckdb_types",
"duckdb_functions",
"duckdb_settings",
"duckdb_databases",
"duckdb_schemas",
"duckdb_tables",
"duckdb_columns",
] {
let function = TableFunction::lookup(name).expect("a known function");
let error = resolve_table(name, &[LogicalType::BigInt]).expect_err("takes none");
assert!(error.to_string().contains(&format!("\"{}\"()", function.name())), "{error}");
let resolved = resolve_table(name, &[]).expect("takes none, and none were given");
assert_eq!(resolved.function, function);
assert!(matches!(resolved.columns, Columns::Fixed(_)));
}
}
#[test]
fn duckdb_keywords_has_duckdbs_two_columns_under_that_name() {
let resolved = resolve_table("DuckDB_Keywords", &[]).expect("a case insensitive name");
assert_eq!(resolved.function, TableFunction::DuckdbKeywords);
let Columns::Fixed(fields) = resolved.columns else { panic!("fixed columns") };
let names: Vec<&str> = fields.iter().map(|field| field.name.as_str()).collect();
assert_eq!(names, ["keyword_name", "keyword_category"]);
assert!(fields.iter().all(|field| field.ty == LogicalType::Varchar));
}
#[test]
fn duckdb_types_has_duckdbs_seventeen_columns_under_that_name() {
let resolved = resolve_table("DuckDB_Types", &[]).expect("a case insensitive name");
assert_eq!(resolved.function, TableFunction::DuckdbTypes);
let Columns::Fixed(fields) = resolved.columns else { panic!("fixed columns") };
let names: Vec<&str> = fields.iter().map(|field| field.name.as_str()).collect();
assert_eq!(names.len(), 17);
assert_eq!(names[0], "database_name");
assert_eq!(names[16], "varargs");
let tags = fields.iter().find(|field| field.name == "tags").expect("a tags column");
assert_eq!(tags.ty, LogicalType::map(LogicalType::Varchar, LogicalType::Varchar));
}
#[test]
fn duckdb_settings_has_duckdbs_seven_columns_under_that_name() {
let resolved = resolve_table("DuckDB_Settings", &[]).expect("a case insensitive name");
assert_eq!(resolved.function, TableFunction::DuckdbSettings);
let Columns::Fixed(fields) = resolved.columns else { panic!("fixed columns") };
let names: Vec<&str> = fields.iter().map(|field| field.name.as_str()).collect();
assert_eq!(
names,
["name", "value", "description", "input_type", "scope", "aliases", "typed_value"]
);
assert_eq!(fields[6].ty, LogicalType::Varchar);
}
#[test]
fn a_negative_step_counts_down_and_stops_on_the_same_rule() {
assert_eq!(series(TableFunction::Range, 5, 1, -2).unwrap(), vec![5, 3]);
assert_eq!(series(TableFunction::GenerateSeries, 5, 1, -2).unwrap(), vec![5, 3, 1]);
}
#[test]
fn a_step_going_the_wrong_way_produces_nothing_rather_than_running_forever() {
assert!(series(TableFunction::Range, 0, 10, -1).unwrap().is_empty());
assert!(series(TableFunction::Range, 10, 0, 1).unwrap().is_empty());
}
#[test]
fn an_empty_range_and_a_single_value_series_are_the_boundary_between_the_two() {
assert!(series(TableFunction::Range, 4, 4, 1).unwrap().is_empty());
assert_eq!(series(TableFunction::GenerateSeries, 4, 4, 1).unwrap(), vec![4]);
}
#[test]
fn a_step_of_zero_is_the_one_case_that_is_an_error_rather_than_nothing() {
let error = series(TableFunction::Range, 1, 5, 0).unwrap_err();
assert!(error.to_string().contains("interval cannot be 0"), "{error}");
}
#[test]
fn a_span_that_does_not_fit_in_an_i64_does_not_overflow_the_length() {
assert_eq!(length(TableFunction::Range, i64::MIN, i64::MAX, 1), usize::MAX);
}
#[test]
fn rudb_strategies_takes_no_arguments_and_produces_a_fixed_table() {
let resolved = resolve_table("rudb_strategies", &[]).unwrap();
assert_eq!(resolved.function, TableFunction::RudbStrategies);
assert!(resolved.arguments.is_empty());
assert_eq!(fixed(&resolved), strategy_fields());
}
#[test]
fn rudb_strategies_with_an_argument_says_it_takes_none() {
let error = resolve_table("rudb_strategies", &[LogicalType::BigInt]).unwrap_err();
assert!(error.to_string().contains("\"rudb_strategies\"()"), "{error}");
assert!(error.to_string().contains("'rudb_strategies(BIGINT)'"), "{error}");
}
#[test]
fn the_two_pragmas_take_a_name_and_nothing_else_does() {
assert!(TableFunction::PragmaTableInfo.takes_a_name());
assert!(TableFunction::PragmaShow.takes_a_name());
for other in [TableFunction::Range, TableFunction::DuckdbTables, TableFunction::ReadParquet]
{
assert!(!other.takes_a_name(), "{}", other.name());
}
}
#[test]
fn pragma_table_info_answers_in_sqlites_six_columns() {
let resolved = resolve_table("PRAGMA_Table_Info", &[LogicalType::Varchar])
.expect("a case insensitive name");
assert_eq!(resolved.function, TableFunction::PragmaTableInfo);
assert_eq!(resolved.arguments, vec![LogicalType::Varchar]);
let names: Vec<&str> = fixed(&resolved).iter().map(|field| field.name.as_str()).collect();
assert_eq!(names, ["cid", "name", "type", "notnull", "dflt_value", "pk"]);
}
#[test]
fn pragma_show_answers_in_the_six_columns_describe_answers_in() {
let resolved =
resolve_table("pragma_show", &[LogicalType::Varchar]).expect("one name, one overload");
assert_eq!(resolved.function, TableFunction::PragmaShow);
let names: Vec<&str> = fixed(&resolved).iter().map(|field| field.name.as_str()).collect();
assert_eq!(names, ["column_name", "column_type", "null", "key", "default", "extra"]);
assert!(fixed(&resolved).iter().all(|field| field.ty == LogicalType::Varchar));
}
#[test]
fn a_null_name_resolves_because_the_catalog_is_what_turns_it_down() {
let resolved = resolve_table("pragma_table_info", &[LogicalType::Null]).expect("a null");
assert_eq!(resolved.arguments, vec![LogicalType::Null]);
}
#[test]
fn a_pragma_given_the_wrong_arguments_lists_its_one_overload() {
for count in [0, 2] {
let error = resolve_table("pragma_table_info", &integers(count)).expect_err("one name");
assert!(
error.message().starts_with(
"No function matches the given name and argument types 'pragma_table_info("
),
"{error}"
);
assert!(error.message().contains("\"pragma_table_info\"(VARCHAR)"), "{error}");
}
let error = resolve_table("pragma_show", &[LogicalType::Integer]).expect_err("a name");
assert!(error.message().contains("'pragma_show(INTEGER)'"), "{error}");
}
#[test]
fn a_pragma_written_as_a_statement_is_complained_about_as_one() {
let error = resolve_pragma("pragma_table_info", &integers(2)).expect_err("one name");
assert!(
error.message().starts_with(
"No function matches the given name and argument types 'table_info(BIGINT, \
BIGINT)'"
),
"{error}"
);
assert!(error.message().contains("\tPRAGMA \"table_info\"(VARCHAR)\n"), "{error}");
let error = resolve_pragma("pragma_version", &integers(1)).expect_err("nothing");
assert!(error.message().contains("'version(BIGINT)'"), "{error}");
assert!(error.message().ends_with("\tPRAGMA \"version\"\n"), "{error}");
}
#[test]
fn a_pragma_that_resolves_resolves_to_what_the_function_spelling_does() {
let name = [LogicalType::Varchar];
let written = resolve_pragma("pragma_table_info", &name).expect("one name");
let called = resolve_table("pragma_table_info", &name).expect("one name");
assert_eq!(written.function, called.function);
assert_eq!(written.arguments, called.arguments);
let written = resolve_pragma("pragma_version", &[]).expect("nothing");
assert_eq!(written.function, TableFunction::PragmaVersion);
}
#[test]
fn the_four_pragmas_about_the_build_take_nothing_and_name_their_own_columns() {
let wanted: [(&str, TableFunction, &[&str]); 4] = [
(
"PRAGMA_Version",
TableFunction::PragmaVersion,
&["library_version", "source_id", "codename"],
),
("pragma_platform", TableFunction::PragmaPlatform, &["platform"]),
("pragma_user_agent", TableFunction::PragmaUserAgent, &["user_agent"]),
(
"pragma_database_size",
TableFunction::PragmaDatabaseSize,
&[
"database_name",
"database_size",
"block_size",
"total_blocks",
"used_blocks",
"free_blocks",
"wal_size",
"memory_usage",
"memory_limit",
],
),
];
for (name, function, columns) in wanted {
let resolved = resolve_table(name, &[]).expect("takes none, and none were given");
assert_eq!(resolved.function, function);
assert!(resolved.arguments.is_empty());
assert!(!function.takes_a_name(), "{name}");
let written: Vec<&str> =
fixed(&resolved).iter().map(|field| field.name.as_str()).collect();
assert_eq!(written, columns);
let error = resolve_table(name, &[LogicalType::Varchar]).expect_err("takes none");
assert!(error.to_string().contains(&format!("\"{}\"()", function.name())), "{error}");
}
}
#[test]
fn the_four_block_columns_are_the_only_numbers_pragma_database_size_reports() {
let fields = database_size_fields();
let numbers: Vec<&str> = fields
.iter()
.filter(|field| field.ty == LogicalType::BigInt)
.map(|field| field.name.as_str())
.collect();
assert_eq!(numbers, ["block_size", "total_blocks", "used_blocks", "free_blocks"]);
assert!(fields.iter().filter(|field| field.ty == LogicalType::Varchar).count() == 5);
}
}