use super::Blueprint;
use super::column::{Column, ColumnDefault, validate_text_literal};
use super::column_types::mysql_blob_like;
use super::constraints::{ForeignKeyAction, Index, IndexKind, validate_constraint_name};
use super::enums::{quoted_label, validate_native_enum};
use super::schema_builder::quoted_table;
use super::validation::validate_identifier;
use crate::Error;
impl Blueprint {
pub(super) fn render(
&self,
table: Option<&str>,
driver: &str,
any_pool: bool,
) -> Result<String, Error> {
if let Some(column) = self.invalid_enum_columns.first() {
return Err(Error::Validation(format!(
"enum_col `{column}` variants must not contain backslashes or control characters"
)));
}
let mut defs = vec![];
for col in &self.columns {
defs.push(self.column_sql(col, driver, any_pool)?);
}
defs.extend(self.table_constraints_sql(table, driver, any_pool)?);
Ok(defs.join(",\n "))
}
pub(super) fn create_statements(
&self,
table: &str,
driver: &str,
any_pool: bool,
) -> Result<Vec<String>, Error> {
self.reject_unreadable_temporal_columns(driver, any_pool)?;
let body = self.render(Some(table), driver, any_pool)?;
let mut statements = vec![format!(
"CREATE TABLE IF NOT EXISTS {} (\n {}\n);",
quoted_table(driver, table),
body
)];
if driver != "mysql" {
for index in self
.indexes
.iter()
.filter(|index| index.kind != IndexKind::UniqueConstraint)
{
statements.push(index_statement(index, table, driver)?);
}
}
Ok(statements)
}
pub(super) fn reject_unreadable_temporal_columns(
&self,
driver: &str,
any_pool: bool,
) -> Result<(), Error> {
if !any_pool || !matches!(driver, "postgres" | "mysql") {
return Ok(());
}
if let Some(column) = self.columns.iter().find(|column| {
self.driver_type(column)
.is_some_and(|kind| kind.is_native_temporal())
}) {
return Err(Error::Validation(format!(
"date/time column `{}` needs the `strict-postgres` or `strict-mysql` feature: SQLx Any cannot read PostgreSQL or MySQL/MariaDB date/time values",
column.name
)));
}
Ok(())
}
fn column_type_sql(&self, col: &Column, driver: &str, any_pool: bool) -> Result<String, Error> {
if let Some((_, definition)) = self
.native_enum_columns
.iter()
.find(|(column, _)| column == &col.name)
{
validate_native_enum(definition.type_name, definition.variants)?;
let labels = definition
.variants
.iter()
.map(|variant| quoted_label(variant))
.collect::<Vec<_>>()
.join(", ");
return Ok(match driver {
"postgres" => format!("\"{}\"", definition.type_name),
"mysql" => format!("ENUM({labels})"),
_ => format!("TEXT CHECK({} IN ({labels}))", col.name),
});
}
let col_type = match self.driver_type(col) {
Some(kind) => kind.for_driver(driver, any_pool).to_string(),
None => col.col_type.clone(),
};
if driver == "postgres" && col.is_auto_increment {
if col.col_type == "INTEGER" || col.col_type == "INT" {
return Ok("SERIAL".to_string());
} else if col.col_type == "BIGINT" {
return Ok("BIGSERIAL".to_string());
}
}
Ok(col_type)
}
pub(super) fn column_sql(
&self,
col: &Column,
driver: &str,
any_pool: bool,
) -> Result<String, Error> {
self.column_definition(col, driver, any_pool, col.is_unique)
}
pub(super) fn column_definition(
&self,
col: &Column,
driver: &str,
any_pool: bool,
unique: bool,
) -> Result<String, Error> {
validate_identifier(&col.name)?;
let col_type_str = self.column_type_sql(col, driver, any_pool)?;
let mut def = format!("{} {}", col.name, col_type_str);
if col.is_primary_key {
def.push_str(" PRIMARY KEY");
}
if col.is_auto_increment {
if driver == "sqlite" {
def.push_str(" AUTOINCREMENT");
} else if driver == "mysql" {
def.push_str(" AUTO_INCREMENT");
}
}
if !col.is_nullable && !col.is_primary_key {
def.push_str(" NOT NULL");
}
if let Some(default) = &col.default_value {
use std::fmt::Write;
if let ColumnDefault::Text(text) = default {
validate_text_literal(text, &format!("DEFAULT of column `{}`", col.name))?;
}
let default_sql = match self.driver_type(col) {
Some(kind) => kind.default_sql(driver, any_pool, default, &col.name)?,
None => default.to_sql(),
};
if driver == "mysql"
&& *default != ColumnDefault::Null
&& mysql_blob_like(&col_type_str)
{
let _ = write!(def, " DEFAULT ({default_sql})");
} else {
let _ = write!(def, " DEFAULT {default_sql}");
}
}
if unique {
def.push_str(" UNIQUE");
}
Ok(def)
}
fn table_constraints_sql(
&self,
table: Option<&str>,
driver: &str,
any_pool: bool,
) -> Result<Vec<String>, Error> {
if driver == "mysql" {
self.reject_mysql_blob_keys(any_pool)?;
}
let mut defs = vec![];
for index in self
.indexes
.iter()
.filter(|index| index.kind == IndexKind::UniqueConstraint)
{
let columns = index.columns_sql()?;
defs.push(
match constraint_name(index.name.as_deref(), table, |table| {
index.default_name(table)
})? {
Some(name) => format!("CONSTRAINT {name} UNIQUE ({columns})"),
None => format!("UNIQUE ({columns})"),
},
);
}
for foreign in &self.foreign_keys {
self.reject_set_null_on_required_column(&foreign.column, foreign.on_delete)?;
self.reject_set_null_on_required_column(&foreign.column, foreign.on_update)?;
let body = foreign.body_sql(driver)?;
defs.push(
match constraint_name(foreign.name.as_deref(), table, |table| {
foreign.default_name(table)
})? {
Some(name) => format!("CONSTRAINT {name} {body}"),
None => body,
},
);
}
for check in &self.checks {
defs.push(check.sql()?);
}
if driver == "mysql" {
for index in self
.indexes
.iter()
.filter(|index| index.kind != IndexKind::UniqueConstraint)
{
let unique = if index.is_unique() { "UNIQUE " } else { "" };
let columns = index.columns_sql()?;
defs.push(
match constraint_name(index.name.as_deref(), table, |table| {
index.default_name(table)
})? {
Some(name) => format!("{unique}INDEX {name} ({columns})"),
None => format!("{unique}INDEX ({columns})"),
},
);
}
}
Ok(defs)
}
fn reject_mysql_blob_keys(&self, any_pool: bool) -> Result<(), Error> {
let keyed = self
.columns
.iter()
.filter(|column| column.is_unique)
.map(|column| column.name.as_str())
.chain(
self.indexes
.iter()
.flat_map(|index| index.columns.iter().map(String::as_str)),
)
.chain(
self.foreign_keys
.iter()
.map(|foreign| foreign.column.as_str()),
);
for name in keyed {
let Some(column) = self.columns.iter().rev().find(|column| column.name == name) else {
continue;
};
if mysql_blob_like(&self.column_type_sql(column, "mysql", any_pool)?) {
return Err(Error::Validation(format!(
"MySQL/MariaDB cannot index the TEXT column `{name}`; give it a bounded type such as `.col_type = \"VARCHAR(255)\".to_string()`"
)));
}
}
Ok(())
}
fn reject_set_null_on_required_column(
&self,
column: &str,
action: Option<ForeignKeyAction>,
) -> Result<(), Error> {
let required = self
.columns
.iter()
.rev()
.find(|candidate| candidate.name == column)
.is_some_and(|candidate| !candidate.is_nullable);
if required && action == Some(ForeignKeyAction::SetNull) {
return Err(Error::Validation(format!(
"foreign key `{column}` uses SET NULL but the column is NOT NULL"
)));
}
Ok(())
}
}
fn constraint_name(
explicit: Option<&str>,
table: Option<&str>,
default: impl FnOnce(&str) -> String,
) -> Result<Option<String>, Error> {
let name = match (explicit, table) {
(Some(name), _) => Some(name.to_string()),
(None, Some(table)) => Some(default(table)),
(None, None) => None,
};
if let Some(name) = &name {
validate_constraint_name(name)?;
}
Ok(name)
}
pub(super) fn index_statement(index: &Index, table: &str, driver: &str) -> Result<String, Error> {
let name = constraint_name(index.name.as_deref(), Some(table), |table| {
index.default_name(table)
})?
.unwrap_or_default();
let unique = if index.is_unique() { "UNIQUE " } else { "" };
let if_not_exists = if driver == "mysql" {
""
} else {
"IF NOT EXISTS "
};
Ok(format!(
"CREATE {unique}INDEX {if_not_exists}{name} ON {} ({});",
quoted_table(driver, table),
index.columns_sql()?
))
}