use heck::ToUpperCamelCase;
use super::rust_type::{BaseType, TYPES_MODULE};
use crate::IdGeneration;
#[derive(Debug, Clone)]
pub struct Schema {
pub tables: Vec<Table>,
pub enums: Vec<EnumType>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumType {
pub schema: Option<String>,
pub sql_name: String,
pub rust_name: String,
pub variants: Vec<EnumVariant>,
pub column: Option<String>,
}
impl EnumType {
pub fn display_name(&self) -> String {
match &self.schema {
Some(schema) => format!("{schema}.{}", self.sql_name),
None => self.sql_name.clone(),
}
}
pub fn rust_path(&self) -> String {
format!("{TYPES_MODULE}::{}", self.rust_name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumVariant {
pub label: String,
pub rust_name: String,
}
#[derive(Debug, Clone)]
pub struct Table {
pub schema: Option<String>,
pub sql_name: String,
pub module_name: String,
pub struct_name: String,
pub columns: Vec<Column>,
pub primary_key: Vec<String>,
pub foreign_keys: Vec<ForeignKey>,
pub unique_keys: Vec<UniqueKey>,
pub key_constraints: Vec<KeyConstraint>,
pub version_column: Option<String>,
pub soft_delete_column: Option<String>,
pub tenant_column: Option<String>,
pub relationships: Vec<Relationship>,
pub before_insert_triggers: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Relationship {
pub name: String,
pub kind: RelationshipKind,
pub target: String,
pub column: String,
pub target_column: String,
pub through: Option<Through>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Through {
pub table: String,
pub column: String,
pub target_column: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelationshipKind {
BelongsTo,
HasMany,
HasOne,
ManyToMany,
}
impl RelationshipKind {
pub fn is_to_many(self) -> bool {
matches!(
self,
RelationshipKind::HasMany | RelationshipKind::ManyToMany
)
}
}
impl Table {
pub fn display_name(&self) -> String {
match &self.schema {
Some(schema) => format!("{schema}.{}", self.sql_name),
None => self.sql_name.clone(),
}
}
pub fn unique_key_columns(&self, soft_delete_column: Option<&str>) -> Vec<Vec<String>> {
let mut keys: Vec<Vec<String>> = Vec::new();
if !self.primary_key.is_empty() {
keys.push(self.primary_key.clone());
}
let declared = self
.unique_keys
.iter()
.filter(|key| key.counts(soft_delete_column));
for key in declared {
let same = |other: &Vec<String>| {
other.len() == key.columns.len()
&& key.columns.iter().all(|column| other.contains(column))
};
if !keys.iter().any(same) {
keys.push(key.columns.clone());
}
}
keys
}
pub fn primary_key_columns(&self) -> Vec<&Column> {
self.primary_key
.iter()
.filter_map(|key| self.columns.iter().find(|column| column.sql_name == *key))
.collect()
}
pub fn key_columns(&self) -> Vec<&Column> {
self.primary_key_columns()
.into_iter()
.filter(|column| !self.is_tenant_column(column))
.collect()
}
pub fn tenant(&self) -> Option<&Column> {
let name = self.tenant_column.as_deref()?;
self.columns.iter().find(|column| column.sql_name == name)
}
pub fn is_soft_delete_column(&self, column: &Column) -> bool {
self.soft_delete_column.as_deref() == Some(column.sql_name.as_str())
}
pub fn is_tenant_column(&self, column: &Column) -> bool {
self.tenant_column.as_deref() == Some(column.sql_name.as_str())
}
fn is_convention_controlled(&self, column: &Column) -> bool {
self.is_soft_delete_column(column) || self.is_tenant_column(column)
}
pub fn insert_columns(&self) -> Vec<&Column> {
self.columns
.iter()
.filter(|column| column.is_insertable() && !self.is_convention_controlled(column))
.collect()
}
pub fn update_columns(&self) -> Vec<&Column> {
self.columns
.iter()
.filter(|column| column.is_updatable() && !self.is_convention_controlled(column))
.collect()
}
}
#[derive(Debug, Clone)]
pub struct Column {
pub sql_name: String,
pub rust_name: String,
pub rust_type: String,
pub sqlx_type: String,
pub nullable: bool,
pub is_primary_key: bool,
pub has_default: bool,
pub is_generated: bool,
pub value_list: Option<Vec<String>>,
pub declared_length: Option<u32>,
pub value_range: Option<ValueRange>,
pub length_range: Option<LengthRange>,
pub typed_id: Option<TypedId>,
pub is_managed: bool,
pub id_generation: Option<IdGeneration>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValueRange {
pub min: Option<NumberBound>,
pub max: Option<NumberBound>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NumberBound {
pub value: String,
pub exclusive: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LengthRange {
pub min: Option<u32>,
pub max: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypedId {
pub module: String,
pub name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterKind {
Ordered,
Text,
Equality,
}
impl Column {
pub fn filter_kind(&self) -> Option<FilterKind> {
match self.base_type() {
BaseType::Integer
| BaseType::Float
| BaseType::Decimal
| BaseType::Date
| BaseType::Time
| BaseType::Timestamp
| BaseType::TimestampTz => Some(FilterKind::Ordered),
BaseType::Text => Some(FilterKind::Text),
BaseType::Bool
| BaseType::Uuid
| BaseType::Enum
| BaseType::Interval
| BaseType::Bytes
| BaseType::TimeTz
| BaseType::Money
| BaseType::Range
| BaseType::IpNetwork
| BaseType::MacAddress => Some(FilterKind::Equality),
BaseType::Array | BaseType::Other => None,
}
}
pub fn base_type(&self) -> BaseType {
BaseType::of(&self.sqlx_type)
}
pub fn sort_variant(&self) -> String {
let camel = self.rust_name.trim_matches('_').to_upper_camel_case();
if camel.starts_with(|character: char| character.is_ascii_digit()) {
format!("Column{camel}")
} else {
camel
}
}
pub fn null_check_field(&self) -> String {
format!("{}_is_null", self.rust_name.trim_end_matches('_'))
}
pub fn is_insertable(&self) -> bool {
!self.is_generated && !self.is_managed
}
pub fn is_optional_in_input(&self) -> bool {
self.is_omittable_on_insert() || self.id_generation.is_some()
}
pub fn is_omittable_on_insert(&self) -> bool {
self.is_insertable() && self.has_default && self.id_generation.is_none()
}
pub fn new_id_expr(&self) -> Option<&'static str> {
let text = self.base_type() == BaseType::Text;
Some(match (self.id_generation?, text) {
(IdGeneration::UuidV4, _) => "uuid::Uuid::new_v4()",
(IdGeneration::UuidV7, _) => "uuid::Uuid::now_v7()",
(IdGeneration::Ulid, false) => "uuid::Uuid::from(ulid::Ulid::new())",
(IdGeneration::Ulid, true) => "ulid::Ulid::new().to_string()",
})
}
pub fn is_updatable(&self) -> bool {
!self.is_generated && !self.is_managed && !self.is_primary_key
}
pub fn value_type(&self) -> &str {
match &self.typed_id {
Some(id) => &id.name,
None => &self.sqlx_type,
}
}
pub fn field_type(&self) -> String {
if self.nullable {
format!("Option<{}>", self.value_type())
} else {
self.value_type().to_owned()
}
}
pub fn supports_typed_id(&self) -> bool {
self.base_type().supports_typed_id()
}
pub fn is_copy(&self) -> bool {
self.supports_typed_id() && self.base_type() != BaseType::Text
}
}
#[derive(Debug, Clone)]
pub struct ForeignKey {
pub column: String,
pub referenced_schema: Option<String>,
pub referenced_table: String,
pub referenced_column: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyConstraint {
pub name: String,
pub columns: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UniqueKey {
pub columns: Vec<String>,
pub predicate: Option<String>,
}
impl UniqueKey {
pub fn new(columns: Vec<String>) -> Self {
Self {
columns,
predicate: None,
}
}
pub fn counts(&self, soft_delete_column: Option<&str>) -> bool {
let Some(predicate) = &self.predicate else {
return true;
};
let Some(column) = soft_delete_column else {
return false;
};
let mut condition = predicate.trim();
while let Some(inner) = condition
.strip_prefix('(')
.and_then(|rest| rest.strip_suffix(')'))
{
condition = inner.trim();
}
let Some(subject) = condition
.strip_suffix("IS NULL")
.or_else(|| condition.strip_suffix("is null"))
else {
return false;
};
let subject = subject.trim();
subject == column || subject == format!("\"{}\"", column.replace('"', "\"\""))
}
}
#[cfg(test)]
mod tests {
use super::UniqueKey;
fn partial(predicate: &str) -> UniqueKey {
UniqueKey {
columns: vec!["sku".to_owned()],
predicate: Some(predicate.to_owned()),
}
}
#[test]
fn a_partial_index_counts_only_on_exactly_the_soft_delete_condition() {
let soft_delete = Some("deleted_at");
assert!(UniqueKey::new(vec!["sku".to_owned()]).counts(None));
assert!(partial("deleted_at IS NULL").counts(soft_delete));
assert!(partial("(deleted_at IS NULL)").counts(soft_delete));
assert!(!partial("deleted_at IS NULL").counts(None));
assert!(!partial("archived_at IS NULL").counts(soft_delete));
assert!(!partial("deleted_at IS NOT NULL").counts(soft_delete));
assert!(!partial("deleted_at IS NULL AND sku <> ''").counts(soft_delete));
}
}