use crate::schema::{ColTy, Column, TABLES, Table};
use core::fmt::Write as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Placeholder {
Question,
Dollar,
AtP,
Colon,
}
impl Placeholder {
#[must_use]
pub fn render(self, position: usize) -> String {
match self {
Self::Question => "?".to_owned(),
Self::Dollar => format!("${position}"),
Self::AtP => format!("@p{position}"),
Self::Colon => format!(":{position}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectKind {
Table,
Index,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Idempotence {
IfNotExists,
Guard,
Inline,
}
pub trait Dialect {
fn name(&self) -> &'static str;
fn col_sql(&self, ty: ColTy) -> String;
fn quote(&self, identifier: &str) -> String;
fn placeholder(&self) -> Placeholder;
fn table_idempotence(&self) -> Idempotence {
Idempotence::IfNotExists
}
fn index_idempotence(&self) -> Idempotence {
Idempotence::IfNotExists
}
fn guard(&self, _kind: ObjectKind, _name: &str, statement: &str) -> String {
statement.to_owned()
}
fn terminator(&self) -> &'static str {
";"
}
fn append_only_sql(&self, _table: &Table) -> Vec<String> {
Vec::new()
}
fn ddl(&self) -> Vec<String> {
let mut out = Vec::new();
for table in TABLES {
out.push(self.create_table(table));
}
if self.index_idempotence() != Idempotence::Inline {
for table in TABLES {
for index in table.indexes {
out.push(self.create_index(table, index));
}
}
}
for table in TABLES {
if table.append_only {
out.extend(self.append_only_sql(table));
}
}
out
}
fn create_table(&self, table: &Table) -> String {
let mut sql = String::new();
let exists = if self.table_idempotence() == Idempotence::IfNotExists {
"IF NOT EXISTS "
} else {
""
};
let _ = writeln!(sql, "CREATE TABLE {exists}{} (", self.quote(table.name));
let mut parts: Vec<String> = Vec::new();
for column in table.columns {
parts.push(format!(
" {} {}{}",
self.quote(column.name),
self.col_sql(column.ty),
if column.nullable { "" } else { " NOT NULL" }
));
}
if !table.primary_key.is_empty() {
let keys: Vec<String> = table.primary_key.iter().map(|k| self.quote(k)).collect();
parts.push(format!(" PRIMARY KEY ({})", keys.join(", ")));
}
for fk in table.foreign_keys {
parts.push(format!(
" FOREIGN KEY ({}) REFERENCES {} ({})",
self.quote(fk.column),
self.quote(fk.table),
self.quote(fk.references)
));
}
if self.index_idempotence() == Idempotence::Inline {
for index in table.indexes {
let columns: Vec<String> = index.columns.iter().map(|c| self.quote(c)).collect();
parts.push(format!(
" {}KEY {} ({})",
if index.unique { "UNIQUE " } else { "" },
self.quote(index.name),
columns.join(", ")
));
}
}
let _ = write!(sql, "{}", parts.join(",\n"));
let _ = write!(sql, "\n)");
if self.table_idempotence() == Idempotence::Guard {
return self.guard(ObjectKind::Table, table.name, &sql);
}
sql
}
fn create_index(&self, table: &Table, index: &crate::schema::Index) -> String {
let unique = if index.unique { "UNIQUE " } else { "" };
let exists = if self.index_idempotence() == Idempotence::IfNotExists {
"IF NOT EXISTS "
} else {
""
};
let columns: Vec<String> = index.columns.iter().map(|c| self.quote(c)).collect();
let sql = format!(
"CREATE {unique}INDEX {exists}{} ON {} ({})",
self.quote(index.name),
self.quote(table.name),
columns.join(", ")
);
if self.index_idempotence() == Idempotence::Guard {
return self.guard(ObjectKind::Index, index.name, &sql);
}
sql
}
}
#[must_use]
pub fn ddl_script<D: Dialect + ?Sized>(dialect: &D) -> String {
let terminator = dialect.terminator();
dialect
.ddl()
.into_iter()
.map(|statement| format!("{statement}{terminator}\n"))
.collect::<Vec<_>>()
.join("\n")
}
#[must_use]
pub fn column_sql<D: Dialect + ?Sized>(dialect: &D, column: &Column) -> String {
dialect.col_sql(column.ty)
}