use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, path::Path, sync::Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DatabaseType {
Turso,
Postgres,
MySql,
}
impl DatabaseType {
pub fn current() -> Self {
#[cfg(feature = "postgres")]
{
return DatabaseType::Postgres;
}
#[cfg(feature = "mysql")]
{
return DatabaseType::MySql;
}
#[cfg(all(not(feature = "postgres"), not(feature = "mysql")))]
{
return DatabaseType::Turso;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ColumnType {
Integer,
Real,
Text,
Blob,
}
impl ColumnType {
pub fn to_sql(&self) -> &'static str {
self.to_sql_for(DatabaseType::Turso)
}
pub fn to_sql_for(&self, db_type: DatabaseType) -> &'static str {
match db_type {
DatabaseType::Turso => match self {
ColumnType::Integer => "INTEGER",
ColumnType::Real => "REAL",
ColumnType::Text => "TEXT",
ColumnType::Blob => "BLOB",
},
DatabaseType::Postgres => match self {
ColumnType::Integer => "BIGINT",
ColumnType::Real => "DOUBLE PRECISION",
ColumnType::Text => "TEXT",
ColumnType::Blob => "BYTEA",
},
DatabaseType::MySql => match self {
ColumnType::Integer => "BIGINT",
ColumnType::Real => "DOUBLE",
ColumnType::Text => "VARCHAR(255)",
ColumnType::Blob => "BLOB",
},
}
}
#[cfg(feature = "mysql")]
pub fn to_mysql_sql(&self) -> &'static str {
self.to_sql_for(DatabaseType::MySql)
}
pub fn from_sql(sql: &str) -> Self {
match sql.to_uppercase().as_str() {
"INTEGER" | "INT" | "BIGINT" | "SMALLINT" | "TINYINT" | "SERIAL" | "BIGSERIAL" => ColumnType::Integer,
"REAL" | "FLOAT" | "DOUBLE" | "NUMERIC" | "DECIMAL" | "DOUBLE PRECISION" => ColumnType::Real,
"TEXT" | "VARCHAR" | "CHAR" | "STRING" | "VARCHAR(255)" | "TEXT[]" => ColumnType::Text,
"BLOB" | "BINARY" | "BYTEA" | "LONGBLOB" => ColumnType::Blob,
_ => ColumnType::Text,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnDef {
pub name: String,
pub col_type: ColumnType,
pub nullable: bool,
pub primary_key: bool,
pub auto_increment: bool,
pub default_value: Option<String>,
pub unique: bool,
}
impl ColumnDef {
pub fn new(name: impl Into<String>, col_type: ColumnType) -> Self {
Self {
name: name.into(),
col_type,
nullable: true,
primary_key: false,
auto_increment: false,
default_value: None,
unique: false,
}
}
pub fn not_null(mut self) -> Self {
self.nullable = false;
self
}
pub fn primary_key(mut self) -> Self {
self.primary_key = true;
self.nullable = false;
self
}
pub fn auto_increment(mut self) -> Self {
self.auto_increment = true;
self
}
pub fn default(mut self, value: impl Into<String>) -> Self {
self.default_value = Some(value.into());
self
}
pub fn unique(mut self) -> Self {
self.unique = true;
self
}
pub fn to_sql(&self) -> String {
self.to_sql_for(DatabaseType::current())
}
pub fn to_sql_for(&self, db_type: DatabaseType) -> String {
let mut sql = format!("{} {}", self.name, self.col_type.to_sql_for(db_type));
if self.primary_key {
sql.push_str(" PRIMARY KEY");
}
if self.auto_increment {
match db_type {
DatabaseType::Turso => sql.push_str(" AUTOINCREMENT"),
DatabaseType::Postgres => {
if self.primary_key && self.col_type == ColumnType::Integer {
sql = format!("{} SERIAL", self.name);
sql.push_str(" PRIMARY KEY");
}
}
DatabaseType::MySql => sql.push_str(" AUTO_INCREMENT"),
}
}
if !self.nullable && !self.primary_key {
sql.push_str(" NOT NULL");
}
if let Some(ref default) = self.default_value {
sql.push_str(&format!(" DEFAULT {}", default));
}
if self.unique && !self.primary_key {
sql.push_str(" UNIQUE");
}
sql
}
#[cfg(feature = "mysql")]
pub fn to_mysql_sql(&self) -> String {
self.to_sql_for(DatabaseType::MySql)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexDef {
pub name: String,
pub table_name: String,
pub columns: Vec<String>,
pub unique: bool,
}
impl IndexDef {
pub fn new(name: impl Into<String>, table_name: impl Into<String>, columns: Vec<String>) -> Self {
Self { name: name.into(), table_name: table_name.into(), columns, unique: false }
}
pub fn unique(mut self) -> Self {
self.unique = true;
self
}
pub fn to_create_sql(&self) -> String {
self.to_create_sql_for(DatabaseType::current())
}
pub fn to_create_sql_for(&self, db_type: DatabaseType) -> String {
let unique_str = if self.unique { "UNIQUE " } else { "" };
let columns = self.columns.join(", ");
match db_type {
DatabaseType::Turso | DatabaseType::Postgres => {
format!("CREATE {}INDEX {} ON {} ({})", unique_str, self.name, self.table_name, columns)
}
DatabaseType::MySql => {
format!("CREATE {}INDEX {} ON {} ({})", unique_str, self.name, self.table_name, columns)
}
}
}
pub fn to_drop_sql(&self) -> String {
format!("DROP INDEX IF EXISTS {}", self.name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseSchema {
pub name: String,
pub r#type: DatabaseType,
pub url: Option<String>,
pub env: Option<String>,
pub schemas: Vec<TableSchema>,
}
impl DatabaseSchema {
pub fn new(name: impl Into<String>, db_type: DatabaseType) -> Self {
Self { name: name.into(), r#type: db_type, url: None, env: None, schemas: Vec::new() }
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
pub fn env(mut self, env: impl Into<String>) -> Self {
self.env = Some(env.into());
self
}
pub fn schema(mut self, schema: TableSchema) -> Self {
self.schemas.push(schema);
self
}
pub fn get_url(&self) -> Result<String, Box<dyn std::error::Error>> {
if let Some(url) = &self.url {
Ok(url.clone())
}
else if let Some(env_name) = &self.env {
std::env::var(env_name).map_err(|e| format!("Failed to read environment variable {}: {}", env_name, e).into())
}
else {
Err("No database URL or environment variable specified".into())
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TableSchema {
pub name: String,
pub columns: Vec<ColumnDef>,
pub indexes: Vec<IndexDef>,
pub foreign_keys: Vec<ForeignKeyDef>,
}
impl TableSchema {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into(), columns: Vec::new(), indexes: Vec::new(), foreign_keys: Vec::new() }
}
pub fn column(mut self, col: ColumnDef) -> Self {
self.columns.push(col);
self
}
pub fn index(mut self, idx: IndexDef) -> Self {
self.indexes.push(idx);
self
}
pub fn foreign_key(mut self, fk: ForeignKeyDef) -> Self {
self.foreign_keys.push(fk);
self
}
pub fn to_drop_sql(&self) -> String {
format!("DROP TABLE IF EXISTS {}", self.name)
}
pub fn primary_key(&self) -> Option<&ColumnDef> {
self.columns.iter().find(|c| c.primary_key)
}
pub fn get_column(&self, name: &str) -> Option<&ColumnDef> {
self.columns.iter().find(|c| c.name == name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForeignKeyDef {
pub name: String,
pub column: String,
pub ref_table: String,
pub ref_column: String,
pub on_update: ReferentialAction,
pub on_delete: ReferentialAction,
}
impl ForeignKeyDef {
pub fn new(
name: impl Into<String>,
column: impl Into<String>,
ref_table: impl Into<String>,
ref_column: impl Into<String>,
) -> Self {
Self {
name: name.into(),
column: column.into(),
ref_table: ref_table.into(),
ref_column: ref_column.into(),
on_update: ReferentialAction::NoAction,
on_delete: ReferentialAction::NoAction,
}
}
pub fn on_update(mut self, action: ReferentialAction) -> Self {
self.on_update = action;
self
}
pub fn on_delete(mut self, action: ReferentialAction) -> Self {
self.on_delete = action;
self
}
pub fn to_constraint_sql(&self) -> String {
format!(
"CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({}) ON UPDATE {} ON DELETE {}",
self.name,
self.column,
self.ref_table,
self.ref_column,
self.on_update.to_sql(),
self.on_delete.to_sql()
)
}
pub fn to_add_sql(&self, table_name: &str) -> String {
format!("ALTER TABLE {} ADD {}", table_name, self.to_constraint_sql())
}
pub fn to_drop_sql(&self, table_name: &str) -> String {
format!("ALTER TABLE {} DROP CONSTRAINT {}", table_name, self.name)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReferentialAction {
NoAction,
Restrict,
Cascade,
SetNull,
SetDefault,
}
impl ReferentialAction {
pub fn to_sql(&self) -> &'static str {
match self {
ReferentialAction::NoAction => "NO ACTION",
ReferentialAction::Restrict => "RESTRICT",
ReferentialAction::Cascade => "CASCADE",
ReferentialAction::SetNull => "SET NULL",
ReferentialAction::SetDefault => "SET DEFAULT",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaConfig {
pub databases: Vec<DatabaseSchema>,
pub default_database: Option<String>,
}
impl SchemaConfig {
pub fn new() -> Self {
Self { databases: Vec::new(), default_database: None }
}
pub fn database(mut self, database: DatabaseSchema) -> Self {
self.databases.push(database);
self
}
pub fn default_database(mut self, name: impl Into<String>) -> Self {
self.default_database = Some(name.into());
self
}
pub fn get_default_database(&self) -> Option<&DatabaseSchema> {
if let Some(default_name) = &self.default_database {
self.databases.iter().find(|db| db.name == *default_name)
}
else {
self.databases.first()
}
}
pub fn get_database(&self, name: &str) -> Option<&DatabaseSchema> {
self.databases.iter().find(|db| db.name == name)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseLinkConfig {
pub r#type: DatabaseType,
pub url: Option<String>,
pub env: Option<String>,
}
impl DatabaseLinkConfig {
pub fn get_url(&self) -> Result<String, Box<dyn std::error::Error>> {
if let Some(url) = &self.url {
Ok(url.clone())
}
else if let Some(env_name) = &self.env {
std::env::var(env_name).map_err(|e| format!("Failed to read environment variable {}: {}", env_name, e).into())
}
else {
Err("No database URL or environment variable specified".into())
}
}
}
pub mod col {
use super::{ColumnDef, ColumnType};
pub fn integer(name: &str) -> ColumnDef {
ColumnDef::new(name, ColumnType::Integer)
}
pub fn real(name: &str) -> ColumnDef {
ColumnDef::new(name, ColumnType::Real)
}
pub fn text(name: &str) -> ColumnDef {
ColumnDef::new(name, ColumnType::Text)
}
pub fn blob(name: &str) -> ColumnDef {
ColumnDef::new(name, ColumnType::Blob)
}
pub fn id(name: &str) -> ColumnDef {
ColumnDef::new(name, ColumnType::Integer).primary_key().auto_increment()
}
pub fn boolean(name: &str) -> ColumnDef {
ColumnDef::new(name, ColumnType::Integer).default("0")
}
pub fn timestamp(name: &str) -> ColumnDef {
ColumnDef::new(name, ColumnType::Text)
}
pub fn json(name: &str) -> ColumnDef {
ColumnDef::new(name, ColumnType::Text)
}
}
static SCHEMA_REGISTRY: Lazy<Mutex<HashMap<String, TableSchema>>> = Lazy::new(|| Mutex::new(HashMap::new()));
pub fn register_schema(schema: TableSchema) {
let mut registry = SCHEMA_REGISTRY.lock().unwrap();
registry.insert(schema.name.clone(), schema);
}
pub fn register_schemas(schemas: Vec<TableSchema>) {
let mut registry = SCHEMA_REGISTRY.lock().unwrap();
for schema in schemas {
registry.insert(schema.name.clone(), schema);
}
}
pub fn get_registered_schemas() -> Vec<TableSchema> {
let registry = SCHEMA_REGISTRY.lock().unwrap();
registry.values().cloned().collect()
}
pub fn get_schema(name: &str) -> Option<TableSchema> {
let registry = SCHEMA_REGISTRY.lock().unwrap();
registry.get(name).cloned()
}
pub fn clear_schemas() {
let mut registry = SCHEMA_REGISTRY.lock().unwrap();
registry.clear();
}
pub fn export_schemas_to_yaml() -> String {
let schemas = get_registered_schemas();
serde_yaml::to_string(&schemas).unwrap_or_else(|e| format!("# Error: {}", e))
}
#[cfg(debug_assertions)]
pub fn export_schemas_to_yaml_file(path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
use std::{fs::File, io::Write};
let yaml = export_schemas_to_yaml();
let mut file = File::create(path)?;
file.write_all(yaml.as_bytes())?;
Ok(())
}
#[cfg(debug_assertions)]
pub fn auto_export_schemas() {
let path = std::path::Path::new("schemas.yaml");
if let Err(e) = export_schemas_to_yaml_file(path) {
eprintln!("Warning: Failed to export schemas: {}", e);
}
else {
println!("Schemas exported to: {}", path.display());
}
}
impl TableSchema {
pub fn register(self) -> Self {
let name = self.name.clone();
register_schema(self);
get_schema(&name).unwrap()
}
pub fn to_yaml(&self) -> String {
serde_yaml::to_string(self).unwrap_or_else(|e| format!("# Error: {}", e))
}
pub fn to_create_sql(&self) -> String {
self.to_create_sql_for(DatabaseType::current())
}
pub fn to_create_sql_for(&self, db_type: DatabaseType) -> String {
let mut parts: Vec<String> = self.columns.iter().map(|c| c.to_sql_for(db_type)).collect();
for fk in &self.foreign_keys {
parts.push(fk.to_constraint_sql());
}
match db_type {
DatabaseType::Turso | DatabaseType::Postgres => {
format!("CREATE TABLE {} ({})", self.name, parts.join(", "))
}
DatabaseType::MySql => {
format!("CREATE TABLE {} ({}) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", self.name, parts.join(", "))
}
}
}
#[cfg(feature = "mysql")]
pub fn to_mysql_create_sql(&self) -> String {
self.to_create_sql_for(DatabaseType::MySql)
}
pub fn to_create_indexes_sql(&self) -> Vec<String> {
self.to_create_indexes_sql_for(DatabaseType::current())
}
pub fn to_create_indexes_sql_for(&self, db_type: DatabaseType) -> Vec<String> {
self.indexes.iter().map(|idx| idx.to_create_sql_for(db_type)).collect()
}
pub fn to_full_create_sql(&self) -> Vec<String> {
self.to_full_create_sql_for(DatabaseType::current())
}
pub fn to_full_create_sql_for(&self, db_type: DatabaseType) -> Vec<String> {
let mut sqls = vec![self.to_create_sql_for(db_type)];
sqls.extend(self.to_create_indexes_sql_for(db_type));
sqls
}
}
pub fn load_schema_config_from_yaml(yaml_str: &str) -> Result<SchemaConfig, serde_yaml::Error> {
serde_yaml::from_str(yaml_str)
}
pub fn load_schema_config_from_yaml_file(path: impl AsRef<Path>) -> Result<SchemaConfig, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?;
let config = load_schema_config_from_yaml(&content)?;
Ok(config)
}
pub fn load_schemas_from_yaml(yaml_str: &str) -> Result<Vec<TableSchema>, serde_yaml::Error> {
serde_yaml::from_str(yaml_str)
}
pub fn load_schemas_from_yaml_file(path: impl AsRef<Path>) -> Result<Vec<TableSchema>, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?;
let schemas = load_schemas_from_yaml(&content)?;
Ok(schemas)
}
pub fn load_and_register_schemas_from_yaml_file(path: impl AsRef<Path>) -> Result<(), Box<dyn std::error::Error>> {
let schemas = load_schemas_from_yaml_file(path)?;
register_schemas(schemas);
Ok(())
}
pub fn export_schema_config_to_yaml(config: &SchemaConfig) -> String {
serde_yaml::to_string(config).unwrap_or_else(|e| format!("# Error: {}", e))
}
pub fn export_schema_config_to_yaml_file(config: &SchemaConfig, path: impl AsRef<Path>) -> std::io::Result<()> {
use std::{fs::File, io::Write};
let yaml = export_schema_config_to_yaml(config);
let mut file = File::create(path)?;
file.write_all(yaml.as_bytes())?;
Ok(())
}
pub fn create_schema_config_from_registered(default_db: DatabaseSchema, default_database: Option<String>) -> SchemaConfig {
let schemas = get_registered_schemas();
let mut default_db = default_db;
default_db.schemas = schemas;
SchemaConfig { databases: vec![default_db], default_database }
}
pub fn generate_full_sql_for_registered_schemas() -> Vec<String> {
generate_full_sql_for_registered_schemas_for(DatabaseType::current())
}
pub fn generate_full_sql_for_registered_schemas_for(db_type: DatabaseType) -> Vec<String> {
let schemas = get_registered_schemas();
let mut all_sql = Vec::new();
for schema in schemas {
all_sql.extend(schema.to_full_create_sql_for(db_type));
}
all_sql
}
#[cfg(debug_assertions)]
pub fn export_sql_for_all_databases(output_dir: impl AsRef<Path>) -> Result<(), Box<dyn std::error::Error>> {
let output_dir = output_dir.as_ref();
fs::create_dir_all(output_dir)?;
let db_types = [DatabaseType::Turso, DatabaseType::Postgres, DatabaseType::MySql];
for &db_type in &db_types {
let sqls = generate_full_sql_for_registered_schemas_for(db_type);
let db_name = match db_type {
DatabaseType::Turso => "turso",
DatabaseType::Postgres => "postgres",
DatabaseType::MySql => "mysql",
};
let file_path = output_dir.join(format!("schema_{}.sql", db_name));
let content = sqls.join(";\n\n") + ";\n";
fs::write(&file_path, content)?;
println!("Exported SQL for {} to: {}", db_name, file_path.display());
}
Ok(())
}