use crate::alloc_prelude::*;
use core::fmt::Write;
use super::{
CheckConstraint, Column, ForeignKey, Generated, GeneratedType, Index, IndexColumnDef,
PrimaryKey, Table, UniqueConstraint, View,
};
fn quote_ident(ident: &str) -> String {
format!("`{}`", ident.replace('`', "``"))
}
#[derive(Clone, Debug)]
pub struct TableSql<'a> {
pub table: &'a Table,
pub columns: &'a [Column],
pub primary_key: Option<&'a PrimaryKey>,
pub foreign_keys: &'a [ForeignKey],
pub unique_constraints: &'a [UniqueConstraint],
pub check_constraints: &'a [CheckConstraint],
}
impl<'a> TableSql<'a> {
#[must_use]
pub const fn new(table: &'a Table) -> Self {
Self {
table,
columns: &[],
primary_key: None,
foreign_keys: &[],
unique_constraints: &[],
check_constraints: &[],
}
}
#[must_use]
pub const fn columns(mut self, columns: &'a [Column]) -> Self {
self.columns = columns;
self
}
#[must_use]
pub const fn primary_key(mut self, pk: Option<&'a PrimaryKey>) -> Self {
self.primary_key = pk;
self
}
#[must_use]
pub const fn foreign_keys(mut self, fks: &'a [ForeignKey]) -> Self {
self.foreign_keys = fks;
self
}
#[must_use]
pub const fn unique_constraints(mut self, uniques: &'a [UniqueConstraint]) -> Self {
self.unique_constraints = uniques;
self
}
#[must_use]
pub const fn check_constraints(mut self, checks: &'a [CheckConstraint]) -> Self {
self.check_constraints = checks;
self
}
#[must_use]
pub fn create_table_sql(&self) -> String {
let mut sql = format!("CREATE TABLE {} (\n", quote_ident(self.table.name()));
let mut lines = Vec::new();
for column in self.columns {
let is_inline_pk = self.primary_key.as_ref().is_some_and(|pk| {
pk.columns.len() == 1
&& pk.columns.iter().any(|c| *c == column.name())
&& !pk.name_explicit
});
let is_inline_unique = self.unique_constraints.iter().any(|u| {
u.columns.len() == 1
&& u.columns.iter().any(|c| *c == column.name())
&& !u.name_explicit
});
lines.push(format!(
"\t{}",
column.to_column_sql(is_inline_pk, is_inline_unique)
));
}
if let Some(pk) = &self.primary_key
&& (pk.columns.len() > 1 || pk.name_explicit)
{
let cols = pk
.columns
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(", ");
lines.push(format!(
"\tCONSTRAINT {} PRIMARY KEY({})",
quote_ident(pk.name()),
cols
));
}
for fk in self.foreign_keys {
lines.push(format!("\t{}", fk.to_constraint_sql()));
}
for unique in self
.unique_constraints
.iter()
.filter(|u| u.columns.len() > 1 || u.name_explicit)
{
let cols = unique
.columns
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(", ");
lines.push(format!(
"\tCONSTRAINT {} UNIQUE({})",
quote_ident(unique.name()),
cols
));
}
for check in self.check_constraints {
lines.push(format!(
"\tCONSTRAINT {} CHECK({})",
quote_ident(check.name()),
check.value
));
}
sql.push_str(&lines.join(",\n"));
sql.push_str("\n)");
let mut options = Vec::new();
if self.table.without_rowid {
options.push("WITHOUT ROWID");
}
if self.table.strict {
options.push("STRICT");
}
if !options.is_empty() {
let _ = write!(sql, " {}", options.join(", "));
}
sql.push(';');
sql
}
#[must_use]
pub fn drop_table_sql(&self) -> String {
format!("DROP TABLE {};", quote_ident(self.table.name()))
}
}
impl Column {
#[must_use]
pub fn to_column_sql(&self, inline_pk: bool, inline_unique: bool) -> String {
let mut sql = format!(
"{} {}",
quote_ident(self.name()),
self.sql_type().to_uppercase()
);
if inline_pk {
sql.push_str(" PRIMARY KEY");
if self.autoincrement.unwrap_or(false) {
sql.push_str(" AUTOINCREMENT");
}
} else if self.autoincrement.unwrap_or(false) {
sql.push_str(" AUTOINCREMENT");
}
if let Some(default) = self.default.as_ref() {
let _ = write!(sql, " DEFAULT {default}");
}
if let Some(generated) = &self.generated {
sql.push_str(&generated.to_sql());
}
if self.not_null && !(inline_pk && self.sql_type().to_lowercase().starts_with("int")) {
sql.push_str(" NOT NULL");
}
if inline_unique && !inline_pk {
sql.push_str(" UNIQUE");
}
if let Some(collate) = self.collate.as_ref() {
let _ = write!(sql, " COLLATE {collate}");
}
sql
}
#[must_use]
pub fn add_column_sql(&self) -> String {
format!(
"ALTER TABLE {} ADD COLUMN {};",
quote_ident(self.table()),
self.to_column_sql(false, false)
)
}
#[must_use]
pub fn drop_column_sql(&self) -> String {
format!(
"ALTER TABLE {} DROP COLUMN {};",
quote_ident(self.table()),
quote_ident(self.name())
)
}
}
impl Generated {
#[must_use]
pub fn to_sql(&self) -> String {
let gen_type = match self.gen_type {
GeneratedType::Stored => "STORED",
GeneratedType::Virtual => "VIRTUAL",
};
format!(" GENERATED ALWAYS AS {} {}", self.expression, gen_type)
}
}
impl ForeignKey {
#[must_use]
pub fn to_constraint_sql(&self) -> String {
let from_cols = self
.columns
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(", ");
let to_cols = self
.columns_to
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(", ");
let mut sql = format!(
"CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {}({})",
quote_ident(self.name()),
from_cols,
quote_ident(&self.table_to),
to_cols
);
if let Some(on_update) = self.on_update.as_ref()
&& on_update != "NO ACTION"
{
let _ = write!(sql, " ON UPDATE {on_update}");
}
if let Some(on_delete) = self.on_delete.as_ref()
&& on_delete != "NO ACTION"
{
let _ = write!(sql, " ON DELETE {on_delete}");
}
sql
}
#[must_use]
pub fn add_fk_sql(&self) -> String {
format!(
"-- SQLite requires table recreation to add foreign keys\n-- FK: {} on {}",
self.name(),
quote_ident(self.table())
)
}
#[must_use]
pub fn drop_fk_sql(&self) -> String {
format!(
"-- SQLite requires table recreation to drop foreign keys\n-- FK: {} on {}",
self.name(),
quote_ident(self.table())
)
}
}
impl Index {
#[must_use]
pub fn create_index_sql(&self) -> String {
let unique = if self.is_unique { "UNIQUE " } else { "" };
let columns = self
.columns
.iter()
.map(super::index::IndexColumn::to_sql)
.collect::<Vec<_>>()
.join(", ");
let mut sql = format!(
"CREATE {}INDEX {} ON {}({});",
unique,
quote_ident(self.name()),
quote_ident(self.table()),
columns
);
if let Some(where_clause) = self.where_clause.as_ref() {
sql.pop();
let _ = write!(sql, " WHERE {where_clause};");
}
sql
}
#[must_use]
pub fn drop_index_sql(&self) -> String {
format!("DROP INDEX {};", quote_ident(self.name()))
}
}
impl IndexColumnDef {
#[must_use]
pub fn to_sql(&self) -> String {
if self.is_expression {
self.value.to_string()
} else {
quote_ident(self.value)
}
}
}
impl View {
#[must_use]
pub fn create_view_sql(&self) -> String {
self.definition.as_ref().map_or_else(
|| format!("-- View {} has no definition", quote_ident(self.name())),
|def| format!("CREATE VIEW {} AS {};", quote_ident(self.name()), def),
)
}
#[must_use]
pub fn drop_view_sql(&self) -> String {
format!("DROP VIEW {};", quote_ident(self.name()))
}
}
impl Table {
#[must_use]
pub fn drop_table_sql(&self) -> String {
format!("DROP TABLE {};", quote_ident(self.name()))
}
#[must_use]
pub fn rename_table_sql(&self, new_name: &str) -> String {
format!(
"ALTER TABLE {} RENAME TO {};",
quote_ident(self.name()),
quote_ident(new_name)
)
}
}
impl PrimaryKey {
#[must_use]
pub fn to_constraint_sql(&self) -> String {
let cols = self
.columns
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(", ");
format!(
"CONSTRAINT {} PRIMARY KEY({})",
quote_ident(self.name()),
cols
)
}
}
impl UniqueConstraint {
#[must_use]
pub fn to_constraint_sql(&self) -> String {
let cols = self
.columns
.iter()
.map(|c| quote_ident(c))
.collect::<Vec<_>>()
.join(", ");
format!("CONSTRAINT {} UNIQUE({})", quote_ident(self.name()), cols)
}
}
impl CheckConstraint {
#[must_use]
pub fn to_constraint_sql(&self) -> String {
format!(
"CONSTRAINT {} CHECK({})",
quote_ident(self.name()),
self.value
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sqlite::ddl::{
ColumnDef, ForeignKeyDef, IndexColumnDef, IndexDef, PrimaryKeyDef, ReferentialAction,
TableDef,
};
use std::borrow::Cow;
#[test]
fn test_simple_create_table() {
let table = TableDef::new("users").into_table();
let columns = [
ColumnDef::new("users", "id", "INTEGER")
.primary_key()
.autoincrement()
.into_column(),
ColumnDef::new("users", "name", "TEXT")
.not_null()
.into_column(),
ColumnDef::new("users", "email", "TEXT").into_column(),
];
const PK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
let pk = PrimaryKeyDef::new("users", "users_pk")
.columns(PK_COLS)
.into_primary_key();
let sql = TableSql::new(&table)
.columns(&columns)
.primary_key(Some(&pk))
.create_table_sql();
assert!(sql.contains("CREATE TABLE `users`"));
assert!(sql.contains("`id` INTEGER PRIMARY KEY AUTOINCREMENT"));
assert!(sql.contains("`name` TEXT NOT NULL"));
assert!(sql.contains("`email` TEXT"));
}
#[test]
fn test_table_with_foreign_key() {
let table = TableDef::new("posts").into_table();
let columns = [
ColumnDef::new("posts", "id", "INTEGER")
.primary_key()
.into_column(),
ColumnDef::new("posts", "user_id", "INTEGER")
.not_null()
.into_column(),
];
const PK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
let pk = PrimaryKeyDef::new("posts", "posts_pk")
.columns(PK_COLS)
.into_primary_key();
const FK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("user_id")];
const FK_REFS: &[Cow<'static, str>] = &[Cow::Borrowed("id")];
let fks = [ForeignKeyDef::new("posts", "posts_user_id_fk")
.columns(FK_COLS)
.references("users", FK_REFS)
.on_delete(ReferentialAction::Cascade)
.into_foreign_key()];
let sql = TableSql::new(&table)
.columns(&columns)
.primary_key(Some(&pk))
.foreign_keys(&fks)
.create_table_sql();
assert!(sql.contains("FOREIGN KEY (`user_id`) REFERENCES `users`(`id`)"));
assert!(sql.contains("ON DELETE CASCADE"));
}
#[test]
fn test_create_index() {
const COLS: &[IndexColumnDef] = &[IndexColumnDef::new("email")];
let index = IndexDef::new("users", "users_email_idx")
.columns(COLS)
.unique()
.into_index();
let sql = index.create_index_sql();
assert_eq!(
sql,
"CREATE UNIQUE INDEX `users_email_idx` ON `users`(`email`);"
);
}
#[test]
fn test_strict_without_rowid() {
let table = TableDef::new("data").strict().without_rowid().into_table();
let columns = [ColumnDef::new("data", "key", "TEXT")
.primary_key()
.not_null()
.into_column()];
const PK_COLS: &[Cow<'static, str>] = &[Cow::Borrowed("key")];
let pk = PrimaryKeyDef::new("data", "data_pk")
.columns(PK_COLS)
.into_primary_key();
let sql = TableSql::new(&table)
.columns(&columns)
.primary_key(Some(&pk))
.create_table_sql();
assert!(sql.ends_with("WITHOUT ROWID, STRICT;"));
}
}