use serde::{Deserialize, Serialize};
use crate::schema::{
foreign_key::ForeignKeySyntax,
names::ColumnName,
primary_key::PrimaryKeySyntax,
str_or_bool::{StrOrBoolOrArray, StringOrBool},
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub struct ColumnDef {
pub name: ColumnName,
pub r#type: ColumnType,
pub nullable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<StringOrBool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub primary_key: Option<PrimaryKeySyntax>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unique: Option<StrOrBoolOrArray>,
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<StrOrBoolOrArray>,
#[serde(skip_serializing_if = "Option::is_none")]
pub foreign_key: Option<ForeignKeySyntax>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case", untagged)]
pub enum ColumnType {
Simple(SimpleColumnType),
Complex(ComplexColumnType),
}
impl ColumnType {
pub fn supports_auto_increment(&self) -> bool {
match self {
ColumnType::Simple(ty) => ty.supports_auto_increment(),
ColumnType::Complex(_) => false,
}
}
pub fn requires_migration(&self, other: &ColumnType) -> bool {
match (self, other) {
(
ColumnType::Complex(ComplexColumnType::Enum {
values: values1, ..
}),
ColumnType::Complex(ComplexColumnType::Enum {
values: values2, ..
}),
) => {
if values1.is_integer() && values2.is_integer() {
false
} else {
values1 != values2
}
}
_ => self != other,
}
}
pub fn to_rust_type(&self, nullable: bool) -> String {
let base: &'static str = match self {
ColumnType::Simple(ty) => match ty {
SimpleColumnType::SmallInt => "i16",
SimpleColumnType::Integer => "i32",
SimpleColumnType::BigInt => "i64",
SimpleColumnType::Real => "f32",
SimpleColumnType::DoublePrecision => "f64",
SimpleColumnType::Text
| SimpleColumnType::Interval
| SimpleColumnType::Inet
| SimpleColumnType::Cidr
| SimpleColumnType::Macaddr
| SimpleColumnType::Xml => "String",
SimpleColumnType::Boolean => "bool",
SimpleColumnType::Date => "Date",
SimpleColumnType::Time => "Time",
SimpleColumnType::Timestamp => "DateTime",
SimpleColumnType::Timestamptz => "DateTimeWithTimeZone",
SimpleColumnType::Bytea => "Vec<u8>",
SimpleColumnType::Uuid => "Uuid",
SimpleColumnType::Json => "Json",
},
ColumnType::Complex(ty) => match ty {
ComplexColumnType::Numeric { .. } => "Decimal",
ComplexColumnType::Varchar { .. }
| ComplexColumnType::Char { .. }
| ComplexColumnType::Custom { .. }
| ComplexColumnType::Enum { .. } => "String",
},
};
if nullable {
format!("Option<{base}>")
} else {
base.to_string()
}
}
pub fn to_display_string(&self) -> String {
match self {
ColumnType::Simple(ty) => ty.to_display_string(),
ColumnType::Complex(ty) => ty.to_display_string(),
}
}
#[must_use]
pub fn display_label(&self) -> String {
match self {
ColumnType::Simple(simple) => simple.model_name().to_string(),
ColumnType::Complex(complex) => complex.display_label(),
}
}
pub fn default_fill_value(&self) -> &'static str {
match self {
ColumnType::Simple(ty) => ty.default_fill_value(),
ColumnType::Complex(ty) => ty.default_fill_value(),
}
}
pub fn enum_variant_names(&self) -> Option<Vec<String>> {
let ColumnType::Complex(ComplexColumnType::Enum { values, .. }) = self else {
return None;
};
Some(match values {
EnumValues::String(v) => v.clone(),
EnumValues::Integer(v) => v.iter().map(|n| n.name.clone()).collect(),
})
}
}
impl ColumnDef {
#[must_use]
pub fn new(name: impl Into<ColumnName>, r#type: ColumnType, nullable: bool) -> Self {
Self {
name: name.into(),
r#type,
nullable,
default: None,
comment: None,
primary_key: None,
unique: None,
index: None,
foreign_key: None,
}
}
#[must_use]
pub fn primary_key(mut self, pk: PrimaryKeySyntax) -> Self {
self.primary_key = Some(pk);
self
}
#[must_use]
pub fn unique(mut self, unique: StrOrBoolOrArray) -> Self {
self.unique = Some(unique);
self
}
#[must_use]
pub fn index(mut self, index: StrOrBoolOrArray) -> Self {
self.index = Some(index);
self
}
#[must_use]
pub fn foreign_key(mut self, fk: ForeignKeySyntax) -> Self {
self.foreign_key = Some(fk);
self
}
#[must_use]
pub fn default(mut self, default: StringOrBool) -> Self {
self.default = Some(default);
self
}
#[must_use]
pub fn comment(mut self, comment: impl Into<String>) -> Self {
self.comment = Some(comment.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SimpleColumnType {
SmallInt,
Integer,
BigInt,
Real,
DoublePrecision,
Text,
Boolean,
Date,
Time,
Timestamp,
Timestamptz,
Interval,
Bytea,
Uuid,
Json,
Inet,
Cidr,
Macaddr,
Xml,
}
impl SimpleColumnType {
#[must_use]
pub fn sql_type(&self) -> &'static str {
match self {
SimpleColumnType::SmallInt => "SMALLINT",
SimpleColumnType::Integer => "INTEGER",
SimpleColumnType::BigInt => "BIGINT",
SimpleColumnType::Real => "REAL",
SimpleColumnType::DoublePrecision => "DOUBLE PRECISION",
SimpleColumnType::Text => "TEXT",
SimpleColumnType::Boolean => "BOOLEAN",
SimpleColumnType::Date => "DATE",
SimpleColumnType::Time => "TIME",
SimpleColumnType::Timestamp => "TIMESTAMP",
SimpleColumnType::Timestamptz => "TIMESTAMPTZ",
SimpleColumnType::Interval => "INTERVAL",
SimpleColumnType::Bytea => "BYTEA",
SimpleColumnType::Uuid => "UUID",
SimpleColumnType::Json => "JSON",
SimpleColumnType::Inet => "INET",
SimpleColumnType::Cidr => "CIDR",
SimpleColumnType::Macaddr => "MACADDR",
SimpleColumnType::Xml => "XML",
}
}
#[must_use]
pub fn model_name(&self) -> &'static str {
match self {
SimpleColumnType::SmallInt => "small_int",
SimpleColumnType::Integer => "integer",
SimpleColumnType::BigInt => "big_int",
SimpleColumnType::Real => "real",
SimpleColumnType::DoublePrecision => "double_precision",
SimpleColumnType::Text => "text",
SimpleColumnType::Boolean => "boolean",
SimpleColumnType::Date => "date",
SimpleColumnType::Time => "time",
SimpleColumnType::Timestamp => "timestamp",
SimpleColumnType::Timestamptz => "timestamptz",
SimpleColumnType::Interval => "interval",
SimpleColumnType::Bytea => "bytea",
SimpleColumnType::Uuid => "uuid",
SimpleColumnType::Json => "json",
SimpleColumnType::Inet => "inet",
SimpleColumnType::Cidr => "cidr",
SimpleColumnType::Macaddr => "macaddr",
SimpleColumnType::Xml => "xml",
}
}
pub fn supports_auto_increment(&self) -> bool {
matches!(
self,
SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt
)
}
#[must_use]
pub fn display_str(&self) -> &'static str {
match self {
SimpleColumnType::SmallInt => "smallint",
SimpleColumnType::Integer => "integer",
SimpleColumnType::BigInt => "bigint",
SimpleColumnType::Real => "real",
SimpleColumnType::DoublePrecision => "double precision",
SimpleColumnType::Text => "text",
SimpleColumnType::Boolean => "boolean",
SimpleColumnType::Date => "date",
SimpleColumnType::Time => "time",
SimpleColumnType::Timestamp => "timestamp",
SimpleColumnType::Timestamptz => "timestamptz",
SimpleColumnType::Interval => "interval",
SimpleColumnType::Bytea => "bytea",
SimpleColumnType::Uuid => "uuid",
SimpleColumnType::Json => "json",
SimpleColumnType::Inet => "inet",
SimpleColumnType::Cidr => "cidr",
SimpleColumnType::Macaddr => "macaddr",
SimpleColumnType::Xml => "xml",
}
}
#[must_use]
pub fn to_display_string(&self) -> String {
self.display_str().to_string()
}
pub fn default_fill_value(&self) -> &'static str {
match self {
SimpleColumnType::SmallInt | SimpleColumnType::Integer | SimpleColumnType::BigInt => {
"0"
}
SimpleColumnType::Real | SimpleColumnType::DoublePrecision => "0.0",
SimpleColumnType::Boolean => "false",
SimpleColumnType::Text | SimpleColumnType::Bytea => "''",
SimpleColumnType::Date => "'1970-01-01'",
SimpleColumnType::Time => "'00:00:00'",
SimpleColumnType::Timestamp | SimpleColumnType::Timestamptz => "CURRENT_TIMESTAMP",
SimpleColumnType::Interval => "'0'",
SimpleColumnType::Uuid => "'00000000-0000-0000-0000-000000000000'",
SimpleColumnType::Json => "'{}'",
SimpleColumnType::Inet | SimpleColumnType::Cidr => "'0.0.0.0'",
SimpleColumnType::Macaddr => "'00:00:00:00:00:00'",
SimpleColumnType::Xml => "'<xml/>'",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct NumValue {
pub name: String,
pub value: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
pub enum EnumValues {
String(Vec<String>),
Integer(Vec<NumValue>),
}
impl EnumValues {
pub fn is_string(&self) -> bool {
matches!(self, EnumValues::String(_))
}
pub fn is_integer(&self) -> bool {
matches!(self, EnumValues::Integer(_))
}
#[must_use]
pub fn variant_names_joined(&self, separator: &str) -> String {
let mut out = String::new();
match self {
EnumValues::String(values) => {
for (i, s) in values.iter().enumerate() {
if i > 0 {
out.push_str(separator);
}
out.push_str(s);
}
}
EnumValues::Integer(values) => {
for (i, v) in values.iter().enumerate() {
if i > 0 {
out.push_str(separator);
}
out.push_str(&v.name);
}
}
}
out
}
pub fn len(&self) -> usize {
match self {
EnumValues::String(values) => values.len(),
EnumValues::Integer(values) => values.len(),
}
}
pub fn is_empty(&self) -> bool {
match self {
EnumValues::String(values) => values.is_empty(),
EnumValues::Integer(values) => values.is_empty(),
}
}
#[must_use]
pub fn contains_value(&self, value: &str) -> bool {
match self {
EnumValues::String(variants) => variants.iter().any(|v| v == value),
EnumValues::Integer(variants) => value.parse::<i64>().map_or_else(
|_| variants.iter().any(|v| v.name == value),
|n| variants.iter().any(|v| v.value == n),
),
}
}
#[must_use]
pub fn sql_values_joined(&self, separator: &str) -> String {
use std::fmt::Write as _;
let mut out = String::new();
match self {
EnumValues::String(values) => {
for (i, s) in values.iter().enumerate() {
if i > 0 {
out.push_str(separator);
}
out.push('\'');
out.push_str(&crate::escape_sql_string_literal(s));
out.push('\'');
}
}
EnumValues::Integer(values) => {
for (i, v) in values.iter().enumerate() {
if i > 0 {
out.push_str(separator);
}
write!(out, "{}", v.value).expect("writing an i64 to a String never fails");
}
}
}
out
}
}
impl From<Vec<String>> for EnumValues {
fn from(values: Vec<String>) -> Self {
EnumValues::String(values)
}
}
impl From<Vec<&str>> for EnumValues {
fn from(values: Vec<&str>) -> Self {
EnumValues::String(
values
.into_iter()
.map(std::string::ToString::to_string)
.collect(),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case", tag = "kind")]
#[non_exhaustive]
pub enum ComplexColumnType {
Varchar { length: u32 },
Numeric { precision: u32, scale: u32 },
Char { length: u32 },
Custom { custom_type: String },
Enum { name: String, values: EnumValues },
}
impl ComplexColumnType {
#[must_use]
pub fn sql_type(&self) -> &'static str {
match self {
ComplexColumnType::Varchar { .. } => "VARCHAR",
ComplexColumnType::Numeric { .. } => "NUMERIC",
ComplexColumnType::Char { .. } => "CHAR",
ComplexColumnType::Custom { .. } => "CUSTOM",
ComplexColumnType::Enum { .. } => "ENUM",
}
}
pub fn to_display_string(&self) -> String {
match self {
ComplexColumnType::Varchar { length } => format!("varchar({length})"),
ComplexColumnType::Numeric { precision, scale } => {
format!("numeric({precision},{scale})")
}
ComplexColumnType::Char { length } => format!("char({length})"),
ComplexColumnType::Custom { custom_type } => custom_type.to_lowercase(),
ComplexColumnType::Enum { name, values } => {
if values.is_integer() {
format!("enum<{name}> (integer)")
} else {
format!("enum<{name}>")
}
}
}
}
#[must_use]
pub fn display_label(&self) -> String {
match self {
ComplexColumnType::Varchar { length } => format!("varchar({length})"),
ComplexColumnType::Char { length } => format!("char({length})"),
ComplexColumnType::Numeric { precision, scale } => {
format!("numeric({precision}, {scale})")
}
ComplexColumnType::Custom { custom_type } => format!("custom({custom_type})"),
ComplexColumnType::Enum { name, .. } => format!("enum({name})"),
}
}
pub fn default_fill_value(&self) -> &'static str {
match self {
ComplexColumnType::Numeric { .. } => "0",
ComplexColumnType::Varchar { .. }
| ComplexColumnType::Char { .. }
| ComplexColumnType::Custom { .. }
| ComplexColumnType::Enum { .. } => "''",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case::varchar(ComplexColumnType::Varchar { length: 32 }, "varchar(32)")]
#[case::char(ComplexColumnType::Char { length: 2 }, "char(2)")]
#[case::numeric(ComplexColumnType::Numeric { precision: 10, scale: 2 }, "numeric(10, 2)")]
#[case::custom(ComplexColumnType::Custom { custom_type: "TSVECTOR".into() }, "custom(TSVECTOR)")]
#[case::enum_type(
ComplexColumnType::Enum {
name: "status".into(),
values: EnumValues::String(vec!["active".into()]),
},
"enum(status)"
)]
fn complex_display_label_renders_wire_format(
#[case] complex: ComplexColumnType,
#[case] expected: &str,
) {
assert_eq!(complex.display_label(), expected);
assert_eq!(ColumnType::Complex(complex).display_label(), expected);
}
#[rstest]
#[case::integer(SimpleColumnType::Integer, "integer")]
#[case::big_int(SimpleColumnType::BigInt, "big_int")]
#[case::timestamptz(SimpleColumnType::Timestamptz, "timestamptz")]
fn simple_display_label_uses_the_model_name(
#[case] simple: SimpleColumnType,
#[case] expected: &str,
) {
assert_eq!(ColumnType::Simple(simple).display_label(), expected);
}
}