use crate::tokens::Trivia;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StoreColumnType {
Uuid,
Text,
Int,
BigInt,
Float,
Double,
Bool,
Timestamptz,
Timestamp,
Date,
Time,
Jsonb,
Json,
Bytea,
Numeric,
}
impl StoreColumnType {
pub const ALL: &'static [StoreColumnType] = &[
StoreColumnType::Uuid,
StoreColumnType::Text,
StoreColumnType::Int,
StoreColumnType::BigInt,
StoreColumnType::Float,
StoreColumnType::Double,
StoreColumnType::Bool,
StoreColumnType::Timestamptz,
StoreColumnType::Timestamp,
StoreColumnType::Date,
StoreColumnType::Time,
StoreColumnType::Jsonb,
StoreColumnType::Json,
StoreColumnType::Bytea,
StoreColumnType::Numeric,
];
pub fn canonical_name(self) -> &'static str {
match self {
StoreColumnType::Uuid => "Uuid",
StoreColumnType::Text => "Text",
StoreColumnType::Int => "Int",
StoreColumnType::BigInt => "BigInt",
StoreColumnType::Float => "Float",
StoreColumnType::Double => "Double",
StoreColumnType::Bool => "Bool",
StoreColumnType::Timestamptz => "Timestamptz",
StoreColumnType::Timestamp => "Timestamp",
StoreColumnType::Date => "Date",
StoreColumnType::Time => "Time",
StoreColumnType::Jsonb => "Jsonb",
StoreColumnType::Json => "Json",
StoreColumnType::Bytea => "Bytea",
StoreColumnType::Numeric => "Numeric",
}
}
pub fn from_token(name: &str) -> Option<StoreColumnType> {
for &t in Self::ALL {
if t.canonical_name() == name {
return Some(t);
}
}
match name.to_ascii_lowercase().as_str() {
"int" | "integer" | "int4" => Some(StoreColumnType::Int),
"bigint" | "int8" => Some(StoreColumnType::BigInt),
"bool" | "boolean" => Some(StoreColumnType::Bool),
"text" | "varchar" | "string" => Some(StoreColumnType::Text),
"uuid" => Some(StoreColumnType::Uuid),
"float" | "float4" | "real" => Some(StoreColumnType::Float),
"double" | "float8" => Some(StoreColumnType::Double),
"timestamptz" => Some(StoreColumnType::Timestamptz),
"timestamp" => Some(StoreColumnType::Timestamp),
"date" => Some(StoreColumnType::Date),
"time" => Some(StoreColumnType::Time),
"jsonb" => Some(StoreColumnType::Jsonb),
"json" => Some(StoreColumnType::Json),
"bytea" => Some(StoreColumnType::Bytea),
"numeric" | "decimal" => Some(StoreColumnType::Numeric),
_ => None,
}
}
pub fn all_canonical_names() -> Vec<&'static str> {
Self::ALL.iter().map(|t| t.canonical_name()).collect()
}
}
impl std::fmt::Display for StoreColumnType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.canonical_name())
}
}
#[derive(Debug, Clone)]
pub struct StoreColumn {
pub name: String,
pub col_type: StoreColumnType,
pub primary_key: bool,
pub auto_increment: bool,
pub not_null: bool,
pub unique: bool,
pub default_value: String,
pub identity: bool,
pub line: u32,
pub column: u32,
}
#[derive(Debug, Clone)]
pub enum StoreColumnSchema {
Inline {
columns: Vec<StoreColumn>,
leading_trivia: Vec<Trivia>,
line: u32,
column: u32,
},
ManifestRef {
qualified_name: String,
line: u32,
column: u32,
},
EnvVar {
var_name: String,
line: u32,
column: u32,
},
}
impl StoreColumnSchema {
pub fn is_inline(&self) -> bool {
matches!(self, StoreColumnSchema::Inline { .. })
}
pub fn inline_columns(&self) -> Option<&[StoreColumn]> {
match self {
StoreColumnSchema::Inline { columns, .. } => Some(columns),
_ => None,
}
}
pub fn loc(&self) -> (u32, u32) {
match self {
StoreColumnSchema::Inline { line, column, .. }
| StoreColumnSchema::ManifestRef { line, column, .. }
| StoreColumnSchema::EnvVar { line, column, .. } => (*line, *column),
}
}
pub fn form_name(&self) -> &'static str {
match self {
StoreColumnSchema::Inline { .. } => "inline",
StoreColumnSchema::ManifestRef { .. } => "manifest_ref",
StoreColumnSchema::EnvVar { .. } => "env_var",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn catalog_has_exactly_15_variants() {
assert_eq!(StoreColumnType::ALL.len(), 15);
}
#[test]
fn every_variant_has_a_unique_canonical_name() {
let mut names: Vec<&'static str> =
StoreColumnType::ALL.iter().map(|t| t.canonical_name()).collect();
names.sort();
let total = names.len();
names.dedup();
assert_eq!(
names.len(),
total,
"canonical names must be unique across the catalog"
);
}
#[test]
fn every_canonical_name_parses_back_to_its_variant() {
for &t in StoreColumnType::ALL {
assert_eq!(
StoreColumnType::from_token(t.canonical_name()),
Some(t),
"{} did not round-trip",
t.canonical_name()
);
}
}
#[test]
fn common_aliases_resolve_to_the_canonical_variant() {
for (alias, expected) in [
("int", StoreColumnType::Int),
("integer", StoreColumnType::Int),
("int4", StoreColumnType::Int),
("bigint", StoreColumnType::BigInt),
("int8", StoreColumnType::BigInt),
("bool", StoreColumnType::Bool),
("boolean", StoreColumnType::Bool),
("text", StoreColumnType::Text),
("varchar", StoreColumnType::Text),
("string", StoreColumnType::Text),
("uuid", StoreColumnType::Uuid),
("float", StoreColumnType::Float),
("real", StoreColumnType::Float),
("double", StoreColumnType::Double),
("float8", StoreColumnType::Double),
("numeric", StoreColumnType::Numeric),
("decimal", StoreColumnType::Numeric),
("timestamptz", StoreColumnType::Timestamptz),
("timestamp", StoreColumnType::Timestamp),
("date", StoreColumnType::Date),
("time", StoreColumnType::Time),
("jsonb", StoreColumnType::Jsonb),
("json", StoreColumnType::Json),
("bytea", StoreColumnType::Bytea),
] {
assert_eq!(
StoreColumnType::from_token(alias),
Some(expected),
"alias `{alias}` did not resolve to `{}`",
expected.canonical_name()
);
}
}
#[test]
fn alias_lookup_is_case_insensitive_on_the_alias_table() {
assert_eq!(StoreColumnType::from_token("INTEGER"), Some(StoreColumnType::Int));
assert_eq!(StoreColumnType::from_token("Boolean"), Some(StoreColumnType::Bool));
assert_eq!(StoreColumnType::from_token("UUID"), Some(StoreColumnType::Uuid));
}
#[test]
fn unknown_type_names_return_none() {
for unknown in [
"Money", "Interval", "Cidr", "Inet", "Macaddr", "Geometry",
"enum", "domain", "citext", "array", "anything", "", " ",
"Tier", "MyCustomType",
] {
assert_eq!(
StoreColumnType::from_token(unknown),
None,
"unknown type `{unknown}` must not resolve"
);
}
}
#[test]
fn display_is_canonical_name() {
for &t in StoreColumnType::ALL {
assert_eq!(t.to_string(), t.canonical_name());
}
}
#[test]
fn schema_form_names_are_the_three_closed_forms() {
let inline = StoreColumnSchema::Inline {
columns: vec![],
leading_trivia: vec![],
line: 0,
column: 0,
};
let manifest_ref = StoreColumnSchema::ManifestRef {
qualified_name: "public.tenants".into(),
line: 0,
column: 0,
};
let env_var = StoreColumnSchema::EnvVar {
var_name: "TENANT_SCHEMA".into(),
line: 0,
column: 0,
};
assert_eq!(inline.form_name(), "inline");
assert_eq!(manifest_ref.form_name(), "manifest_ref");
assert_eq!(env_var.form_name(), "env_var");
assert!(inline.is_inline());
assert!(!manifest_ref.is_inline());
assert!(!env_var.is_inline());
assert!(inline.inline_columns().is_some());
assert!(manifest_ref.inline_columns().is_none());
assert!(env_var.inline_columns().is_none());
}
}