#![warn(missing_docs)]
mod column;
mod constraints;
mod default;
mod macros;
mod uuid;
mod validators;
mod value;
use std::collections::HashMap;
use std::marker::PhantomData;
pub use crate::schema::constraints::ColumnConstraint;
pub use crate::schema::constraints::GeneratedColumn;
pub use crate::schema::default::DefaultToSql;
pub use crate::schema::default::DefaultValueEnum;
pub use crate::schema::validators::ColumnValidators;
use crate::table::TableDefinition;
pub use column::Column;
use std::fmt::Debug;
pub use uuid::Uuid;
pub use value::Value;
pub use value::convert_to_value;
pub trait Schema {
fn table_name() -> &'static str;
fn get_all_columns() -> Vec<ColumnInfo<'static>>;
fn ensure_registered();
fn values(&self) -> HashMap<String, Value>;
}
pub trait UpdateTrait {
fn get_updated(self) -> Vec<(&'static str, Value)>;
}
pub trait Select {
fn default() -> Self;
fn get_selected(self) -> Vec<&'static str>;
}
#[derive(Debug, Clone)]
pub struct ColumnInfo<'a> {
pub name: &'static str,
pub data_type: &'static str,
pub has_default: bool,
pub default_sql: Option<DefaultValueEnum<String>>,
pub comment: Option<&'static str>,
pub charset: Option<&'static str>,
pub collate: Option<&'static str>,
pub validators: &'a Vec<ColumnValidators>,
pub constraints: &'a Vec<ColumnConstraint>,
}
pub fn type_to_sql_string<T: 'static>() -> &'static str {
use std::any::TypeId;
let type_id = TypeId::of::<T>();
#[cfg(feature = "postgres")]
{
if type_id == TypeId::of::<Vec<String>>() {
return "TEXT[]";
} else if type_id == TypeId::of::<Vec<bool>>() {
return "BOOLEAN[]";
} else if type_id == TypeId::of::<Vec<i8>>() || type_id == TypeId::of::<Vec<i16>>() {
return "SMALLINT[]";
} else if type_id == TypeId::of::<Vec<i32>>()
|| type_id == TypeId::of::<Vec<u16>>()
|| type_id == TypeId::of::<Vec<u32>>()
{
return "INT[]";
} else if type_id == TypeId::of::<Vec<i64>>() || type_id == TypeId::of::<Vec<u64>>() {
return "BIGINT[]";
} else if type_id == TypeId::of::<Vec<f32>>() {
return "REAL[]";
} else if type_id == TypeId::of::<Vec<f64>>() {
return "DOUBLE PRECISION[]";
}
}
#[cfg(any(feature = "mysql", feature = "sqlite"))]
{
if type_id == TypeId::of::<Vec<String>>()
|| type_id == TypeId::of::<Vec<bool>>()
|| type_id == TypeId::of::<Vec<i8>>()
|| type_id == TypeId::of::<Vec<i16>>()
|| type_id == TypeId::of::<Vec<i32>>()
|| type_id == TypeId::of::<Vec<i64>>()
|| type_id == TypeId::of::<Vec<u8>>()
|| type_id == TypeId::of::<Vec<u16>>()
|| type_id == TypeId::of::<Vec<u32>>()
|| type_id == TypeId::of::<Vec<u64>>()
|| type_id == TypeId::of::<Vec<f32>>()
|| type_id == TypeId::of::<Vec<f64>>()
{
return "JSON";
}
}
if type_id == TypeId::of::<crate::schema::Uuid>() {
#[cfg(feature = "postgres")]
{
return "UUID";
}
#[cfg(any(feature = "mysql", feature = "sqlite"))]
{
return "CHAR(36)";
}
} else if type_id == TypeId::of::<String>() {
"VARCHAR(255)"
} else if type_id == TypeId::of::<i8>() {
"TINYINT"
} else if type_id == TypeId::of::<i16>() {
"SMALLINT"
} else if type_id == TypeId::of::<i32>() {
"INT"
} else if type_id == TypeId::of::<i64>() {
"BIGINT"
} else if type_id == TypeId::of::<u8>() {
"TINYINT UNSIGNED"
} else if type_id == TypeId::of::<u16>() {
"SMALLINT UNSIGNED"
} else if type_id == TypeId::of::<u32>() {
"INT UNSIGNED"
} else if type_id == TypeId::of::<u64>() {
"BIGINT UNSIGNED"
} else if type_id == TypeId::of::<f32>() {
"FLOAT"
} else if type_id == TypeId::of::<f64>() {
"DOUBLE"
} else if type_id == TypeId::of::<bool>() {
"BOOLEAN"
} else if type_id == TypeId::of::<time::Date>() {
"DATE"
} else if type_id == TypeId::of::<time::OffsetDateTime>() {
"DATETIME"
} else {
"VARCHAR(255)" }
}
#[derive(Debug)]
pub(crate) struct SchemaWrapper<T: Schema + Debug> {
_phantom: PhantomData<T>,
}
impl<T: Schema + Debug> Clone for SchemaWrapper<T> {
fn clone(&self) -> Self {
Self {
_phantom: PhantomData,
}
}
}
impl<T: Schema + Debug> SchemaWrapper<T> {
pub(crate) fn new() -> Self {
Self {
_phantom: PhantomData,
}
}
}
impl<T: Schema + Debug + Sync + Send + 'static> TableDefinition for SchemaWrapper<T> {
fn table_name(&self) -> &'static str {
T::table_name()
}
fn get_columns(&self) -> Vec<ColumnInfo<'static>> {
T::get_all_columns()
}
fn to_create_sql(&self) -> String {
let table_name = self.table_name();
let columns = self.get_columns();
let mut sql = format!("CREATE TABLE IF NOT EXISTS {} (\n", table_name);
let column_definitions: Vec<String> = columns
.iter()
.map(|col| {
let mut def = format!(" {} {}", col.name, col.data_type);
let constraints = col.constraints;
for constraint in constraints {
match constraint {
ColumnConstraint::NonNullable => {
def.push_str(" NOT NULL");
}
ColumnConstraint::Unique => {
def.push_str(" UNIQUE");
}
ColumnConstraint::PrimaryKey => {
def.push_str(" PRIMARY KEY");
}
ColumnConstraint::Indexed => {}
ColumnConstraint::AutoIncrement => {
if is_mysql_integer_type(col.data_type) {
def.push_str(" AUTO_INCREMENT");
}
}
ColumnConstraint::Invisible => {
def.push_str(" INVISIBLE");
}
ColumnConstraint::OnUpdateCurrentTimestamp => {
def.push_str(" ON UPDATE CURRENT_TIMESTAMP");
}
ColumnConstraint::Check(expression) => {
def.push_str(&format!(" CHECK ({})", expression));
}
ColumnConstraint::Generated(generated) => {
def.push_str(&format!(" GENERATED {}", generated));
}
}
}
if col.comment.is_some() {
let escaped = col.comment.unwrap().replace("'", "''");
def.push_str(&format!(" COMMENT '{}'", escaped));
}
if col.charset.is_some() {
def.push_str(&format!(" CHARACTER SET {}", col.charset.unwrap()));
}
if col.collate.is_some() {
def.push_str(&format!(" COLLATE {}", col.collate.unwrap()));
}
if col.has_default {
if let Some(ref default) = col.default_sql {
if let DefaultValueEnum::Value(default) = default {
let is_empty_string = default == "" || default == "''";
let is_primary_key =
col.constraints.contains(&ColumnConstraint::PrimaryKey);
if is_primary_key && is_empty_string {
} else {
let needs_quotes = col.data_type == "TEXT"
|| col.data_type.starts_with("VARCHAR")
|| col.data_type == "CHAR"
|| col.data_type == "STRING"
|| col.data_type == "UUID";
if needs_quotes
&& !(default.starts_with('\'') && default.ends_with('\''))
{
def.push_str(&format!(
" DEFAULT '{}'",
default.replace('\'', "''")
));
} else {
def.push_str(&format!(" DEFAULT {}", default));
}
}
} else if &DefaultValueEnum::CurrentTimestamp == default {
def.push_str(" DEFAULT CURRENT_TIMESTAMP");
} else if &DefaultValueEnum::Random == default {
def.push_str(" DEFAULT (UUID())");
}
}
}
def
})
.collect();
sql.push_str(&column_definitions.join(",\n"));
sql.push_str("\n);");
let indexes: Vec<String> = columns
.iter()
.filter(|col| {
col.constraints.contains(&ColumnConstraint::Indexed)
&& !col.constraints.contains(&ColumnConstraint::PrimaryKey)
})
.map(|col| {
format!(
"CREATE INDEX idx_{}_{} ON {} ({});",
table_name, col.name, table_name, col.name
)
})
.collect();
if !indexes.is_empty() {
sql.push_str("\n\n");
sql.push_str(&indexes.join("\n"));
}
sql
}
fn clone_box(&self) -> Box<dyn TableDefinition> {
Box::new(self.clone())
}
}
fn is_mysql_integer_type(data_type: &str) -> bool {
match data_type {
"TINYINT" | "SMALLINT" | "MEDIUMINT" | "INT" | "INTEGER" | "BIGINT" |
"TINYINT UNSIGNED" | "SMALLINT UNSIGNED" | "MEDIUMINT UNSIGNED" |
"INT UNSIGNED" | "INTEGER UNSIGNED" | "BIGINT UNSIGNED" => true,
_ => false,
}
}
pub trait CustomSqlType {}