use chrono::{NaiveDate, NaiveDateTime};
use super::Dialect;
use crate::{Driver, Error, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ColumnType {
Id,
String(u32),
Text,
Integer,
BigInteger,
Boolean,
DateTime,
}
#[derive(Debug, Clone)]
pub struct Column {
pub(crate) name: String,
pub(crate) ty: ColumnType,
nullable: bool,
default: Option<Value>,
}
impl Column {
pub(crate) fn new(name: &str, ty: ColumnType) -> Self {
Self {
name: name.to_owned(),
ty,
nullable: false,
default: None,
}
}
pub fn nullable(&mut self) -> &mut Self {
self.nullable = true;
self
}
pub fn default(&mut self, value: impl Into<Value>) -> &mut Self {
self.default = Some(value.into());
self
}
pub fn length(&mut self, length: u32) -> &mut Self {
if let ColumnType::String(n) = &mut self.ty {
*n = length;
}
self
}
pub(crate) fn render(&self, driver: Driver, dialect: Dialect) -> Result<String, Error> {
crate::query::validate(&self.name)?;
let name = driver.quote(&self.name);
if self.ty == ColumnType::Id {
let id = match dialect {
Dialect::MySql => "BIGINT AUTO_INCREMENT PRIMARY KEY",
Dialect::Postgres => "BIGSERIAL PRIMARY KEY",
Dialect::Sqlite => "INTEGER PRIMARY KEY AUTOINCREMENT",
};
return Ok(format!("{name} {id}"));
}
let mut sql = format!("{name} {}", self.sql_type(dialect));
sql.push_str(if self.nullable { " NULL" } else { " NOT NULL" });
if let Some(value) = &self.default {
sql.push_str(" DEFAULT ");
sql.push_str(&literal(value, driver, dialect)?);
}
Ok(sql)
}
fn sql_type(&self, dialect: Dialect) -> String {
match (self.ty, dialect) {
(ColumnType::Id, _) => unreachable!("an id column is rendered whole"),
(ColumnType::String(_), Dialect::Sqlite) => "TEXT".to_owned(),
(ColumnType::String(n), _) => format!("VARCHAR({n})"),
(ColumnType::Text, _) => "TEXT".to_owned(),
(ColumnType::Integer, _) => "INTEGER".to_owned(),
(ColumnType::BigInteger, Dialect::Sqlite) => "INTEGER".to_owned(),
(ColumnType::BigInteger, _) => "BIGINT".to_owned(),
(ColumnType::Boolean, Dialect::Postgres) => "BOOLEAN".to_owned(),
(ColumnType::Boolean, _) => "TINYINT(1)".to_owned(),
(ColumnType::DateTime, Dialect::Postgres) => {
"TIMESTAMP(0) WITHOUT TIME ZONE".to_owned()
}
(ColumnType::DateTime, _) => "DATETIME".to_owned(),
}
}
}
fn literal(value: &Value, driver: Driver, dialect: Dialect) -> Result<String, Error> {
let unsupported = || Error::Unsupported {
operation: "a column default of this type",
driver: driver.name(),
};
Ok(match value {
Value::Null => "NULL".to_owned(),
Value::Bool(b) => match (dialect, b) {
(Dialect::Postgres, true) => "true".to_owned(),
(Dialect::Postgres, false) => "false".to_owned(),
(_, true) => "1".to_owned(),
(_, false) => "0".to_owned(),
},
Value::Int(n) => n.to_string(),
Value::UInt(n) => n.to_string(),
Value::Float(f) if f.is_finite() => format!("{f:?}"),
Value::Float(_) => return Err(unsupported()),
Value::Text(s) => quote_text(s, dialect),
Value::DateTime(d) => quote_text(&format_datetime(d), dialect),
Value::Date(d) => quote_text(&format_date(d), dialect),
Value::Bytes(_) => return Err(unsupported()),
})
}
fn format_datetime(d: &NaiveDateTime) -> String {
d.format("%Y-%m-%d %H:%M:%S").to_string()
}
fn format_date(d: &NaiveDate) -> String {
d.format("%Y-%m-%d").to_string()
}
fn quote_text(s: &str, dialect: Dialect) -> String {
let mut out = s.replace('\'', "''");
if dialect == Dialect::MySql {
out = out.replace('\\', "\\\\");
}
format!("'{out}'")
}