use std::borrow::Cow;
use std::fmt::Write;
use rorm_declaration::imr::DefaultValue;
#[cfg(feature = "postgres")]
use crate::create_trigger::trigger_annotation_to_trigger_postgres;
#[cfg(feature = "sqlite")]
use crate::create_trigger::trigger_annotation_to_trigger_sqlite;
#[cfg(feature = "postgres")]
use crate::db_specific::postgres;
#[cfg(feature = "sqlite")]
use crate::db_specific::sqlite;
use crate::error::Error;
use crate::{Annotation, DbType, Value};
pub trait CreateColumn<'post_build>: Sized {
fn build(self, s: &mut String) -> Result<(), Error>;
}
#[derive(Debug)]
pub struct SQLAnnotation<'post_build> {
pub(crate) annotation: &'post_build Annotation,
}
#[derive(Debug)]
#[cfg(feature = "sqlite")]
pub struct CreateColumnSQLiteData<'until_build, 'post_build> {
pub(crate) name: &'until_build str,
pub(crate) table_name: &'until_build str,
pub(crate) data_type: DbType,
pub(crate) annotations: Vec<SQLAnnotation<'post_build>>,
pub(crate) statements: Option<&'until_build mut Vec<(String, Vec<Value<'post_build>>)>>,
pub(crate) lookup: Option<&'until_build mut Vec<Value<'post_build>>>,
}
#[derive(Debug)]
#[cfg(feature = "postgres")]
pub struct CreateColumnPostgresData<'until_build, 'post_build> {
pub(crate) name: &'until_build str,
pub(crate) table_name: &'until_build str,
pub(crate) data_type: DbType,
pub(crate) annotations: Vec<SQLAnnotation<'post_build>>,
pub(crate) pre_statements: Option<&'until_build mut Vec<(String, Vec<Value<'post_build>>)>>,
pub(crate) statements: Option<&'until_build mut Vec<(String, Vec<Value<'post_build>>)>>,
}
#[derive(Debug)]
pub enum CreateColumnImpl<'until_build, 'post_build> {
#[cfg(feature = "sqlite")]
SQLite(CreateColumnSQLiteData<'until_build, 'post_build>),
#[cfg(feature = "postgres")]
Postgres(CreateColumnPostgresData<'until_build, 'post_build>),
}
impl<'post_build> CreateColumn<'post_build> for CreateColumnImpl<'_, 'post_build> {
fn build(self, sql: &mut String) -> Result<(), Error> {
match self {
#[cfg(feature = "sqlite")]
CreateColumnImpl::SQLite(mut column) => {
write!(
sql,
"\"{}\" {}",
column.name,
sqlite_type(column.data_type)?
)
.unwrap();
for x in &column.annotations {
let SQLAnnotation { annotation } = x;
if let Some(s) = &mut column.statements {
trigger_annotation_to_trigger_sqlite(
annotation,
&column.data_type,
column.table_name,
column.name,
s,
);
}
sql.push(' ');
match &annotation {
Annotation::AutoIncrement => write!(sql, "AUTOINCREMENT").unwrap(),
Annotation::AutoCreateTime => {
write!(
sql,
"DEFAULT {}",
match column.data_type {
DbType::Date => "CURRENT_DATE",
DbType::DateTime => "CURRENT_TIMESTAMP",
DbType::Timestamp => "CURRENT_TIMESTAMP",
DbType::Time => "CURRENT_TIME",
_ =>
return Err(Error::SQLBuildError(format!(
"AutoCreateTime not compatible with {:?}",
column.data_type
))),
}
)
.unwrap();
}
Annotation::DefaultValue(DefaultValue::String(x)) => {
write!(sql, "DEFAULT {}", sqlite::fmt(x)).unwrap()
}
Annotation::DefaultValue(DefaultValue::Integer(x)) => {
write!(sql, "DEFAULT {x}").unwrap()
}
Annotation::DefaultValue(DefaultValue::Float(x)) => {
write!(sql, "DEFAULT {x}").unwrap()
}
Annotation::DefaultValue(DefaultValue::Boolean(true)) => {
write!(sql, "DEFAULT 1").unwrap()
}
Annotation::DefaultValue(DefaultValue::Boolean(false)) => {
write!(sql, "DEFAULT 0").unwrap()
}
Annotation::NotNull => write!(sql, "NOT NULL").unwrap(),
Annotation::PrimaryKey => write!(sql, "PRIMARY KEY").unwrap(),
Annotation::Unique => write!(sql, "UNIQUE").unwrap(),
Annotation::ForeignKey(fk) => write!(
sql,
"REFERENCES \"{}\" (\"{}\") ON DELETE {} ON UPDATE {}",
fk.table_name, fk.column_name, fk.on_delete, fk.on_update
)
.unwrap(),
_ => {}
}
}
Ok(())
}
#[cfg(feature = "postgres")]
CreateColumnImpl::Postgres(mut column) => {
write!(sql, "\"{}\" ", column.name).unwrap();
match postgres_type(
column.data_type,
column.annotations.iter().map(|x| x.annotation),
)? {
PostgresType::Normal(x) => write!(sql, "{x}").unwrap(),
PostgresType::Choices(values) => {
if let Some(stmts) = column.pre_statements {
stmts.push((
format!(
"CREATE TYPE _{}_{} AS ENUM({});",
column.table_name,
column.name,
values
.iter()
.map(|x| { postgres::fmt(x) })
.collect::<Vec<String>>()
.join(", ")
),
vec![],
));
};
write!(sql, "_{}_{}", column.table_name, column.name,).unwrap();
}
};
for x in &column.annotations {
let SQLAnnotation { annotation } = x;
if let Some(s) = &mut column.statements {
trigger_annotation_to_trigger_postgres(
annotation,
column.table_name,
column.name,
s,
);
}
sql.push(' ');
match &annotation {
Annotation::AutoCreateTime => {
write!(
sql,
"DEFAULT {}",
match column.data_type {
DbType::Date => "CURRENT_DATE",
DbType::DateTime => "now()",
DbType::Timestamp => "CURRENT_TIMESTAMP",
DbType::Time => "CURRENT_TIME",
_ =>
return Err(Error::SQLBuildError(format!(
"AutoCreateTime not compatible with {:?}",
column.data_type
))),
}
)
.unwrap();
}
Annotation::DefaultValue(DefaultValue::String(x)) => {
write!(sql, "DEFAULT {}", postgres::fmt(x)).unwrap()
}
Annotation::DefaultValue(DefaultValue::Integer(x)) => {
write!(sql, "DEFAULT {x}").unwrap()
}
Annotation::DefaultValue(DefaultValue::Float(x)) => {
write!(sql, "DEFAULT {x}").unwrap()
}
Annotation::DefaultValue(DefaultValue::Boolean(true)) => {
write!(sql, "DEFAULT true").unwrap()
}
Annotation::DefaultValue(DefaultValue::Boolean(false)) => {
write!(sql, "DEFAULT false").unwrap()
}
Annotation::NotNull => write!(sql, "NOT NULL").unwrap(),
Annotation::PrimaryKey => write!(sql, "PRIMARY KEY").unwrap(),
Annotation::Unique => write!(sql, "UNIQUE").unwrap(),
Annotation::ForeignKey(fk) => write!(
sql,
"REFERENCES \"{}\"(\"{}\") ON DELETE {} ON UPDATE {}",
fk.table_name, fk.column_name, fk.on_delete, fk.on_update
)
.unwrap(),
Annotation::MaxLength(max_length) => {
if matches!(column.data_type, DbType::Text) {
write!(
sql,
"CONSTRAINT \"{}\" CHECK (length(\"{}\") <= {max_length})",
postgres::max_length_check_name(column.table_name, column.name),
column.name,
)
.unwrap();
}
}
_ => {}
};
}
Ok(())
}
}
}
}
pub fn sqlite_type(data_type: DbType) -> Result<&'static str, Error> {
#[allow(deprecated)]
Ok(match data_type {
DbType::Binary | DbType::Uuid => "BLOB",
DbType::VarChar
| DbType::Text
| DbType::Date
| DbType::DateTime
| DbType::Timestamp
| DbType::Time
| DbType::Choices => "TEXT",
DbType::Int8 | DbType::Int16 | DbType::Int32 | DbType::Int64 | DbType::Boolean => "INTEGER",
DbType::Float | DbType::Double => "REAL",
DbType::BitVec | DbType::MacAddress | DbType::IpNetwork => {
return Err(Error::SQLBuildError(format!(
"{data_type:?} is not available for sqlite"
)))
}
})
}
pub enum PostgresType<'a> {
Normal(Cow<'static, str>),
Choices(&'a [String]),
}
pub fn postgres_type<'a>(
data_type: DbType,
annotations: impl IntoIterator<Item = &'a Annotation> + Clone,
) -> Result<PostgresType<'a>, Error> {
let auto_increment = annotations
.clone()
.into_iter()
.any(|x| matches!(x, Annotation::AutoIncrement));
let max_length = annotations.clone().into_iter().find_map(|x| match x {
Annotation::MaxLength(x) => Some(x),
_ => None,
});
let choices = annotations.clone().into_iter().find_map(|x| match x {
Annotation::Choices(x) => Some(x.as_slice()),
_ => None,
});
#[allow(deprecated)]
Ok(PostgresType::Normal(Cow::Borrowed(match data_type {
DbType::Text => "text",
DbType::Uuid => "uuid",
DbType::MacAddress => "macaddr",
DbType::IpNetwork => "inet",
DbType::BitVec => "varbit",
DbType::Binary => "bytea",
DbType::Int8 => "smallint",
DbType::Int16 if auto_increment => "smallserial",
DbType::Int16 => "smallint",
DbType::Int32 if auto_increment => "serial",
DbType::Int32 => "integer",
DbType::Int64 if auto_increment => "bigserial",
DbType::Int64 => "bigint",
DbType::Float => "real",
DbType::Double => "double precision",
DbType::Boolean => "boolean",
DbType::Date => "date",
DbType::DateTime => "timestamptz",
DbType::Timestamp => "timestamp",
DbType::Time => "time",
DbType::VarChar => {
return match max_length {
Some(x) => Ok(PostgresType::Normal(Cow::Owned(format!(
"character varying ({x})"
)))),
None => Err(Error::SQLBuildError(
"character varying must have a max_length annotation".to_string(),
)),
};
}
DbType::Choices => {
return match choices {
Some(x) => Ok(PostgresType::Choices(x)),
None => Err(Error::SQLBuildError(
"VARCHAR must have a MaxLength annotation".to_string(),
)),
};
}
})))
}