use rudb_common::{Field, LogicalType};
pub type Signature = &'static [(&'static str, &'static str)];
#[derive(Debug, Clone, Copy)]
pub struct TypeEntry {
pub name: &'static str,
pub logical_type: &'static str,
pub oid: Option<i64>,
pub varargs: Option<&'static str>,
pub signatures: &'static [Signature],
}
const BARE: &[Signature] = &[&[]];
const STRING: &[Signature] = &[&[], &[("length", "BIGINT")], &[("collation", "VARCHAR")]];
const LENGTH: &[Signature] = &[&[], &[("length", "BIGINT")]];
const WIDTH_SCALE: &[Signature] = &[&[], &[("width", "UTINYINT"), ("scale", "UTINYINT")]];
const PRECISION: &[Signature] = &[&[], &[("precision", "UTINYINT")]];
pub static TYPE_NAMES: &[TypeEntry] = &[
entry("bigint", "BIGINT", Some(14)),
entry("binary", "BLOB", Some(26)),
TypeEntry { signatures: LENGTH, oid: Some(36), ..entry("bit", "BIT", None) },
TypeEntry { signatures: LENGTH, ..entry("bitstring", "BIT", None) },
entry("blob", "BLOB", None),
entry("bool", "BOOLEAN", Some(10)),
entry("boolean", "BOOLEAN", None),
TypeEntry { signatures: STRING, oid: Some(25), ..entry("bpchar", "VARCHAR", None) },
entry("bytea", "BLOB", None),
TypeEntry { signatures: STRING, ..entry("char", "VARCHAR", None) },
entry("date", "DATE", Some(15)),
TypeEntry { signatures: PRECISION, oid: Some(19), ..entry("datetime", "TIMESTAMP", None) },
TypeEntry { signatures: WIDTH_SCALE, oid: Some(21), ..entry("dec", "DECIMAL", None) },
TypeEntry { signatures: WIDTH_SCALE, ..entry("decimal", "DECIMAL", None) },
entry("double", "DOUBLE", Some(23)),
entry("float", "FLOAT", Some(22)),
entry("float4", "FLOAT", None),
entry("float8", "DOUBLE", None),
entry("guid", "UUID", Some(54)),
entry("hugeint", "HUGEINT", Some(50)),
entry("int", "INTEGER", Some(13)),
entry("int1", "TINYINT", Some(11)),
entry("int128", "HUGEINT", None),
entry("int16", "SMALLINT", Some(12)),
entry("int2", "SMALLINT", None),
entry("int32", "INTEGER", None),
entry("int4", "INTEGER", None),
entry("int64", "BIGINT", None),
entry("int8", "BIGINT", None),
entry("integer", "INTEGER", None),
entry("integral", "INTEGER", None),
TypeEntry { signatures: PRECISION, oid: Some(27), ..entry("interval", "INTERVAL", None) },
TypeEntry {
signatures: &[&[("child", "TYPE")]],
oid: Some(101),
..entry("list", "LIST", None)
},
entry("logical", "BOOLEAN", None),
entry("long", "BIGINT", None),
TypeEntry {
signatures: &[&[("key", "TYPE"), ("value", "TYPE")]],
oid: Some(102),
..entry("map", "MAP", None)
},
entry("null", "NULL", Some(1)),
TypeEntry { signatures: WIDTH_SCALE, ..entry("numeric", "DECIMAL", None) },
TypeEntry { signatures: STRING, ..entry("nvarchar", "VARCHAR", None) },
entry("oid", "BIGINT", None),
entry("real", "FLOAT", None),
TypeEntry { varargs: Some("TYPE"), oid: Some(100), ..entry("row", "STRUCT", None) },
entry("short", "SMALLINT", None),
entry("signed", "INTEGER", None),
entry("smallint", "SMALLINT", None),
TypeEntry { signatures: STRING, ..entry("string", "VARCHAR", None) },
TypeEntry { varargs: Some("TYPE"), ..entry("struct", "STRUCT", None) },
TypeEntry { signatures: STRING, ..entry("text", "VARCHAR", None) },
entry("time", "TIME", Some(16)),
entry("time with time zone", "TIME WITH TIME ZONE", Some(34)),
TypeEntry { signatures: PRECISION, ..entry("timestamp", "TIMESTAMP", None) },
entry("timestamp with time zone", "TIMESTAMP WITH TIME ZONE", Some(32)),
entry("timestamptz", "TIMESTAMP WITH TIME ZONE", None),
entry("timestamp_ms", "TIMESTAMP_MS", Some(18)),
entry("timestamp_ns", "TIMESTAMP_NS", Some(20)),
entry("timestamp_s", "TIMESTAMP_S", Some(17)),
entry("timestamp_us", "TIMESTAMP", None),
entry("timetz", "TIME WITH TIME ZONE", None),
entry("tinyint", "TINYINT", None),
entry("ubigint", "UBIGINT", Some(31)),
entry("uhugeint", "UHUGEINT", Some(49)),
entry("uint128", "UHUGEINT", None),
entry("uint16", "USMALLINT", Some(29)),
entry("uint32", "UINTEGER", Some(30)),
entry("uint64", "UBIGINT", None),
entry("uint8", "UTINYINT", Some(28)),
entry("uinteger", "UINTEGER", None),
TypeEntry { varargs: Some("TYPE"), oid: Some(107), ..entry("union", "UNION", None) },
entry("usmallint", "USMALLINT", None),
entry("utinyint", "UTINYINT", None),
entry("uuid", "UUID", None),
entry("varbinary", "BLOB", None),
TypeEntry { signatures: STRING, ..entry("varchar", "VARCHAR", None) },
];
const fn entry(name: &'static str, logical_type: &'static str, oid: Option<i64>) -> TypeEntry {
TypeEntry { name, logical_type, oid, varargs: None, signatures: BARE }
}
#[must_use]
pub fn type_fields() -> Vec<Field> {
vec![
Field::new("database_name", LogicalType::Varchar),
Field::new("database_oid", LogicalType::BigInt),
Field::new("schema_name", LogicalType::Varchar),
Field::new("schema_oid", LogicalType::BigInt),
Field::new("type_oid", LogicalType::BigInt),
Field::new("type_name", LogicalType::Varchar),
Field::new("type_size", LogicalType::BigInt),
Field::new("logical_type", LogicalType::Varchar),
Field::new("type_category", LogicalType::Varchar),
Field::new("comment", LogicalType::Varchar),
Field::new("tags", LogicalType::map(LogicalType::Varchar, LogicalType::Varchar)),
Field::new("internal", LogicalType::Boolean),
Field::new("extension_name", LogicalType::Varchar),
Field::new("labels", LogicalType::list(LogicalType::Varchar)),
Field::new("parameters", LogicalType::list(LogicalType::Varchar)),
Field::new("parameter_types", LogicalType::list(LogicalType::Varchar)),
Field::new("varargs", LogicalType::Varchar),
]
}
#[must_use]
pub fn representative(logical_type: &str) -> Option<LogicalType> {
Some(match logical_type {
"NULL" => LogicalType::Null,
"BOOLEAN" => LogicalType::Boolean,
"TINYINT" => LogicalType::TinyInt,
"SMALLINT" => LogicalType::SmallInt,
"INTEGER" => LogicalType::Integer,
"BIGINT" => LogicalType::BigInt,
"HUGEINT" => LogicalType::HugeInt,
"UTINYINT" => LogicalType::UTinyInt,
"USMALLINT" => LogicalType::USmallInt,
"UINTEGER" => LogicalType::UInteger,
"UBIGINT" => LogicalType::UBigInt,
"UHUGEINT" => LogicalType::UHugeInt,
"FLOAT" => LogicalType::Float,
"DOUBLE" => LogicalType::Double,
"DECIMAL" => LogicalType::Decimal { width: 18, scale: 3 },
"VARCHAR" => LogicalType::Varchar,
"BLOB" => LogicalType::Blob,
"BIT" => LogicalType::Bit,
"UUID" => LogicalType::Uuid,
"DATE" => LogicalType::Date,
"TIME" => LogicalType::Time,
"TIME WITH TIME ZONE" => LogicalType::TimeTz,
"TIMESTAMP" => LogicalType::Timestamp,
"TIMESTAMP_S" => LogicalType::TimestampS,
"TIMESTAMP_MS" => LogicalType::TimestampMs,
"TIMESTAMP_NS" => LogicalType::TimestampNs,
"TIMESTAMP WITH TIME ZONE" => LogicalType::TimestampTz,
"INTERVAL" => LogicalType::Interval,
"LIST" => LogicalType::list(LogicalType::Integer),
"MAP" => LogicalType::map(LogicalType::Varchar, LogicalType::Varchar),
"STRUCT" => LogicalType::Struct(Vec::new()),
"UNION" => LogicalType::Union(Vec::new()),
_ => return None,
})
}
#[must_use]
pub fn type_oid(logical_type: &str) -> Option<i64> {
TYPE_NAMES
.iter()
.find(|entry| entry.logical_type == logical_type && entry.oid.is_some())
.and_then(|entry| entry.oid)
}
#[must_use]
pub fn type_size(logical_type: &str) -> Option<i64> {
if logical_type == "DECIMAL" {
return None;
}
let ty = representative(logical_type)?;
i64::try_from(ty.physical().size()).ok()
}
#[must_use]
pub fn type_category(logical_type: &str) -> Option<&'static str> {
let ty = representative(logical_type)?;
Some(match ty {
LogicalType::Boolean => "BOOLEAN",
LogicalType::Varchar => "STRING",
LogicalType::TinyInt
| LogicalType::SmallInt
| LogicalType::Integer
| LogicalType::BigInt
| LogicalType::HugeInt
| LogicalType::UTinyInt
| LogicalType::USmallInt
| LogicalType::UInteger
| LogicalType::UBigInt
| LogicalType::UHugeInt
| LogicalType::Float
| LogicalType::Double
| LogicalType::Decimal { .. } => "NUMERIC",
LogicalType::Date
| LogicalType::Time
| LogicalType::TimeTz
| LogicalType::Timestamp
| LogicalType::TimestampS
| LogicalType::TimestampMs
| LogicalType::TimestampNs
| LogicalType::TimestampTz
| LogicalType::Interval => "DATETIME",
LogicalType::List(_)
| LogicalType::Array(_, _)
| LogicalType::Map(_, _)
| LogicalType::Struct(_)
| LogicalType::Union(_) => "COMPOSITE",
_ => return None,
})
}
#[must_use]
pub fn sort_key(name: &str) -> String {
name.to_ascii_lowercase().replace('_', "{")
}
#[cfg(test)]
mod tests {
use rudb_common::LogicalType;
use super::{TYPE_NAMES, sort_key, type_category, type_fields, type_size};
#[test]
fn the_table_is_the_shape_the_pin_returns() {
let rows: usize = TYPE_NAMES.iter().map(|entry| entry.signatures.len()).sum();
assert_eq!(TYPE_NAMES.len(), 73, "names");
assert_eq!(rows, 93, "rows, which is the pin's 104 less the eleven for types we lack");
assert_eq!(type_fields().len(), 17);
}
#[test]
fn the_oid_sits_on_the_first_name_of_its_type() {
for entry in TYPE_NAMES {
let first = TYPE_NAMES
.iter()
.filter(|other| other.logical_type == entry.logical_type)
.min_by_key(|other| sort_key(other.name))
.expect("at least itself");
let expected = entry.name == first.name;
assert_eq!(
entry.oid.is_some(),
expected,
"{} carries an oid and {} is the first name of {}",
entry.name,
first.name,
entry.logical_type
);
}
let mut oids: Vec<i64> = TYPE_NAMES.iter().filter_map(|entry| entry.oid).collect();
oids.sort_unstable();
let total = oids.len();
oids.dedup();
assert_eq!(oids.len(), total, "two names claim the same oid");
assert_eq!(total, 32, "one oid per type this engine has");
}
#[test]
fn the_names_are_in_the_order_the_pin_returns_them() {
let mut sorted: Vec<&str> = TYPE_NAMES.iter().map(|entry| entry.name).collect();
sorted.sort_by_key(|name| sort_key(name));
let listed: Vec<&str> = TYPE_NAMES.iter().map(|entry| entry.name).collect();
assert_eq!(listed, sorted);
assert!(sort_key("timestamptz_ns") < sort_key("timestamp_ms"));
assert!(sort_key("timetz") < sort_key("time_ns"));
assert!(sort_key("time with time zone") < sort_key("timestamp"));
}
#[test]
fn every_name_here_is_a_name_a_cast_can_spell() {
for entry in TYPE_NAMES {
if entry.name == "list" {
assert!(LogicalType::parse("list").is_err(), "the pin refuses this too");
continue;
}
let spelled = match entry.logical_type {
"DECIMAL" => format!("{}(9, 2)", entry.name),
"MAP" => format!("{}(VARCHAR, VARCHAR)", entry.name),
"STRUCT" | "UNION" => format!("{}(a INTEGER)", entry.name),
_ => entry.name.to_string(),
};
let parsed = LogicalType::parse(&spelled)
.unwrap_or_else(|e| panic!("{} does not parse: {e}", entry.name));
let canonical = entry.logical_type.to_string();
let got = parsed.to_string().replace('"', "");
assert!(
got == canonical || got.starts_with(&canonical),
"{} parses to {got} and the table says {canonical}",
entry.name
);
}
}
#[test]
fn the_sizes_are_this_engines_and_three_of_them_differ_from_the_pin() {
assert_eq!(type_size("INTEGER"), Some(4));
assert_eq!(type_size("VARCHAR"), Some(16));
assert_eq!(type_size("HUGEINT"), Some(16));
assert_eq!(type_size("DECIMAL"), None, "the width decides, so there is no one answer");
assert_eq!(type_size("STRUCT"), Some(0), "the parent holds nothing of its own");
assert_eq!(type_size("LIST"), Some(8), "the pin says 16");
assert_eq!(type_size("MAP"), Some(8), "the pin says 16");
assert_eq!(type_size("NULL"), Some(0), "the pin says 4");
}
#[test]
fn four_types_are_in_no_category_and_that_is_the_pins_answer() {
assert_eq!(type_category("BIGINT"), Some("NUMERIC"));
assert_eq!(type_category("DECIMAL"), Some("NUMERIC"));
assert_eq!(type_category("VARCHAR"), Some("STRING"));
assert_eq!(type_category("BOOLEAN"), Some("BOOLEAN"));
assert_eq!(type_category("INTERVAL"), Some("DATETIME"));
assert_eq!(type_category("MAP"), Some("COMPOSITE"));
for uncategorised in ["NULL", "BIT", "BLOB", "UUID"] {
assert_eq!(type_category(uncategorised), None, "{uncategorised}");
}
}
#[test]
fn two_names_for_one_type_can_take_different_modifiers() {
let of = |name: &str| {
TYPE_NAMES.iter().find(|entry| entry.name == name).expect("a name in the table")
};
assert_eq!(of("timestamp").signatures.len(), 2, "timestamp takes a precision");
assert_eq!(of("timestamp_us").signatures.len(), 1, "the same type, and it does not");
assert_eq!(of("varchar").signatures.len(), 3);
assert_eq!(of("blob").signatures.len(), 1);
assert_eq!(of("bpchar").signatures[1], [("length", "BIGINT")]);
assert_eq!(of("bpchar").signatures[2], [("collation", "VARCHAR")]);
}
}