use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::models::types::DataType;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct TableId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ColumnId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ConstraintId(pub u32);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Column {
pub id: ColumnId,
pub name: String,
pub data_type: DataType,
pub nullable: bool,
pub is_primary: bool,
pub is_foreign: bool,
pub is_unique: bool,
pub default: Option<String>,
pub comment: String,
}
impl Column {
pub fn new(id: ColumnId, name: impl Into<String>, data_type: DataType) -> Self {
Self {
id,
name: name.into(),
data_type,
nullable: false,
is_primary: false,
is_foreign: false,
is_unique: false,
default: None,
comment: String::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum ReferentialAction {
#[default]
NoAction,
Restrict,
Cascade,
SetNull,
SetDefault,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ForeignKey {
pub columns: Vec<ColumnId>,
pub references_table: TableId,
pub references_columns: Vec<ColumnId>,
pub on_update: ReferentialAction,
pub on_delete: ReferentialAction,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ConstraintKind {
PrimaryKey {
columns: Vec<ColumnId>,
},
Unique {
columns: Vec<ColumnId>,
},
ForeignKey(ForeignKey),
Check {
expression: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Constraint {
pub id: ConstraintId,
pub name: Option<String>,
pub kind: ConstraintKind,
}
impl Constraint {
pub fn is_primary_key(&self) -> bool {
matches!(self.kind, ConstraintKind::PrimaryKey { .. })
}
pub fn is_foreign_key(&self) -> bool {
matches!(self.kind, ConstraintKind::ForeignKey(_))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Table {
pub id: TableId,
pub name: String,
pub columns: IndexMap<ColumnId, Column>,
pub constraints: IndexMap<ConstraintId, Constraint>,
pub comment: String,
}
impl Table {
pub fn columns_iter(&self) -> impl Iterator<Item = &Column> {
self.columns.values()
}
pub fn primary_key(&self) -> Option<&Constraint> {
self.constraints.values().find(|c| c.is_primary_key())
}
pub fn primary_key_columns(&self) -> &[ColumnId] {
match self.primary_key().map(|c| &c.kind) {
Some(ConstraintKind::PrimaryKey { columns }) => columns,
_ => &[],
}
}
pub fn column(&self, id: ColumnId) -> Option<&Column> {
self.columns.get(&id)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LogicalModel {
pub name: String,
next_id: u32,
pub tables: IndexMap<TableId, Table>,
}
impl LogicalModel {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into(), ..Self::default() }
}
pub(crate) fn mint(&mut self) -> u32 {
self.next_id = self.next_id.checked_add(1).expect("ID space exhausted");
self.next_id
}
pub fn add_table(&mut self, name: impl Into<String>) -> TableId {
let id = TableId(self.mint());
self.tables.insert(
id,
Table {
id,
name: name.into(),
columns: IndexMap::new(),
constraints: IndexMap::new(),
comment: String::new(),
},
);
id
}
pub fn table(&self, id: TableId) -> Result<&Table> {
self.tables
.get(&id)
.ok_or_else(|| Error::UnknownReference { kind: "table", id: format!("{}", id.0) })
}
pub fn table_mut(&mut self, id: TableId) -> Result<&mut Table> {
self.tables
.get_mut(&id)
.ok_or_else(|| Error::UnknownReference { kind: "table", id: format!("{}", id.0) })
}
pub fn add_column(
&mut self,
table: TableId,
name: impl Into<String>,
data_type: DataType,
) -> Result<ColumnId> {
let id = ColumnId(self.mint());
let column = Column::new(id, name, data_type);
self.table_mut(table)?.columns.insert(id, column);
Ok(id)
}
pub fn set_primary_key(&mut self, table: TableId, columns: Vec<ColumnId>) -> Result<ConstraintId> {
{
let t = self.table_mut(table)?;
for c in &columns {
let col = t.columns.get_mut(c).ok_or_else(|| Error::UnknownReference {
kind: "column",
id: format!("{}", c.0),
})?;
col.is_primary = true;
col.nullable = false;
}
let existing_pk: Vec<ConstraintId> = t
.constraints
.iter()
.filter_map(|(id, c)| c.is_primary_key().then_some(*id))
.collect();
for id in existing_pk {
t.constraints.shift_remove(&id);
}
}
let id = ConstraintId(self.mint());
let t = self.table_mut(table)?;
t.constraints
.insert(id, Constraint { id, name: None, kind: ConstraintKind::PrimaryKey { columns } });
Ok(id)
}
pub fn add_foreign_key(&mut self, table: TableId, fk: ForeignKey) -> Result<ConstraintId> {
{
let t = self.table_mut(table)?;
for c in &fk.columns {
let col = t.columns.get_mut(c).ok_or_else(|| Error::UnknownReference {
kind: "column",
id: format!("{}", c.0),
})?;
col.is_foreign = true;
}
}
let id = ConstraintId(self.mint());
let t = self.table_mut(table)?;
t.constraints
.insert(id, Constraint { id, name: None, kind: ConstraintKind::ForeignKey(fk) });
Ok(id)
}
pub fn add_unique(&mut self, table: TableId, columns: Vec<ColumnId>) -> Result<ConstraintId> {
let id = ConstraintId(self.mint());
let t = self.table_mut(table)?;
if columns.len() == 1 {
if let Some(col) = t.columns.get_mut(&columns[0]) {
col.is_unique = true;
}
}
t.constraints
.insert(id, Constraint { id, name: None, kind: ConstraintKind::Unique { columns } });
Ok(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_table_with_pk_and_fk() {
let mut m = LogicalModel::new("shop");
let customer = m.add_table("Customer");
let cid = m.add_column(customer, "id", DataType::Integer).unwrap();
m.add_column(customer, "name", DataType::Varchar(120)).unwrap();
m.set_primary_key(customer, vec![cid]).unwrap();
let order = m.add_table("Order");
let oid = m.add_column(order, "id", DataType::Integer).unwrap();
let ocust = m.add_column(order, "customer_id", DataType::Integer).unwrap();
m.set_primary_key(order, vec![oid]).unwrap();
m.add_foreign_key(
order,
ForeignKey {
columns: vec![ocust],
references_table: customer,
references_columns: vec![cid],
on_update: ReferentialAction::NoAction,
on_delete: ReferentialAction::NoAction,
},
)
.unwrap();
let order_t = m.table(order).unwrap();
assert_eq!(order_t.primary_key_columns(), &[oid]);
assert!(order_t.column(ocust).unwrap().is_foreign);
assert!(!order_t.column(ocust).unwrap().nullable);
}
}