use std::collections::HashMap;
use sz_orm_core::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TestDialect {
MySql,
Postgres,
Sqlite,
Oracle,
MsSql,
}
impl TestDialect {
pub fn as_str(&self) -> &'static str {
match self {
TestDialect::MySql => "mysql",
TestDialect::Postgres => "postgres",
TestDialect::Sqlite => "sqlite",
TestDialect::Oracle => "oracle",
TestDialect::MsSql => "mssql",
}
}
}
pub struct TestSchemaBuilder {
dialect: TestDialect,
}
impl TestSchemaBuilder {
pub fn new(dialect: TestDialect) -> Self {
Self { dialect }
}
pub fn build_ddl(&self) -> Vec<String> {
let id_type = match self.dialect {
TestDialect::Oracle => "NUMBER(19)".to_string(),
TestDialect::MsSql => "BIGINT".to_string(),
_ => "BIGINT".to_string(),
};
let text_type = match self.dialect {
TestDialect::MySql => "VARCHAR(255)".to_string(),
TestDialect::Postgres => "TEXT".to_string(),
TestDialect::Sqlite => "TEXT".to_string(),
TestDialect::Oracle => "VARCHAR2(255)".to_string(),
TestDialect::MsSql => "NVARCHAR(255)".to_string(),
};
let auto_id = match self.dialect {
TestDialect::MySql => "BIGINT AUTO_INCREMENT PRIMARY KEY".to_string(),
TestDialect::Postgres => "BIGSERIAL PRIMARY KEY".to_string(),
TestDialect::Sqlite => "INTEGER PRIMARY KEY AUTOINCREMENT".to_string(),
TestDialect::Oracle => {
"NUMBER(19) GENERATED ALWAYS AS IDENTITY PRIMARY KEY".to_string()
}
TestDialect::MsSql => "BIGINT IDENTITY(1,1) PRIMARY KEY".to_string(),
};
vec![
format!("CREATE TABLE IF NOT EXISTS users (id {auto_id}, name {text_type}, email {text_type})"),
format!("CREATE TABLE IF NOT EXISTS profiles (id {auto_id}, user_id {id_type}, bio {text_type})"),
format!("CREATE TABLE IF NOT EXISTS orders (id {auto_id}, user_id {id_type}, amount {id_type}, status {text_type})"),
format!("CREATE TABLE IF NOT EXISTS roles (id {auto_id}, name {text_type})"),
format!("CREATE TABLE IF NOT EXISTS user_roles (id {auto_id}, user_id {id_type}, role_id {id_type})"),
]
}
pub fn teardown_ddl(&self) -> Vec<String> {
let drop_if_exists = |table: &str| -> String {
match self.dialect {
TestDialect::Oracle => format!("DROP TABLE IF EXISTS {table}"),
_ => format!("DROP TABLE IF EXISTS {table}"),
}
};
vec![
drop_if_exists("user_roles"),
drop_if_exists("orders"),
drop_if_exists("profiles"),
drop_if_exists("roles"),
drop_if_exists("users"),
]
}
pub fn seed_data(&self) -> Vec<String> {
vec![
"INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@test.com')".to_string(),
"INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'bob@test.com')".to_string(),
"INSERT INTO users (id, name, email) VALUES (3, 'Carol', 'carol@test.com')".to_string(),
"INSERT INTO users (id, name, email) VALUES (4, 'Dave', 'dave@test.com')".to_string(),
"INSERT INTO users (id, name, email) VALUES (5, 'Eve', 'eve@test.com')".to_string(),
"INSERT INTO profiles (id, user_id, bio) VALUES (1, 1, 'Alice bio')".to_string(),
"INSERT INTO profiles (id, user_id, bio) VALUES (2, 2, 'Bob bio')".to_string(),
"INSERT INTO profiles (id, user_id, bio) VALUES (3, 4, 'Dave bio')".to_string(),
"INSERT INTO orders (id, user_id, amount, status) VALUES (1, 1, 100, 'paid')"
.to_string(),
"INSERT INTO orders (id, user_id, amount, status) VALUES (2, 1, 200, 'paid')"
.to_string(),
"INSERT INTO orders (id, user_id, amount, status) VALUES (3, 1, 300, 'pending')"
.to_string(),
"INSERT INTO orders (id, user_id, amount, status) VALUES (4, 1, 50, 'cancelled')"
.to_string(),
"INSERT INTO orders (id, user_id, amount, status) VALUES (5, 2, 150, 'paid')"
.to_string(),
"INSERT INTO orders (id, user_id, amount, status) VALUES (6, 2, 250, 'pending')"
.to_string(),
"INSERT INTO orders (id, user_id, amount, status) VALUES (7, 2, 350, 'paid')"
.to_string(),
"INSERT INTO orders (id, user_id, amount, status) VALUES (8, 4, 400, 'paid')"
.to_string(),
"INSERT INTO orders (id, user_id, amount, status) VALUES (9, 5, 500, 'pending')"
.to_string(),
"INSERT INTO orders (id, user_id, amount, status) VALUES (10, 5, 600, 'paid')"
.to_string(),
"INSERT INTO roles (id, name) VALUES (1, 'admin')".to_string(),
"INSERT INTO roles (id, name) VALUES (2, 'user')".to_string(),
"INSERT INTO roles (id, name) VALUES (3, 'guest')".to_string(),
"INSERT INTO user_roles (id, user_id, role_id) VALUES (1, 1, 1)".to_string(),
"INSERT INTO user_roles (id, user_id, role_id) VALUES (2, 1, 2)".to_string(),
"INSERT INTO user_roles (id, user_id, role_id) VALUES (3, 2, 2)".to_string(),
"INSERT INTO user_roles (id, user_id, role_id) VALUES (4, 4, 2)".to_string(),
"INSERT INTO user_roles (id, user_id, role_id) VALUES (5, 4, 3)".to_string(),
"INSERT INTO user_roles (id, user_id, role_id) VALUES (6, 5, 1)".to_string(),
]
}
pub fn expected_counts() -> TestSchemaCounts {
TestSchemaCounts {
users: 5,
profiles: 3,
orders: 10,
roles: 3,
user_roles: 6,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TestSchemaCounts {
pub users: usize,
pub profiles: usize,
pub orders: usize,
pub roles: usize,
pub user_roles: usize,
}
pub fn make_row(pairs: &[(&str, Value)]) -> HashMap<String, Value> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_ddl_all_dialects() {
for dialect in [
TestDialect::MySql,
TestDialect::Postgres,
TestDialect::Sqlite,
TestDialect::Oracle,
TestDialect::MsSql,
] {
let builder = TestSchemaBuilder::new(dialect);
let ddl = builder.build_ddl();
assert_eq!(ddl.len(), 5, "{} 应生成 5 表 DDL", dialect.as_str());
assert!(ddl[0].contains("users"), "第一张表应为 users");
assert!(ddl[4].contains("user_roles"), "最后一张表应为 user_roles");
}
}
#[test]
fn test_teardown_ddl_reverse_order() {
let builder = TestSchemaBuilder::new(TestDialect::MySql);
let teardown = builder.teardown_ddl();
assert_eq!(teardown.len(), 5);
assert!(teardown[0].contains("user_roles"), "先删引用表 user_roles");
assert!(teardown[4].contains("users"), "最后删被引用表 users");
}
#[test]
fn test_seed_data_covers_boundary() {
let builder = TestSchemaBuilder::new(TestDialect::MySql);
let seed = builder.seed_data();
let counts = TestSchemaBuilder::expected_counts();
let user_count = seed
.iter()
.filter(|s| s.contains("INSERT INTO users"))
.count();
let order_count = seed
.iter()
.filter(|s| s.contains("INSERT INTO orders"))
.count();
let profile_count = seed
.iter()
.filter(|s| s.contains("INSERT INTO profiles"))
.count();
let role_count = seed
.iter()
.filter(|s| s.contains("INSERT INTO roles"))
.count();
let user_role_count = seed
.iter()
.filter(|s| s.contains("INSERT INTO user_roles"))
.count();
assert_eq!(user_count, counts.users);
assert_eq!(order_count, counts.orders);
assert_eq!(profile_count, counts.profiles);
assert_eq!(role_count, counts.roles);
assert_eq!(user_role_count, counts.user_roles);
}
#[test]
fn test_seed_data_user3_no_relations() {
let builder = TestSchemaBuilder::new(TestDialect::MySql);
let seed = builder.seed_data();
let user3_orders = seed
.iter()
.any(|s| s.contains("INSERT INTO orders") && s.contains("3, 3,"));
let user3_profiles = seed
.iter()
.any(|s| s.contains("INSERT INTO profiles") && s.contains("3, 3,"));
let user3_roles = seed
.iter()
.any(|s| s.contains("INSERT INTO user_roles") && s.contains("3, 3,"));
assert!(!user3_orders, "user3 不应有 orders");
assert!(!user3_profiles, "user3 不应有 profiles");
assert!(!user3_roles, "user3 不应有 roles");
}
#[test]
fn test_dialect_as_str() {
assert_eq!(TestDialect::MySql.as_str(), "mysql");
assert_eq!(TestDialect::Postgres.as_str(), "postgres");
assert_eq!(TestDialect::Sqlite.as_str(), "sqlite");
assert_eq!(TestDialect::Oracle.as_str(), "oracle");
assert_eq!(TestDialect::MsSql.as_str(), "mssql");
}
#[test]
fn test_make_row() {
let row = make_row(&[
("id", Value::I64(1)),
("name", Value::String("a".to_string())),
]);
assert_eq!(row.len(), 2);
assert_eq!(row.get("id"), Some(&Value::I64(1)));
}
}