use crate::table::row::{Row, Value};
use crate::table::schema::Schema;
#[derive(Debug, Clone)]
pub enum Constraint {
NotNull { column: String },
Unique { columns: Vec<String> },
Check { expr: crate::parser::ast::Expr }, ForeignKey {
columns: Vec<String>,
ref_table: String,
ref_columns: Vec<String>,
},
}
#[derive(Debug, Clone, Default)]
pub struct TableConstraints {
pub constraints: Vec<Constraint>,
}
impl TableConstraints {
pub fn new() -> Self { Self::default() }
pub fn add(&mut self, c: Constraint) { self.constraints.push(c); }
pub fn from_schema(schema: &Schema) -> Self {
let mut tc = TableConstraints::new();
for col in &schema.columns {
if !col.nullable {
tc.add(Constraint::NotNull { column: col.name.clone() });
}
}
tc
}
}
pub fn check_row(
row: &Row,
schema: &Schema,
constraints: &TableConstraints,
existing_rows: &[Row],
) -> Result<(), String> {
for constraint in &constraints.constraints {
match constraint {
Constraint::NotNull { column } => {
let idx = schema.index_of(column)
.ok_or_else(|| format!("column '{}' not found", column))?;
if matches!(row.values.get(idx), Some(Value::Null) | None) {
return Err(format!("NOT NULL constraint failed: {}", column));
}
}
Constraint::Unique { columns } => {
let idxs: Vec<usize> = columns.iter()
.map(|c| schema.index_of(c)
.ok_or_else(|| format!("column '{}' not found", c)))
.collect::<Result<_, _>>()?;
let new_vals: Vec<&Value> = idxs.iter()
.map(|&i| row.values.get(i).unwrap_or(&Value::Null))
.collect();
if new_vals.iter().any(|v| matches!(v, Value::Null)) { continue; }
for existing in existing_rows {
let ex_vals: Vec<&Value> = idxs.iter()
.map(|&i| existing.values.get(i).unwrap_or(&Value::Null))
.collect();
if ex_vals == new_vals {
return Err(format!("UNIQUE constraint failed: {}",
columns.join(", ")));
}
}
}
Constraint::Check { expr } => {
if let Ok(val) = crate::planner::executor::eval_expr(expr, row, &schema.columns.iter().map(|c| c.name.clone()).collect::<Vec<_>>()) {
if !crate::planner::executor::is_truthy(&val) {
return Err("CHECK constraint failed".to_string());
}
}
}
Constraint::ForeignKey { columns, ref_table, ref_columns } => {
let _ = (columns, ref_table, ref_columns);
}
}
}
Ok(())
}
pub fn check_fk_on_delete(
_deleted_key: &Value,
_constraints: &[TableConstraints],
) -> Result<(), String> {
Ok(())
}
pub fn constraints_from_ast(
stmt: &crate::parser::ast::CreateTableStmt,
) -> TableConstraints {
use crate::parser::ast::{ColumnConstraint, TableConstraint};
let mut tc = TableConstraints::new();
for col in &stmt.columns {
for constraint in &col.constraints {
match constraint {
ColumnConstraint::NotNull => {
tc.add(Constraint::NotNull { column: col.name.clone() });
}
ColumnConstraint::Unique => {
tc.add(Constraint::Unique { columns: vec![col.name.clone()] });
}
ColumnConstraint::PrimaryKey { .. } => {
tc.add(Constraint::NotNull { column: col.name.clone() });
}
ColumnConstraint::Default(_) => {} ColumnConstraint::Check(expr) => {
tc.add(Constraint::Check { expr: expr.clone() });
}
ColumnConstraint::References { table, column } => {
let ref_col = column.clone().unwrap_or_else(|| "id".to_string());
tc.add(Constraint::ForeignKey {
columns: vec![col.name.clone()],
ref_table: table.clone(),
ref_columns: vec![ref_col],
});
}
}
}
}
for tc_ast in &stmt.constraints {
match tc_ast {
TableConstraint::PrimaryKey(cols) => {
for col in cols {
tc.add(Constraint::NotNull { column: col.clone() });
}
tc.add(Constraint::Unique { columns: cols.clone() });
}
TableConstraint::Unique(cols) => {
tc.add(Constraint::Unique { columns: cols.clone() });
}
}
}
tc
}
#[cfg(test)]
mod tests {
use super::*;
use crate::table::schema::{Column, DataType, Schema};
fn schema() -> Schema {
Schema::new(vec![
Column::new("id", DataType::Integer),
Column::new("email", DataType::Text),
Column::new("age", DataType::Integer),
])
}
fn row(id: i64, email: &str, age: i64) -> Row {
Row::new(vec![Value::Integer(id), Value::Text(email.into()), Value::Integer(age)])
}
fn null_row() -> Row {
Row::new(vec![Value::Integer(1), Value::Null, Value::Integer(20)])
}
#[test]
fn not_null_passes() {
let schema = schema();
let mut tc = TableConstraints::new();
tc.add(Constraint::NotNull { column: "email".into() });
let r = row(1, "alice@example.com", 30);
assert!(check_row(&r, &schema, &tc, &[]).is_ok());
}
#[test]
fn not_null_fails() {
let schema = schema();
let mut tc = TableConstraints::new();
tc.add(Constraint::NotNull { column: "email".into() });
assert!(check_row(&null_row(), &schema, &tc, &[]).is_err());
}
#[test]
fn unique_passes() {
let schema = schema();
let mut tc = TableConstraints::new();
tc.add(Constraint::Unique { columns: vec!["email".into()] });
let existing = vec![row(1, "alice@example.com", 30)];
let new_row = row(2, "bob@example.com", 25);
assert!(check_row(&new_row, &schema, &tc, &existing).is_ok());
}
#[test]
fn unique_fails_on_duplicate() {
let schema = schema();
let mut tc = TableConstraints::new();
tc.add(Constraint::Unique { columns: vec!["email".into()] });
let existing = vec![row(1, "alice@example.com", 30)];
let dup_row = row(2, "alice@example.com", 25); assert!(check_row(&dup_row, &schema, &tc, &existing).is_err());
}
#[test]
fn unique_allows_multiple_nulls() {
let schema = schema();
let mut tc = TableConstraints::new();
tc.add(Constraint::Unique { columns: vec!["email".into()] });
let existing = vec![null_row()];
let another_null = null_row();
assert!(check_row(&another_null, &schema, &tc, &existing).is_ok());
}
#[test]
fn composite_unique() {
let schema = schema();
let mut tc = TableConstraints::new();
tc.add(Constraint::Unique { columns: vec!["id".into(), "email".into()] });
let existing = vec![row(1, "alice@example.com", 30)];
let r2 = row(1, "bob@example.com", 25);
assert!(check_row(&r2, &schema, &tc, &existing).is_ok());
let dup = row(1, "alice@example.com", 99);
assert!(check_row(&dup, &schema, &tc, &existing).is_err());
}
#[test]
fn from_ast_not_null() {
use crate::parser::parse;
let stmts = parse("CREATE TABLE t (id INTEGER NOT NULL, name TEXT)").unwrap();
if let crate::parser::ast::Statement::CreateTable(stmt) = &stmts[0] {
let tc = constraints_from_ast(stmt);
assert!(tc.constraints.iter().any(|c| matches!(c, Constraint::NotNull { column } if column == "id")));
}
}
#[test]
fn from_ast_unique() {
use crate::parser::parse;
let stmts = parse("CREATE TABLE t (id INTEGER, email TEXT UNIQUE)").unwrap();
if let crate::parser::ast::Statement::CreateTable(stmt) = &stmts[0] {
let tc = constraints_from_ast(stmt);
assert!(tc.constraints.iter().any(|c| matches!(c, Constraint::Unique { .. })));
}
}
}