use crate::error::SammError;
use crate::metamodel::{Aspect, ModelElement};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SqlDialect {
PostgreSql,
MySql,
Sqlite,
}
pub fn generate_sql(aspect: &Aspect, dialect: SqlDialect) -> Result<String, SammError> {
match dialect {
SqlDialect::PostgreSql => generate_postgresql(aspect),
SqlDialect::MySql => generate_mysql(aspect),
SqlDialect::Sqlite => generate_sqlite(aspect),
}
}
fn generate_postgresql(aspect: &Aspect) -> Result<String, SammError> {
let table_name = to_snake_case(&aspect.name());
let mut sql = String::new();
let mut foreign_keys = Vec::new();
sql.push_str(&format!("-- PostgreSQL DDL for {}\n", aspect.name()));
sql.push_str(&format!("CREATE TABLE {} (\n", table_name));
sql.push_str(" id BIGSERIAL PRIMARY KEY,\n");
for prop in aspect.properties() {
let col_name = to_snake_case(&prop.name());
let (sql_type, is_foreign_key) = if let Some(char) = &prop.characteristic {
match char.kind() {
crate::metamodel::CharacteristicKind::SingleEntity { entity_type } => {
let ref_table =
to_snake_case(entity_type.split('#').next_back().unwrap_or(entity_type));
foreign_keys.push((col_name.clone(), ref_table));
("BIGINT".to_string(), true)
}
_ => {
if let Some(dt) = &char.data_type {
(map_xsd_to_postgresql(dt), false)
} else {
("TEXT".to_string(), false)
}
}
}
} else {
("TEXT".to_string(), false)
};
let nullable = if prop.optional { "" } else { " NOT NULL" };
sql.push_str(&format!(" {} {}{},\n", col_name, sql_type, nullable));
}
sql.push_str(" created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,\n");
sql.push_str(" updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP");
for (col_name, ref_table) in &foreign_keys {
sql.push_str(",\n");
sql.push_str(&format!(
" CONSTRAINT fk_{}_{} FOREIGN KEY ({}) REFERENCES {} (id)",
table_name, col_name, col_name, ref_table
));
}
sql.push_str("\n);\n\n");
for prop in aspect.properties() {
if !prop.optional {
let col_name = to_snake_case(&prop.name());
sql.push_str(&format!(
"CREATE INDEX idx_{}_{} ON {} ({});\n",
table_name, col_name, table_name, col_name
));
}
}
for (col_name, _) in &foreign_keys {
sql.push_str(&format!(
"CREATE INDEX idx_{}_{}_fk ON {} ({});\n",
table_name, col_name, table_name, col_name
));
}
Ok(sql)
}
fn generate_mysql(aspect: &Aspect) -> Result<String, SammError> {
let table_name = to_snake_case(&aspect.name());
let mut sql = String::new();
let mut foreign_keys = Vec::new();
sql.push_str(&format!("-- MySQL DDL for {}\n", aspect.name()));
sql.push_str(&format!("CREATE TABLE {} (\n", table_name));
sql.push_str(" id BIGINT AUTO_INCREMENT PRIMARY KEY,\n");
for prop in aspect.properties() {
let col_name = to_snake_case(&prop.name());
let (sql_type, _is_foreign_key) = if let Some(char) = &prop.characteristic {
match char.kind() {
crate::metamodel::CharacteristicKind::SingleEntity { entity_type } => {
let ref_table =
to_snake_case(entity_type.split('#').next_back().unwrap_or(entity_type));
foreign_keys.push((col_name.clone(), ref_table));
("BIGINT".to_string(), true)
}
_ => {
if let Some(dt) = &char.data_type {
(map_xsd_to_mysql(dt), false)
} else {
("TEXT".to_string(), false)
}
}
}
} else {
("TEXT".to_string(), false)
};
let nullable = if prop.optional { "" } else { " NOT NULL" };
sql.push_str(&format!(" {} {}{},\n", col_name, sql_type, nullable));
}
sql.push_str(" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,\n");
sql.push_str(" updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP");
for (col_name, ref_table) in &foreign_keys {
sql.push_str(",\n");
sql.push_str(&format!(
" CONSTRAINT fk_{}_{} FOREIGN KEY ({}) REFERENCES {} (id)",
table_name, col_name, col_name, ref_table
));
}
sql.push_str("\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n\n");
for (col_name, _) in &foreign_keys {
sql.push_str(&format!(
"CREATE INDEX idx_{}_{}_fk ON {} ({});\n",
table_name, col_name, table_name, col_name
));
}
Ok(sql)
}
fn generate_sqlite(aspect: &Aspect) -> Result<String, SammError> {
let table_name = to_snake_case(&aspect.name());
let mut sql = String::new();
let mut foreign_keys = Vec::new();
sql.push_str(&format!("-- SQLite DDL for {}\n", aspect.name()));
sql.push_str(&format!("CREATE TABLE {} (\n", table_name));
sql.push_str(" id INTEGER PRIMARY KEY AUTOINCREMENT,\n");
for prop in aspect.properties() {
let col_name = to_snake_case(&prop.name());
let (sql_type, _is_foreign_key) = if let Some(char) = &prop.characteristic {
match char.kind() {
crate::metamodel::CharacteristicKind::SingleEntity { entity_type } => {
let ref_table =
to_snake_case(entity_type.split('#').next_back().unwrap_or(entity_type));
foreign_keys.push((col_name.clone(), ref_table));
("INTEGER".to_string(), true)
}
_ => {
if let Some(dt) = &char.data_type {
(map_xsd_to_sqlite(dt), false)
} else {
("TEXT".to_string(), false)
}
}
}
} else {
("TEXT".to_string(), false)
};
let nullable = if prop.optional { "" } else { " NOT NULL" };
sql.push_str(&format!(" {} {}{},\n", col_name, sql_type, nullable));
}
sql.push_str(" created_at TEXT DEFAULT CURRENT_TIMESTAMP,\n");
sql.push_str(" updated_at TEXT DEFAULT CURRENT_TIMESTAMP");
for (col_name, ref_table) in &foreign_keys {
sql.push_str(",\n");
sql.push_str(&format!(
" FOREIGN KEY ({}) REFERENCES {} (id)",
col_name, ref_table
));
}
sql.push_str("\n);\n\n");
for (col_name, _) in &foreign_keys {
sql.push_str(&format!(
"CREATE INDEX idx_{}_{}_fk ON {} ({});\n",
table_name, col_name, table_name, col_name
));
}
Ok(sql)
}
fn map_xsd_to_postgresql(xsd_type: &str) -> String {
match xsd_type {
t if t.ends_with("string") => "TEXT".to_string(),
t if t.ends_with("int") | t.ends_with("integer") => "INTEGER".to_string(),
t if t.ends_with("long") => "BIGINT".to_string(),
t if t.ends_with("short") | t.ends_with("byte") => "SMALLINT".to_string(),
t if t.ends_with("decimal") => "NUMERIC".to_string(),
t if t.ends_with("float") => "REAL".to_string(),
t if t.ends_with("double") => "DOUBLE PRECISION".to_string(),
t if t.ends_with("boolean") => "BOOLEAN".to_string(),
t if t.ends_with("date") => "DATE".to_string(),
t if t.ends_with("dateTime") | t.ends_with("dateTimeStamp") => {
"TIMESTAMP WITH TIME ZONE".to_string()
}
t if t.ends_with("time") => "TIME".to_string(),
t if t.ends_with("anyURI") => "TEXT".to_string(),
_ => "TEXT".to_string(),
}
}
fn map_xsd_to_mysql(xsd_type: &str) -> String {
match xsd_type {
t if t.ends_with("string") => "VARCHAR(255)".to_string(),
t if t.ends_with("int") | t.ends_with("integer") => "INT".to_string(),
t if t.ends_with("long") => "BIGINT".to_string(),
t if t.ends_with("short") | t.ends_with("byte") => "SMALLINT".to_string(),
t if t.ends_with("decimal") => "DECIMAL(10,2)".to_string(),
t if t.ends_with("float") => "FLOAT".to_string(),
t if t.ends_with("double") => "DOUBLE".to_string(),
t if t.ends_with("boolean") => "BOOLEAN".to_string(),
t if t.ends_with("date") => "DATE".to_string(),
t if t.ends_with("dateTime") | t.ends_with("dateTimeStamp") => "DATETIME".to_string(),
t if t.ends_with("time") => "TIME".to_string(),
t if t.ends_with("anyURI") => "TEXT".to_string(),
_ => "TEXT".to_string(),
}
}
fn map_xsd_to_sqlite(xsd_type: &str) -> String {
match xsd_type {
t if t.ends_with("string") => "TEXT".to_string(),
t if t.ends_with("int")
| t.ends_with("integer")
| t.ends_with("long")
| t.ends_with("short")
| t.ends_with("byte") =>
{
"INTEGER".to_string()
}
t if t.ends_with("decimal") | t.ends_with("float") | t.ends_with("double") => {
"REAL".to_string()
}
t if t.ends_with("boolean") => "INTEGER".to_string(), t if t.ends_with("date")
| t.ends_with("dateTime")
| t.ends_with("dateTimeStamp")
| t.ends_with("time") =>
{
"TEXT".to_string()
}
_ => "TEXT".to_string(),
}
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() {
if i > 0 {
result.push('_');
}
result.push(
ch.to_lowercase()
.next()
.expect("lowercase should produce a character"),
);
} else {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sql_dialect_variants() {
assert_eq!(SqlDialect::PostgreSql, SqlDialect::PostgreSql);
assert_ne!(SqlDialect::MySql, SqlDialect::Sqlite);
}
#[test]
fn test_snake_case_conversion() {
assert_eq!(to_snake_case("MovementAspect"), "movement_aspect");
assert_eq!(to_snake_case("position"), "position");
}
}