use std::collections::HashSet;
use sqlparser::ast::{CreateIndex, IndexColumn, TableConstraint};
use super::alter::column_index;
use super::checks::{apply_range_check, range_check};
use super::enums::{restrict_to_values, value_list_check};
use super::names::{
default_constraint_name, find_table, ident_name, index_column_name, object_leaf_name,
object_schema_name,
};
use super::table::rust_type;
use crate::schema::{ForeignKey, KeyConstraint, Table, UniqueKey};
pub(super) fn add_constraint(
table: &mut Table,
constraint: &TableConstraint,
taken: &HashSet<String>,
) -> std::result::Result<(), String> {
let clause = |reason: String| format!("ADD CONSTRAINT: {reason}");
let known_columns = |table: &Table, keys: &[String]| match keys
.iter()
.find(|key| column_index(table, key).is_none())
{
Some(key) => Err(clause(format!("column `{key}` does not exist"))),
None => Ok(()),
};
match constraint {
TableConstraint::PrimaryKey { name, columns, .. } => {
let keys = key_columns(columns, "PRIMARY KEY").map_err(clause)?;
if !table.primary_key.is_empty() {
return Err(clause("the table already has a primary key".to_owned()));
}
known_columns(table, &keys)?;
for column in &mut table.columns {
if keys.contains(&column.sql_name) {
column.is_primary_key = true;
column.nullable = false;
column.rust_type = rust_type(&column.sqlx_type, false);
}
}
add_key_constraint(
table,
name.as_ref().map(ident_name),
keys.clone(),
true,
taken,
);
table.primary_key = keys;
}
TableConstraint::Unique { name, columns, .. } => {
let keys = key_columns(columns, "UNIQUE").map_err(clause)?;
known_columns(table, &keys)?;
add_key_constraint(
table,
name.as_ref().map(ident_name),
keys.clone(),
false,
taken,
);
table.unique_keys.push(UniqueKey::new(keys));
}
TableConstraint::ForeignKey { columns, .. } => {
let keys: Vec<String> = columns.iter().map(ident_name).collect();
known_columns(table, &keys)?;
table
.foreign_keys
.extend(constraint_foreign_key(constraint));
}
TableConstraint::Check { expr, .. } => {
if let Some((name, labels)) = value_list_check(expr)
&& let Some(index) = column_index(table, &name)
{
restrict_to_values(&mut table.columns[index], labels);
}
if let Some(check) = range_check(expr)
&& let Some(index) = column_index(table, &check.column)
{
apply_range_check(&mut table.columns[index], check);
}
}
other => return Err(format!("unsupported ALTER TABLE clause `ADD {other}`")),
}
Ok(())
}
pub(super) fn record_unique_index(tables: &mut [Table], index: &CreateIndex) {
if !index.unique {
return;
}
let Some(position) = find_table(tables, &index.table_name) else {
return;
};
let Some(columns) = index
.columns
.iter()
.map(index_column_name)
.collect::<Option<Vec<String>>>()
else {
return;
};
let table = &mut tables[position];
if columns
.iter()
.any(|column| column_index(table, column).is_none())
{
return;
}
table.unique_keys.push(UniqueKey {
columns,
predicate: index.predicate.as_ref().map(ToString::to_string),
});
}
pub(super) fn add_key_constraint(
table: &mut Table,
name: Option<String>,
columns: Vec<String>,
primary: bool,
elsewhere: &HashSet<String>,
) {
let name = name.unwrap_or_else(|| {
let taken = |candidate: &str| {
candidate == table.sql_name
|| elsewhere.contains(candidate)
|| table
.key_constraints
.iter()
.any(|constraint| constraint.name == candidate)
};
default_constraint_name(&table.sql_name, &columns, primary, taken)
});
table.key_constraints.push(KeyConstraint { name, columns });
}
pub(super) fn key_columns(
keys: &[IndexColumn],
what: &str,
) -> std::result::Result<Vec<String>, String> {
let mut names = Vec::with_capacity(keys.len());
for key in keys {
let name = index_column_name(key)
.ok_or_else(|| format!("{what} constraint contains a non-column expression"))?;
if names.contains(&name) {
return Err(format!("{what} lists column `{name}` more than once"));
}
names.push(name);
}
Ok(names)
}
pub(super) fn constraint_foreign_key(constraint: &TableConstraint) -> Option<ForeignKey> {
match constraint {
TableConstraint::ForeignKey {
columns,
foreign_table,
referred_columns,
..
} if columns.len() == 1 && referred_columns.len() == 1 => Some(ForeignKey {
column: ident_name(&columns[0]),
referenced_schema: object_schema_name(foreign_table),
referenced_table: object_leaf_name(foreign_table),
referenced_column: ident_name(&referred_columns[0]),
}),
_ => None,
}
}