use super::validation::validate_table_name;
use crate::Error;
const MAX_CONSTRAINT_NAME_BYTES: usize = 63;
pub const MAX_CHECK_EXPRESSION_BYTES: usize = 512;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ForeignKeyAction {
Cascade,
Restrict,
SetNull,
NoAction,
}
impl ForeignKeyAction {
pub(super) fn sql(self) -> &'static str {
match self {
Self::Cascade => "CASCADE",
Self::Restrict => "RESTRICT",
Self::SetNull => "SET NULL",
Self::NoAction => "NO ACTION",
}
}
}
#[derive(Clone, Debug)]
pub struct ForeignKey {
pub(super) column: String,
pub(super) referenced: Option<(String, String)>,
pub(super) on_delete: Option<ForeignKeyAction>,
pub(super) on_update: Option<ForeignKeyAction>,
pub(super) name: Option<String>,
}
impl ForeignKey {
pub(super) fn new(column: String) -> Self {
Self {
column,
referenced: None,
on_delete: None,
on_update: None,
name: None,
}
}
pub fn references(&mut self, table: impl Into<String>, column: impl Into<String>) -> &mut Self {
self.referenced = Some((table.into(), column.into()));
self
}
pub fn on_delete(&mut self, action: ForeignKeyAction) -> &mut Self {
self.on_delete = Some(action);
self
}
pub fn on_update(&mut self, action: ForeignKeyAction) -> &mut Self {
self.on_update = Some(action);
self
}
pub fn name(&mut self, name: impl Into<String>) -> &mut Self {
self.name = Some(name.into());
self
}
pub(super) fn default_name(&self, table: &str) -> String {
format!("{table}_{}_foreign", self.column)
}
pub(super) fn body_sql(&self, driver: &str) -> Result<String, Error> {
validate_table_name(&self.column)?;
let (table, column) = self.referenced.as_ref().ok_or_else(|| {
Error::Validation(format!(
"foreign key on `{}` needs references(table, column)",
self.column
))
})?;
validate_table_name(table)?;
validate_table_name(column)?;
let mut sql = format!(
"FOREIGN KEY ({}) {}",
self.column,
self.references_sql(driver, table, column)
);
sql.push_str(&self.actions_sql());
Ok(sql)
}
pub(super) fn column_reference_sql(&self, driver: &str) -> Result<String, Error> {
let (table, column) = self.referenced.as_ref().ok_or_else(|| {
Error::Validation(format!(
"foreign key on `{}` needs references(table, column)",
self.column
))
})?;
validate_table_name(table)?;
validate_table_name(column)?;
Ok(format!(
"{}{}",
self.references_sql(driver, table, column),
self.actions_sql()
))
}
fn references_sql(&self, driver: &str, table: &str, column: &str) -> String {
format!(
"REFERENCES {} ({column})",
super::schema_builder::quoted_table(driver, table)
)
}
fn actions_sql(&self) -> String {
let mut sql = String::new();
if let Some(action) = self.on_delete {
sql.push_str(" ON DELETE ");
sql.push_str(action.sql());
}
if let Some(action) = self.on_update {
sql.push_str(" ON UPDATE ");
sql.push_str(action.sql());
}
sql
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum IndexKind {
Plain,
Unique,
UniqueConstraint,
}
#[derive(Clone, Debug)]
pub struct Index {
pub(super) columns: Vec<String>,
pub(super) kind: IndexKind,
pub(super) name: Option<String>,
}
impl Index {
pub(super) fn new(columns: &[&str], kind: IndexKind) -> Self {
Self {
columns: columns.iter().map(|column| column.to_string()).collect(),
kind,
name: None,
}
}
pub fn name(&mut self, name: impl Into<String>) -> &mut Self {
self.name = Some(name.into());
self
}
pub(super) fn is_unique(&self) -> bool {
self.kind != IndexKind::Plain
}
pub(super) fn default_name(&self, table: &str) -> String {
let suffix = if self.is_unique() { "unique" } else { "index" };
format!("{table}_{}_{suffix}", self.columns.join("_"))
}
pub(super) fn columns_sql(&self) -> Result<String, Error> {
if self.columns.is_empty() {
return Err(Error::Validation(
"an index or unique constraint needs at least one column".to_string(),
));
}
for column in &self.columns {
validate_table_name(column)?;
}
Ok(self.columns.join(", "))
}
}
#[derive(Clone, Debug)]
pub struct Check {
pub(super) expression: String,
pub(super) name: Option<String>,
}
impl Check {
pub(super) fn new(expression: String) -> Self {
Self {
expression,
name: None,
}
}
pub fn name(&mut self, name: impl Into<String>) -> &mut Self {
self.name = Some(name.into());
self
}
pub(super) fn sql(&self) -> Result<String, Error> {
validate_check_expression(&self.expression)?;
let check = format!("CHECK ({})", self.expression);
match &self.name {
Some(name) => {
validate_constraint_name(name)?;
Ok(format!("CONSTRAINT {name} {check}"))
}
None => Ok(check),
}
}
}
pub(super) fn validate_constraint_name(name: &str) -> Result<(), Error> {
validate_table_name(name)?;
if name.len() > MAX_CONSTRAINT_NAME_BYTES {
return Err(Error::Validation(format!(
"constraint or index name `{name}` is longer than {MAX_CONSTRAINT_NAME_BYTES} bytes; set a shorter one with .name(...)"
)));
}
Ok(())
}
pub(super) fn validate_check_expression(expression: &str) -> Result<(), Error> {
let invalid = |reason: &str| {
Err(Error::Validation(format!(
"CHECK expression {reason}; it is trusted developer SQL, not user input"
)))
};
if expression.trim().is_empty() {
return invalid("must not be empty");
}
if expression.len() > MAX_CHECK_EXPRESSION_BYTES {
return invalid("must be at most 512 bytes");
}
if expression.contains("--") || expression.contains("/*") || expression.contains("*/") {
return invalid("must not contain SQL comments");
}
if expression
.chars()
.any(|character| matches!(character, ';' | '\\' | '$' | '`') || character.is_control())
{
return invalid("must not contain `;`, `\\`, `$`, backticks or control characters");
}
let mut depth = 0_usize;
let mut quote = None;
for character in expression.chars() {
match (quote, character) {
(Some(open), _) if character == open => quote = None,
(Some(_), _) => {}
(None, '\'' | '"') => quote = Some(character),
(None, '(') => depth += 1,
(None, ')') => {
depth = match depth.checked_sub(1) {
Some(depth) => depth,
None => return invalid("has an unbalanced `)`"),
};
}
(None, _) => {}
}
}
if quote.is_some() {
return invalid("has an unterminated quote");
}
if depth != 0 {
return invalid("has an unbalanced `(`");
}
Ok(())
}
#[cfg(test)]
#[path = "constraints_tests.rs"]
mod tests;