refactor(data): remove MySQL and SQLite support

Use PostgreSQL as the only database backend across runtime, schema tooling, installation, Compose, and CI. Update regression tests and reject removed drivers explicitly.
This commit is contained in:
elky
2026-09-07 00:09:42 +08:00
parent b5ed802277
commit 2281f2b754
298 changed files with 793 additions and 134804 deletions
@@ -1,6 +1,6 @@
use std::path::PathBuf;
use aether_data_schema::dialect::{mysql, postgres, sqlite};
use aether_data_schema::dialect::postgres;
use aether_data_schema::{check_generated_dir, generate_loaded_to_dir, load_schema_sources};
use clap::{Parser, Subcommand, ValueEnum};
@@ -39,8 +39,6 @@ enum Command {
#[derive(Debug, Clone, Copy, ValueEnum)]
enum Driver {
Postgres,
Mysql,
Sqlite,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -66,8 +64,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let schema = load_schema_sources(schema_dir)?.schema;
let output = match driver {
Driver::Postgres => postgres::emit_schema(&schema),
Driver::Mysql => mysql::emit_schema(&schema),
Driver::Sqlite => sqlite::emit_schema(&schema),
};
print!("{output}");
}
+1 -65
View File
@@ -1,22 +1,16 @@
use crate::{Column, DefaultValue, DriverColumnOverride, LogicalType, ReferentialAction};
pub mod mysql;
pub mod postgres;
pub mod sqlite;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dialect {
Postgres,
Mysql,
Sqlite,
}
impl Dialect {
pub const fn as_str(self) -> &'static str {
match self {
Self::Postgres => "postgres",
Self::Mysql => "mysql",
Self::Sqlite => "sqlite",
}
}
}
@@ -29,25 +23,11 @@ fn default_sql(default: &DefaultValue) -> String {
match default {
DefaultValue::String(value) => quote_string(value),
DefaultValue::Integer(value) => value.to_string(),
DefaultValue::Bool(value) => {
if *value {
"1".to_string()
} else {
"0".to_string()
}
}
DefaultValue::Bool(value) => value.to_string(),
DefaultValue::Raw { raw } => raw.clone(),
}
}
fn default_sql_bool_keywords(default: &DefaultValue) -> String {
match default {
DefaultValue::Bool(true) => "true".to_string(),
DefaultValue::Bool(false) => "false".to_string(),
_ => default_sql(default),
}
}
fn column_default<'a>(
column: &'a Column,
override_: Option<&'a DriverColumnOverride>,
@@ -94,50 +74,6 @@ fn postgres_type(column: &Column) -> String {
}
}
fn mysql_type(column: &Column) -> String {
let override_ = column.driver.mysql.as_ref();
if let Some(sql_type) = override_type(override_) {
return sql_type.to_string();
}
match column.logical_type {
LogicalType::TextId | LogicalType::Text => match column.length {
Some(length) => format!("VARCHAR({length})"),
None => "TEXT".to_string(),
},
LogicalType::LongText => "LONGTEXT".to_string(),
LogicalType::Bool => "TINYINT(1)".to_string(),
LogicalType::Int32 => "INT".to_string(),
LogicalType::Int64 | LogicalType::UnixSeconds | LogicalType::UnixMillis => {
"BIGINT".to_string()
}
LogicalType::Float64 => "DOUBLE".to_string(),
LogicalType::DecimalMoney => "DECIMAL(20,8)".to_string(),
LogicalType::Timestamp => "BIGINT".to_string(),
LogicalType::Json => "JSON".to_string(),
LogicalType::Bytes => "LONGBLOB".to_string(),
}
}
fn sqlite_type(column: &Column) -> String {
let override_ = column.driver.sqlite.as_ref();
if let Some(sql_type) = override_type(override_) {
return sql_type.to_string();
}
match column.logical_type {
LogicalType::TextId | LogicalType::Text | LogicalType::LongText | LogicalType::Json => {
"TEXT".to_string()
}
LogicalType::Bool
| LogicalType::Int32
| LogicalType::Int64
| LogicalType::UnixSeconds
| LogicalType::UnixMillis
| LogicalType::Timestamp => "INTEGER".to_string(),
LogicalType::Float64 | LogicalType::DecimalMoney => "REAL".to_string(),
LogicalType::Bytes => "BLOB".to_string(),
}
}
fn referential_action_sql(action: &ReferentialAction) -> &'static str {
match action {
ReferentialAction::Cascade => "CASCADE",
@@ -1,117 +0,0 @@
use crate::dialect::{
column_default, column_nullable, default_sql, mysql_type, referential_action_sql,
};
use crate::LogicalSchema;
pub fn emit_schema(schema: &LogicalSchema) -> String {
emit_named_schema(schema, &schema.ordered_table_names())
}
pub fn emit_named_schema(schema: &LogicalSchema, table_names: &[String]) -> String {
let mut out = String::new();
for table_name in table_names {
let table = schema
.tables
.get(table_name)
.expect("named schema table should exist");
let mut definitions = Vec::new();
for column in &table.columns {
let mut definition = format!(" `{}` {}", column.name, mysql_type(column));
if !column_nullable(column, column.driver.mysql.as_ref()) {
definition.push_str(" NOT NULL");
}
if column.auto_increment {
definition.push_str(" AUTO_INCREMENT");
}
if let Some(default) = column_default(column, column.driver.mysql.as_ref()) {
definition.push_str(" DEFAULT ");
definition.push_str(&default_sql(default));
}
definitions.push(definition);
}
if !table.primary_key.is_empty() {
definitions.push(format!(
" PRIMARY KEY ({})",
table
.primary_key
.iter()
.map(|column| format!("`{column}`"))
.collect::<Vec<_>>()
.join(", ")
));
}
for unique in &table.uniques {
definitions.push(format!(
" UNIQUE KEY {} ({})",
unique.name,
unique
.columns
.iter()
.map(|column| format!("`{column}`"))
.collect::<Vec<_>>()
.join(", ")
));
}
for index in &table.indexes {
let unique = if index.unique { "UNIQUE " } else { "" };
definitions.push(format!(
" {unique}KEY {} ({})",
index.name,
index
.columns
.iter()
.map(|column| format!("`{column}`"))
.collect::<Vec<_>>()
.join(", ")
));
}
for foreign_key in &table.foreign_keys {
let mut definition = format!(
" CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({})",
foreign_key.name,
foreign_key
.columns
.iter()
.map(|column| format!("`{column}`"))
.collect::<Vec<_>>()
.join(", "),
foreign_key.references_table,
foreign_key
.references_columns
.iter()
.map(|column| format!("`{column}`"))
.collect::<Vec<_>>()
.join(", ")
);
if let Some(action) = &foreign_key.on_delete {
definition.push_str(" ON DELETE ");
definition.push_str(referential_action_sql(action));
}
definitions.push(definition);
}
out.push_str(&format!(
"CREATE TABLE IF NOT EXISTS {} (\n",
quote_identifier_if_needed(table_name)
));
out.push_str(&definitions.join(",\n"));
out.push_str("\n);\n\n");
}
out
}
fn quote_identifier_if_needed(identifier: &str) -> String {
if needs_quoting(identifier) {
quote_identifier(identifier)
} else {
identifier.to_string()
}
}
fn needs_quoting(identifier: &str) -> bool {
matches!(identifier, "date" | "usage")
}
fn quote_identifier(identifier: &str) -> String {
format!("`{}`", identifier.replace('`', "``"))
}
@@ -1,6 +1,5 @@
use crate::dialect::{
column_default, column_nullable, default_sql_bool_keywords, postgres_type,
referential_action_sql,
column_default, column_nullable, default_sql, postgres_type, referential_action_sql,
};
use crate::LogicalSchema;
@@ -30,7 +29,7 @@ pub fn emit_named_schema(schema: &LogicalSchema, table_names: &[String]) -> Stri
out.push_str(&postgres_type(column));
if let Some(default) = column_default(column, column.driver.postgres.as_ref()) {
out.push_str(" DEFAULT ");
out.push_str(&default_sql_bool_keywords(default));
out.push_str(&default_sql(default));
}
if !column_nullable(column, column.driver.postgres.as_ref()) {
out.push_str(" NOT NULL");
@@ -1,92 +0,0 @@
use crate::dialect::{
column_default, column_nullable, default_sql, referential_action_sql, sqlite_type,
};
use crate::LogicalSchema;
pub fn emit_schema(schema: &LogicalSchema) -> String {
emit_named_schema(schema, &schema.ordered_table_names())
}
pub fn emit_named_schema(schema: &LogicalSchema, table_names: &[String]) -> String {
let mut out = String::new();
for table_name in table_names {
let table = schema
.tables
.get(table_name)
.expect("named schema table should exist");
let mut definitions = Vec::new();
for column in &table.columns {
let mut definition = format!(" {} {}", column.name, sqlite_type(column));
if table.primary_key.len() == 1 && table.primary_key[0] == column.name {
definition.push_str(" PRIMARY KEY");
if column.auto_increment {
definition.push_str(" AUTOINCREMENT");
}
}
if !column.auto_increment && !column_nullable(column, column.driver.sqlite.as_ref()) {
definition.push_str(" NOT NULL");
}
if let Some(default) = column_default(column, column.driver.sqlite.as_ref()) {
definition.push_str(" DEFAULT ");
definition.push_str(&default_sql(default));
}
definitions.push(definition);
}
if table.primary_key.len() > 1 {
definitions.push(format!(
" PRIMARY KEY ({})",
table.primary_key.join(", ")
));
}
for unique in &table.uniques {
definitions.push(format!(" UNIQUE ({})", unique.columns.join(", ")));
}
for foreign_key in &table.foreign_keys {
let mut definition = format!(
" CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({})",
foreign_key.name,
foreign_key.columns.join(", "),
foreign_key.references_table,
foreign_key.references_columns.join(", ")
);
if let Some(action) = &foreign_key.on_delete {
definition.push_str(" ON DELETE ");
definition.push_str(referential_action_sql(action));
}
definitions.push(definition);
}
let quoted_table_name = quote_identifier_if_needed(table_name);
out.push_str(&format!(
"CREATE TABLE IF NOT EXISTS {quoted_table_name} (\n"
));
out.push_str(&definitions.join(",\n"));
out.push_str("\n);\n");
for index in &table.indexes {
let unique = if index.unique { "UNIQUE " } else { "" };
out.push_str(&format!(
"CREATE {unique}INDEX IF NOT EXISTS {} ON {quoted_table_name} ({});\n",
index.name,
index.columns.join(", ")
));
}
out.push('\n');
}
out
}
fn quote_identifier_if_needed(identifier: &str) -> String {
if needs_quoting(identifier) {
quote_identifier(identifier)
} else {
identifier.to_string()
}
}
fn needs_quoting(identifier: &str) -> bool {
matches!(identifier, "date" | "usage")
}
fn quote_identifier(identifier: &str) -> String {
format!("\"{}\"", identifier.replace('"', "\"\""))
}
+4 -72
View File
@@ -139,10 +139,6 @@ pub enum DefaultValue {
pub struct DriverColumnOverrides {
#[serde(default)]
pub postgres: Option<DriverColumnOverride>,
#[serde(default)]
pub mysql: Option<DriverColumnOverride>,
#[serde(default)]
pub sqlite: Option<DriverColumnOverride>,
}
#[derive(Debug, Clone, serde::Deserialize, PartialEq, Eq)]
@@ -430,12 +426,6 @@ pub fn generate_sources_to_dir(
write_driver_sources(output_root, "postgres", sources, |tables| {
dialect::postgres::emit_named_schema(schema, tables)
})?;
write_driver_sources(output_root, "mysql", sources, |tables| {
dialect::mysql::emit_named_schema(schema, tables)
})?;
write_driver_sources(output_root, "sqlite", sources, |tables| {
dialect::sqlite::emit_named_schema(schema, tables)
})?;
Ok(())
}
@@ -449,12 +439,6 @@ pub fn check_generated_dir(
check_driver_sources(output_root, "postgres", &loaded.sources, |tables| {
dialect::postgres::emit_named_schema(&loaded.schema, tables)
})?;
check_driver_sources(output_root, "mysql", &loaded.sources, |tables| {
dialect::mysql::emit_named_schema(&loaded.schema, tables)
})?;
check_driver_sources(output_root, "sqlite", &loaded.sources, |tables| {
dialect::sqlite::emit_named_schema(&loaded.schema, tables)
})?;
Ok(())
}
@@ -688,12 +672,7 @@ fn write_generated_readme(output_root: &Path) -> Result<(), SchemaError> {
}
fn assert_generated_root_files(output_root: &Path) -> Result<(), SchemaError> {
let expected = BTreeSet::from([
"README.md".to_string(),
"mysql".to_string(),
"postgres".to_string(),
"sqlite".to_string(),
]);
let expected = BTreeSet::from(["README.md".to_string(), "postgres".to_string()]);
let actual = fs::read_dir(output_root)
.map_err(|source| SchemaError::Read {
path: output_root.to_path_buf(),
@@ -774,7 +753,7 @@ fn generated_readme() -> String {
bash crates/aether-data/runtime/schema/compose_schema.sh generate\n\
```\n\n\
Runtime migrations are not loaded from this directory. The executable SQL lives under \
`crates/aether-data/adapters/{postgres,mysql,sqlite}/migrations`, and the Postgres bootstrap snapshot \
`crates/aether-data/adapters/postgres/migrations`, and the Postgres bootstrap snapshot \
is generated at build time from `crates/aether-data/runtime/schema/bootstrap/postgres` into the crate \
build output until a generated fragment is deliberately promoted into the driver-specific \
schema manifests.\n"
@@ -808,7 +787,7 @@ fn write_generated(path: PathBuf, contents: &str) -> Result<(), SchemaError> {
#[cfg(test)]
mod tests {
use super::*;
use crate::dialect::{mysql, postgres, sqlite};
use crate::dialect::postgres;
fn announcements_schema() -> LogicalSchema {
LogicalSchema {
@@ -879,42 +858,6 @@ mod tests {
assert!(postgres_sql.contains(
"CREATE INDEX IF NOT EXISTS announcements_is_active_idx ON public.announcements USING btree (is_active);"
));
let mysql_sql = mysql::emit_schema(&schema);
assert!(mysql_sql.contains("`id` VARCHAR(64) NOT NULL"));
assert!(mysql_sql.contains("`is_active` TINYINT(1) NOT NULL DEFAULT 1"));
assert!(mysql_sql.contains("KEY announcements_is_active_idx (`is_active`)"));
let sqlite_sql = sqlite::emit_schema(&schema);
assert!(sqlite_sql.contains("id TEXT PRIMARY KEY NOT NULL"));
assert!(sqlite_sql.contains("is_active INTEGER NOT NULL DEFAULT 1"));
assert!(sqlite_sql.contains(
"CREATE INDEX IF NOT EXISTS announcements_is_active_idx ON announcements (is_active);"
));
}
#[test]
fn mysql_column_type_override_can_select_binary_collation() {
let mut schema = announcements_schema();
schema
.tables
.get_mut("announcements")
.expect("fixture table exists")
.columns
.first_mut()
.expect("fixture id column exists")
.driver
.mysql = Some(DriverColumnOverride {
sql_type: Some(
"VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin".to_string(),
),
default: None,
nullable: None,
});
let mysql_sql = mysql::emit_schema(&schema);
assert!(mysql_sql
.contains("`id` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin NOT NULL"));
}
#[test]
@@ -1028,19 +971,8 @@ ALTER TABLE users ADD COLUMN ldap_dn VARCHAR(1024);
.and_then(Path::parent)
.expect("crate should live under workspace/crates/aether-data");
let schema_dir = workspace.join("crates/aether-data/runtime/schema/logical");
let mut required_sql_paths = vec![workspace
let required_sql_paths = vec![workspace
.join("crates/aether-data/adapters/postgres/migrations/20260403000000_baseline.sql")];
for driver in ["mysql", "sqlite"] {
let driver_dir =
workspace.join(format!("crates/aether-data/adapters/{driver}/migrations"));
let mut paths = std::fs::read_dir(&driver_dir)
.unwrap_or_else(|err| panic!("failed to read {}: {err}", driver_dir.display()))
.map(|entry| entry.expect("migration entry should be readable").path())
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("sql"))
.collect::<Vec<_>>();
paths.sort();
required_sql_paths.extend(paths);
}
let loaded = load_schema_sources(schema_dir).expect("workspace logical schema should load");
check_required_tables(&loaded.schema, &required_sql_paths)