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
+2 -1
View File
@@ -81,7 +81,7 @@ sha1 = "0.10"
sha2 = { workspace = true, features = ["oid"] }
socket2.workspace = true
tar.workspace = true
sqlx = { workspace = true, features = ["postgres", "mysql", "sqlite", "migrate"] }
sqlx = { workspace = true, features = ["postgres", "migrate"] }
sysinfo = "0.32"
thiserror.workspace = true
tokio.workspace = true
@@ -102,6 +102,7 @@ tikv-jemallocator = { version = "0.6", optional = true }
tikv-jemalloc-sys = { version = "0.6", optional = true }
[dev-dependencies]
aether-testkit = { workspace = true, features = ["postgres"] }
aether-test-support.workspace = true
aws-lc-rs.workspace = true
tracing-subscriber.workspace = true
@@ -1499,7 +1499,6 @@ fn get_cached_auth_context_with_age(
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -1595,40 +1594,24 @@ mod tests {
.expect("endpoint should build")
}
struct SqliteAuthConfigNodes {
struct PostgresAuthConfigNodes {
first: AppState,
second: AppState,
_database: TemporarySqliteDatabase,
_database: aether_testkit::ManagedPostgresServer,
}
struct TemporarySqliteDatabase(PathBuf);
impl Drop for TemporarySqliteDatabase {
fn drop(&mut self) {
for suffix in ["", "-shm", "-wal"] {
let mut path = self.0.as_os_str().to_os_string();
path.push(suffix);
let _ = std::fs::remove_file(PathBuf::from(path));
}
}
}
async fn sqlite_auth_config_nodes(
async fn postgres_auth_config_nodes(
auth_repository: Arc<InMemoryAuthApiKeySnapshotRepository>,
) -> SqliteAuthConfigNodes {
let database_path = std::env::temp_dir().join(format!(
"aether-auth-config-{}.sqlite",
uuid::Uuid::new_v4()
));
) -> PostgresAuthConfigNodes {
let server = aether_testkit::ManagedPostgresServer::start()
.await
.expect("temporary PostgreSQL should start");
let mut pool = SqlPoolConfig::default();
pool.min_connections = 0;
pool.max_connections = 4;
let database = SqlDatabaseConfig::new(
DatabaseDriver::Sqlite,
format!("sqlite://{}", database_path.display()),
pool,
)
.expect("sqlite config should build");
let database =
SqlDatabaseConfig::new(DatabaseDriver::Postgres, server.database_url(), pool)
.expect("postgres config should build");
let config = GatewayDataConfig::from_database_config(database);
let first_data = GatewayDataState::from_config(config.clone())
.expect("first data state should build")
@@ -1637,27 +1620,27 @@ mod tests {
assert!(first_data
.run_database_migrations()
.await
.expect("sqlite migrations should run"));
.expect("postgres migrations should run"));
let second_data = GatewayDataState::from_config(config)
.expect("second data state should build")
.with_auth_api_key_reader(auth_repository)
.without_wallet_reader_for_tests();
SqliteAuthConfigNodes {
PostgresAuthConfigNodes {
first: AppState::new()
.expect("first app state should build")
.with_data_state_for_tests(first_data),
second: AppState::new()
.expect("second app state should build")
.with_data_state_for_tests(second_data),
_database: TemporarySqliteDatabase(database_path),
_database: server,
}
}
#[tokio::test]
async fn strong_system_config_read_bypasses_app_and_data_caches() {
let nodes =
sqlite_auth_config_nodes(Arc::new(InMemoryAuthApiKeySnapshotRepository::seed([])))
postgres_auth_config_nodes(Arc::new(InMemoryAuthApiKeySnapshotRepository::seed([])))
.await;
let key = format!("test.auth.strong-read.{}", uuid::Uuid::new_v4());
let old_value = serde_json::json!({"version": "old"});
@@ -2627,7 +2610,7 @@ mod tests {
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
None, snapshot,
)]));
let nodes = sqlite_auth_config_nodes(auth_repository.clone()).await;
let nodes = postgres_auth_config_nodes(auth_repository.clone()).await;
nodes
.first
.auth_context_cache
+10 -42
View File
@@ -113,10 +113,7 @@ impl GatewayDataConfig {
return (self.clone(), None);
};
let total_max = database.pool.max_connections;
if total_max < 2
|| configured_background_max == Some(0)
|| is_private_sqlite_memory_database(database)
{
if total_max < 2 || configured_background_max == Some(0) {
return (self.clone(), None);
}
@@ -152,14 +149,9 @@ impl GatewayDataConfig {
}
}
fn is_private_sqlite_memory_database(database: &aether_data::SqlDatabaseConfig) -> bool {
database.driver == aether_data::DatabaseDriver::Sqlite
&& matches!(database.url.trim(), "sqlite::memory:" | "sqlite://:memory:")
}
#[cfg(test)]
mod tests {
use super::GatewayDataConfig;
use super::{GatewayDataConfig, PostgresPoolConfig};
use aether_data::{DatabaseDriver, SqlDatabaseConfig, SqlPoolConfig};
#[test]
@@ -226,7 +218,10 @@ mod tests {
#[test]
fn runtime_pool_split_can_be_disabled_or_degrade_for_single_connection() {
let mut database = SqlDatabaseConfig::sqlite_default();
let mut database = SqlDatabaseConfig::from_postgres_config(PostgresPoolConfig {
database_url: "postgres://localhost/aether".to_string(),
..Default::default()
});
database.pool.max_connections = 1;
let config = GatewayDataConfig::from_database_config(database);
assert!(config
@@ -234,7 +229,10 @@ mod tests {
.1
.is_none());
let mut database = SqlDatabaseConfig::sqlite_default();
let mut database = SqlDatabaseConfig::from_postgres_config(PostgresPoolConfig {
database_url: "postgres://localhost/aether".to_string(),
..Default::default()
});
database.pool.max_connections = 8;
let config = GatewayDataConfig::from_database_config(database);
assert!(config
@@ -242,34 +240,4 @@ mod tests {
.1
.is_none());
}
#[test]
fn runtime_pool_split_keeps_private_sqlite_memory_database_in_one_pool() {
for url in ["sqlite::memory:", "sqlite://:memory:"] {
let config = GatewayDataConfig::from_database_config(
SqlDatabaseConfig::new(
DatabaseDriver::Sqlite,
url,
SqlPoolConfig {
min_connections: 1,
max_connections: 8,
..SqlPoolConfig::default()
},
)
.expect("sqlite memory database config should be valid"),
);
let (foreground, background) = config.split_runtime_pools_with_background_max(Some(2));
assert!(background.is_none(), "private SQLite URL {url} was split");
assert_eq!(
foreground
.database()
.expect("foreground database")
.pool
.max_connections,
8
);
}
}
}
+1 -10
View File
@@ -1116,10 +1116,7 @@ impl GatewayDataState {
}
fn database_driver_supports_usage_counter_flush(driver: Option<DatabaseDriver>) -> bool {
matches!(
driver,
Some(DatabaseDriver::Postgres | DatabaseDriver::Mysql | DatabaseDriver::Sqlite)
)
matches!(driver, Some(DatabaseDriver::Postgres))
}
#[cfg(test)]
@@ -1132,12 +1129,6 @@ mod usage_counter_flush_backend_tests {
assert!(database_driver_supports_usage_counter_flush(Some(
DatabaseDriver::Postgres
)));
assert!(database_driver_supports_usage_counter_flush(Some(
DatabaseDriver::Mysql
)));
assert!(database_driver_supports_usage_counter_flush(Some(
DatabaseDriver::Sqlite
)));
assert!(!database_driver_supports_usage_counter_flush(None));
}
}
-34
View File
@@ -254,40 +254,6 @@ async fn app_state_wires_gateway_data_state_from_config() {
assert!(state.data.has_video_task_reader());
}
#[tokio::test]
async fn app_state_prepares_sqlite_database_startup() -> Result<(), Box<dyn std::error::Error>> {
let mut pool = SqlPoolConfig::default();
pool.min_connections = 0;
pool.max_connections = 1;
let database = SqlDatabaseConfig::new(DatabaseDriver::Sqlite, "sqlite::memory:", pool)?;
let state =
AppState::new()?.with_data_config(GatewayDataConfig::from_database_config(database))?;
let pending = state
.prepare_database_for_startup()
.await?
.expect("sqlite database should expose migration state");
assert!(
!pending.is_empty(),
"fresh sqlite gateway databases should report pending migrations"
);
assert!(
state.run_database_migrations().await?,
"sqlite gateway database should run migrations"
);
let pending = state
.prepare_database_for_startup()
.await?
.expect("sqlite database should expose migration state");
assert!(
pending.is_empty(),
"sqlite gateway databases should be current after migrations"
);
Ok(())
}
#[tokio::test]
async fn data_state_checks_user_uniqueness_through_user_reader() {
let user = StoredUserAuthRecord::new(
@@ -8631,7 +8631,7 @@ enum WalletOwner<'a> {
mod tests {
use std::sync::Arc;
use aether_data::repository::pool_scores::SqlitePoolMemberScoreRepository;
use aether_data::repository::pool_scores::PostgresPoolMemberScoreRepository;
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogKey, StoredProviderCatalogProvider,
@@ -9349,12 +9349,11 @@ mod tests {
.await
.expect("a disabled score writer remains an allowed no-op");
let pool = sqlx::sqlite::SqlitePoolOptions::new()
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.expect("sqlite pool should connect");
let score_repository = Arc::new(SqlitePoolMemberScoreRepository::new(pool.clone()));
.connect_lazy("postgres://localhost/aether")
.expect("postgres pool should build");
let score_repository = Arc::new(PostgresPoolMemberScoreRepository::new(pool.clone()));
pool.close().await;
let app = AppState::new()
.expect("app state should build")
@@ -32,7 +32,7 @@ use super::{
const USERS_ME_USAGE_DATA_UNAVAILABLE_DETAIL: &str = "用户用量数据暂不可用";
// The active-usage endpoint accepts an explicit list of request IDs. Keep
// this list bounded before it reaches the repository layer: SQLite/MySQL
// this list bounded before it reaches the repository layer: the database
// expand every value into a bind parameter, while PostgreSQL still has to
// materialize the complete array. Request IDs are normally UUIDs, but a
// generous per-item bound preserves compatibility with provider-generated
+23 -347
View File
@@ -120,7 +120,7 @@ use aether_data::lifecycle::export::{
copy_database_records, export_database_jsonl, import_database_jsonl, DataCopyOptions,
ExportDomain, MAX_JSONL_INPUT_BYTES,
};
use aether_data::{DatabaseDriver, SqlDatabaseConfig, SqlPoolConfig, DEFAULT_SQLITE_DATABASE_URL};
use aether_data::{DatabaseDriver, SqlDatabaseConfig, SqlPoolConfig};
use aether_gateway::{
attach_static_frontend, build_router_with_state,
prewarm_direct_h2c_sender_cache_from_env_for_startup, set_gateway_frontdoor_app_port, AppState,
@@ -192,8 +192,6 @@ impl DeploymentTopologyArg {
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
enum DatabaseDriverArg {
Sqlite,
Mysql,
Postgres,
}
@@ -270,8 +268,6 @@ impl From<ExportDomainArg> for ExportDomain {
impl From<DatabaseDriverArg> for DatabaseDriver {
fn from(value: DatabaseDriverArg) -> Self {
match value {
DatabaseDriverArg::Sqlite => DatabaseDriver::Sqlite,
DatabaseDriverArg::Mysql => DatabaseDriver::Mysql,
DatabaseDriverArg::Postgres => DatabaseDriver::Postgres,
}
}
@@ -398,7 +394,6 @@ const DEFAULT_SQL_POOL_ACQUIRE_TIMEOUT_MS: u64 = 10_000;
const DEFAULT_SQL_POOL_IDLE_TIMEOUT_MS: u64 = 30_000;
const DEFAULT_SQL_POOL_MAX_LIFETIME_MS: u64 = 30 * 60_000;
const DEFAULT_SQL_POOL_STATEMENT_CACHE_CAPACITY: usize = 100;
const DEFAULT_SQLITE_POOL_MAX_CONNECTIONS: u32 = 1;
// Per-process default for server SQL backends. Keep this below common
// database server max_connections defaults; operators can override with
// AETHER_GATEWAY_DATA_POSTGRES_{MIN,MAX}_CONNECTIONS after sizing the DB.
@@ -552,9 +547,6 @@ fn usage_queue_worker_database_cap(
let Some(database) = database else {
return MAX_USAGE_QUEUE_WORKERS;
};
if database.driver == DatabaseDriver::Sqlite {
return 1;
}
let max_connections = database.pool.max_connections.max(1) as usize;
// An isolated pool is already a dedicated background budget. Applying the shared-pool
@@ -583,9 +575,6 @@ fn usage_worker_record_concurrency_database_cap(
database_is_isolated: bool,
) -> Option<usize> {
let database = database?;
if database.driver == DatabaseDriver::Sqlite {
return Some(1);
}
let max_connections = database.pool.max_connections.max(1) as usize;
// The isolated background pool has already been carved out of the foreground pool. Keep one
@@ -662,8 +651,7 @@ fn automatic_sql_pool_config_for_parallelism(
parallelism: u32,
) -> SqlPoolConfig {
let (min_connections, max_connections) = match driver {
DatabaseDriver::Sqlite => (1, DEFAULT_SQLITE_POOL_MAX_CONNECTIONS),
DatabaseDriver::Mysql | DatabaseDriver::Postgres => {
DatabaseDriver::Postgres => {
let cpu_count = parallelism.max(1);
let max_connections = cpu_count
.saturating_mul(AUTO_SERVER_SQL_POOL_CONNECTIONS_PER_CPU)
@@ -854,7 +842,7 @@ impl GatewayDataArgs {
statement_cache_capacity: self
.postgres_statement_cache_capacity
.unwrap_or(auto.statement_cache_capacity),
require_ssl: driver != DatabaseDriver::Sqlite && self.postgres_require_ssl,
require_ssl: self.postgres_require_ssl,
}
}
@@ -931,10 +919,6 @@ fn resolve_database_url(
}
match driver {
Some(DatabaseDriver::Sqlite) => {
generic_database_url.or_else(|| Some(DEFAULT_SQLITE_DATABASE_URL.to_string()))
}
Some(DatabaseDriver::Mysql) => generic_database_url,
Some(DatabaseDriver::Postgres) | None => legacy_postgres_url.or(generic_database_url),
}
}
@@ -1378,7 +1362,7 @@ struct DataCopyArgs {
source_url: String,
/// Permit a cleartext source connection for a non-loopback database.
/// Leave unset to require TLS for remote MySQL/Postgres URLs.
/// Leave unset to require TLS for remote Postgres URLs.
#[arg(long)]
source_allow_insecure: bool,
@@ -1389,7 +1373,7 @@ struct DataCopyArgs {
target_url: String,
/// Permit a cleartext target connection for a non-loopback database.
/// Leave unset to require TLS for remote MySQL/Postgres URLs.
/// Leave unset to require TLS for remote Postgres URLs.
#[arg(long)]
target_allow_insecure: bool,
@@ -1654,7 +1638,7 @@ impl Args {
fn effective_runtime_backend(
&self,
database: Option<&SqlDatabaseConfig>,
_database: Option<&SqlDatabaseConfig>,
data_redis_url: Option<&str>,
) -> RuntimeBackendArg {
if let Some(runtime_backend) = self.runtime_backend {
@@ -1665,9 +1649,6 @@ impl Args {
if matches!(self.deployment_topology, DeploymentTopologyArg::MultiNode) {
return RuntimeBackendArg::Redis;
}
if database.is_some_and(|database| database.driver == DatabaseDriver::Sqlite) {
return RuntimeBackendArg::Memory;
}
if data_redis_url.is_some() {
RuntimeBackendArg::Redis
} else {
@@ -2039,13 +2020,6 @@ fn validate_deployment_topology(
));
}
if database.is_some_and(|database| database.driver == DatabaseDriver::Sqlite) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"AETHER_DATABASE_DRIVER=sqlite is only valid for single-node deployment",
));
}
if args
.video_task_store_path
.as_deref()
@@ -3135,7 +3109,7 @@ fn copy_database_url_is_literal_loopback(
) -> Result<bool, io::Error> {
// Parse with the same SQLx driver that will open the pool. This preserves
// query-parameter overrides such as PostgreSQL `host`/`hostaddr` and
// MySQL/PostgreSQL Unix `socket` paths, which URL authority inspection
// PostgreSQL Unix `socket` paths, which URL authority inspection
// alone would miss.
match driver {
DatabaseDriver::Postgres => {
@@ -3150,19 +3124,6 @@ fn copy_database_url_is_literal_loopback(
Ok(options.get_socket().is_some()
|| copy_database_host_is_literal_loopback(options.get_host()))
}
DatabaseDriver::Mysql => {
let options = url
.parse::<sqlx::mysql::MySqlConnectOptions>()
.map_err(|error| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("{label} database URL is invalid: {error}"),
)
})?;
Ok(options.get_socket().is_some()
|| copy_database_host_is_literal_loopback(options.get_host()))
}
DatabaseDriver::Sqlite => Ok(false),
}
}
@@ -3185,12 +3146,8 @@ fn copy_database_config(
// while every named/remote SQL host defaults to an encrypted connection.
// `allow_insecure` is deliberately endpoint-specific so a local source
// does not silently downgrade a remote target (or vice versa).
let literal_loopback = if driver == DatabaseDriver::Sqlite {
false
} else {
copy_database_url_is_literal_loopback(driver, url, label)?
};
let require_ssl = driver != DatabaseDriver::Sqlite && !allow_insecure && !literal_loopback;
let literal_loopback = copy_database_url_is_literal_loopback(driver, url, label)?;
let require_ssl = !allow_insecure && !literal_loopback;
Ok(SqlDatabaseConfig::new(
driver,
url,
@@ -3591,8 +3548,6 @@ mod tests {
fn test_database(driver: DatabaseDriver, max_connections: u32) -> SqlDatabaseConfig {
let url = match driver {
DatabaseDriver::Sqlite => "sqlite://./data/aether.db",
DatabaseDriver::Mysql => "mysql://root:root@localhost/aether",
DatabaseDriver::Postgres => "postgres://postgres:postgres@localhost/aether",
};
let max_connections = max_connections.max(1);
@@ -3608,19 +3563,6 @@ mod tests {
.expect("test database config should build")
}
fn temporary_sqlite_args(label: &str) -> (Args, std::path::PathBuf) {
let mut args = test_args();
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("clock should be available")
.as_nanos();
let database_path =
std::env::temp_dir().join(format!("aether-{label}-{}-{nonce}.db", std::process::id()));
args.data.database_driver = Some(DatabaseDriverArg::Sqlite);
args.data.database_url = Some(format!("sqlite://{}", database_path.display()));
(args, database_path)
}
#[test]
fn resolves_healthcheck_url_from_app_port() {
assert_eq!(
@@ -3851,16 +3793,16 @@ mod tests {
let before = Args::try_parse_from([
"aether-gateway",
"--database-driver",
"sqlite",
"postgres",
"--database-url",
"sqlite:///tmp/before.db",
"postgres://localhost/before",
"db",
"status",
])
.expect("database arguments before db should parse");
assert_eq!(
before.data.database_url.as_deref(),
Some("sqlite:///tmp/before.db")
Some("postgres://localhost/before")
);
let after = Args::try_parse_from([
@@ -3868,57 +3810,17 @@ mod tests {
"db",
"prepare",
"--database-driver",
"sqlite",
"postgres",
"--database-url",
"sqlite:///tmp/after.db",
"postgres://localhost/after",
])
.expect("database arguments after db prepare should parse");
assert_eq!(
after.data.database_url.as_deref(),
Some("sqlite:///tmp/after.db")
Some("postgres://localhost/after")
);
}
#[test]
fn gateway_data_pool_auto_sizes_sqlite_to_single_connection() {
let mut args = test_args();
args.data.database_driver = Some(DatabaseDriverArg::Sqlite);
args.data.database_url = Some("sqlite://./data/aether.db".to_string());
let database = args
.data
.effective_sql_database_config()
.expect("sqlite database config should build");
assert_eq!(database.driver, DatabaseDriver::Sqlite);
assert_eq!(database.pool.min_connections, 1);
assert_eq!(database.pool.max_connections, 1);
}
#[test]
fn explicit_mysql_driver_accepts_generic_database_url() {
let url = super::resolve_database_url(
Some(DatabaseDriver::Mysql),
None,
Some("postgres://legacy/aether".to_string()),
Some("mysql://root:root@localhost/aether".to_string()),
);
assert_eq!(url.as_deref(), Some("mysql://root:root@localhost/aether"));
}
#[test]
fn explicit_sqlite_driver_accepts_generic_database_url() {
let url = super::resolve_database_url(
Some(DatabaseDriver::Sqlite),
None,
Some("postgres://legacy/aether".to_string()),
Some("sqlite:///opt/aether/data/aether.db".to_string()),
);
assert_eq!(url.as_deref(), Some("sqlite:///opt/aether/data/aether.db"));
}
#[test]
fn postgres_legacy_url_keeps_precedence_over_generic_database_url() {
let url = super::resolve_database_url(
@@ -4262,27 +4164,11 @@ mod tests {
assert_eq!(workers, 10);
}
#[test]
fn gateway_usage_queue_workers_auto_uses_single_worker_for_sqlite() {
let database = test_database(DatabaseDriver::Sqlite, 1);
let workers = automatic_usage_queue_workers_for_parallelism(
16,
NodeRoleArg::All,
Some(5_000),
None,
Some(&database),
false,
);
assert_eq!(workers, 1);
}
#[test]
fn gateway_data_pool_explicit_values_override_auto_sizing() {
let mut args = test_args();
args.data.database_driver = Some(DatabaseDriverArg::Sqlite);
args.data.database_url = Some("sqlite://./data/aether.db".to_string());
args.data.database_driver = Some(DatabaseDriverArg::Postgres);
args.data.database_url = Some("postgres://localhost/aether".to_string());
args.data.postgres_min_connections = Some(2);
args.data.postgres_max_connections = Some(8);
args.data.postgres_acquire_timeout_ms = Some(2_000);
@@ -4290,7 +4176,7 @@ mod tests {
let database = args
.data
.effective_sql_database_config()
.expect("sqlite database config should build");
.expect("postgres database config should build");
assert_eq!(database.pool.min_connections, 2);
assert_eq!(database.pool.max_connections, 8);
@@ -4329,27 +4215,11 @@ mod tests {
assert_eq!(database.pool.max_connections, 128);
}
#[test]
fn sqlite_database_defaults_to_memory_runtime_backend() {
let args = test_args();
let database = SqlDatabaseConfig::new(
DatabaseDriver::Sqlite,
"sqlite://./data/aether.db".to_string(),
SqlPoolConfig::default(),
)
.expect("sqlite config should build");
assert_eq!(
args.effective_runtime_backend(Some(&database), Some("redis://127.0.0.1/0")),
RuntimeBackendArg::Memory
);
}
#[test]
fn memory_runtime_data_config_keeps_redis_out_of_data_layer() {
let mut args = test_args();
args.data.database_driver = Some(DatabaseDriverArg::Sqlite);
args.data.database_url = Some("sqlite://./data/aether.db".to_string());
args.data.database_driver = Some(DatabaseDriverArg::Postgres);
args.data.database_url = Some("postgres://localhost/aether".to_string());
args.data.redis_url = Some("redis://127.0.0.1/0".to_string());
let config = args.data.to_config();
@@ -4359,7 +4229,7 @@ mod tests {
.database()
.expect("database should be configured")
.driver,
DatabaseDriver::Sqlite
DatabaseDriver::Postgres
);
}
@@ -4394,15 +4264,6 @@ mod tests {
.expect("remote postgres config should build");
assert!(remote_postgres.pool.require_ssl);
let remote_mysql = copy_database_config(
DatabaseDriverArg::Mysql,
"mysql://user:[email protected]:3306/aether",
"target",
false,
)
.expect("remote mysql config should build");
assert!(remote_mysql.pool.require_ssl);
for url in [
"postgres://user:pass@localhost/aether",
"postgres://user:[email protected]/aether",
@@ -4425,15 +4286,6 @@ mod tests {
)
.expect("explicit insecure opt-out should build");
assert!(!explicitly_insecure.pool.require_ssl);
let sqlite = copy_database_config(
DatabaseDriverArg::Sqlite,
"sqlite://./data/aether.db",
"target",
false,
)
.expect("sqlite config should build");
assert!(!sqlite.pool.require_ssl);
}
#[test]
@@ -4498,9 +4350,9 @@ mod tests {
"postgres://user:[email protected]/aether",
"--source-allow-insecure",
"--target-driver",
"mysql",
"postgres",
"--target-url",
"mysql://user:[email protected]/aether",
"postgres://user:[email protected]/aether",
])
.expect("copy command should parse endpoint-specific TLS flags");
@@ -4677,82 +4529,6 @@ mod tests {
);
}
#[test]
fn mysql_database_with_redis_defaults_to_redis_runtime_backend() {
let args = test_args();
let database = SqlDatabaseConfig::new(
DatabaseDriver::Mysql,
"mysql://aether:aether@localhost:3306/aether".to_string(),
SqlPoolConfig::default(),
)
.expect("mysql config should build");
assert_eq!(
args.effective_runtime_backend(Some(&database), Some("redis://127.0.0.1/0")),
RuntimeBackendArg::Redis
);
}
#[test]
fn sqlite_database_allows_explicit_redis_runtime_backend_when_redis_is_configured() {
let mut args = test_args();
args.runtime_backend = Some(RuntimeBackendArg::Redis);
let database = SqlDatabaseConfig::new(
DatabaseDriver::Sqlite,
"sqlite://./data/aether.db".to_string(),
SqlPoolConfig::default(),
)
.expect("sqlite config should build");
assert_eq!(
args.effective_runtime_backend(Some(&database), Some("redis://127.0.0.1/0")),
RuntimeBackendArg::Redis
);
super::validate_deployment_topology(
&args,
Some(&database),
Some("redis://127.0.0.1/0"),
RuntimeBackendArg::Redis,
)
.expect("single-node sqlite should allow explicit redis runtime");
}
#[test]
fn single_node_sqlite_without_redis_allows_memory_runtime_backend() {
let args = test_args();
let database = SqlDatabaseConfig::new(
DatabaseDriver::Sqlite,
"sqlite://./data/aether.db".to_string(),
SqlPoolConfig::default(),
)
.expect("sqlite config should build");
super::validate_deployment_topology(
&args,
Some(&database),
None,
RuntimeBackendArg::Memory,
)
.expect("single-node sqlite memory runtime should be accepted");
}
#[test]
fn multi_node_accepts_mysql_database_backend() {
let mut args = test_args();
args.deployment_topology = DeploymentTopologyArg::MultiNode;
args.node_role = NodeRoleArg::Frontdoor;
args.video_task_store_path = None;
let database = test_database(DatabaseDriver::Mysql, 8);
super::validate_deployment_topology(
&args,
Some(&database),
Some("redis://127.0.0.1/0"),
RuntimeBackendArg::Redis,
)
.expect("multi-node mysql with shared redis should be accepted");
}
#[test]
fn multi_node_rejects_memory_runtime_backend() {
let mut args = test_args();
@@ -4799,29 +4575,6 @@ mod tests {
assert!(error.to_string().contains("REDIS_URL"));
}
#[test]
fn multi_node_rejects_sqlite_database_backend() {
let mut args = test_args();
args.deployment_topology = DeploymentTopologyArg::MultiNode;
args.node_role = NodeRoleArg::Frontdoor;
let database = SqlDatabaseConfig::new(
DatabaseDriver::Sqlite,
"sqlite://./data/aether.db".to_string(),
SqlPoolConfig::default(),
)
.expect("sqlite config should build");
let error = super::validate_deployment_topology(
&args,
Some(&database),
Some("redis://127.0.0.1/0"),
RuntimeBackendArg::Redis,
)
.expect_err("multi-node sqlite should be rejected");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("AETHER_DATABASE_DRIVER=sqlite"));
}
#[test]
fn pending_schema_error_mentions_database_prepare_command() {
let error = pending_schema_error(2, 20260413020000, "squash usage schema split");
@@ -4871,63 +4624,6 @@ mod tests {
.expect("disabled data backend should not block startup");
}
#[tokio::test]
async fn verify_only_does_not_prepare_fresh_sqlite_database() {
let (args, database_path) = temporary_sqlite_args("verify-only");
let state = AppState::new()
.expect("state should build")
.with_data_config(args.data.to_config())
.expect("sqlite state should build");
let pending_before = state
.pending_database_migrations()
.await
.expect("pending migrations should load")
.expect("sqlite should expose migration state");
assert!(!pending_before.is_empty());
let error =
super::prepare_database_startup_requirements(&state, DatabaseModeArg::VerifyOnly)
.await
.expect_err("verify-only should reject a fresh database");
assert!(error.to_string().contains("aether-gateway db prepare"));
let pending_after = state
.pending_database_migrations()
.await
.expect("pending migrations should reload")
.expect("sqlite should expose migration state");
assert_eq!(pending_after, pending_before);
drop(state);
let _ = std::fs::remove_file(database_path);
}
#[tokio::test]
async fn auto_mode_prepares_fresh_sqlite_database() {
let (args, database_path) = temporary_sqlite_args("auto-prepare");
let state = AppState::new()
.expect("state should build")
.with_data_config(args.data.to_config())
.expect("sqlite state should build");
super::prepare_database_startup_requirements(&state, DatabaseModeArg::Auto)
.await
.expect("auto mode should prepare a fresh database");
assert!(state
.pending_database_migrations()
.await
.expect("pending migrations should load")
.expect("sqlite should expose migration state")
.is_empty());
assert!(state
.pending_database_backfills()
.await
.expect("pending backfills should load")
.expect("sqlite should expose backfill state")
.is_empty());
drop(state);
let _ = std::fs::remove_file(database_path);
}
#[tokio::test]
async fn database_prepare_requires_database_url() {
let data = test_args().data;
@@ -4974,26 +4670,6 @@ mod tests {
assert!(message.contains("--apply-backfills"));
}
#[tokio::test]
async fn explicit_backfills_are_noop_for_sqlite_database() {
let mut args = test_args();
let database_path = std::env::temp_dir().join(format!(
"aether-sqlite-backfill-noop-{}-{}.db",
std::process::id(),
crate::current_unix_secs().expect("clock should be available")
));
args.data.database_driver = Some(DatabaseDriverArg::Sqlite);
args.data.database_url = Some(format!("sqlite://{}", database_path.display()));
super::run_explicit_migrations(&args)
.await
.expect("sqlite migrations should run before backfills");
super::run_explicit_backfills(&args)
.await
.expect("sqlite backfills should be an explicit no-op");
let _ = std::fs::remove_file(database_path);
}
#[tokio::test]
async fn first_request_gate_closes_a_connection_that_never_reaches_service() {
let gate = super::GatewayFirstRequestGate::new();
@@ -137,11 +137,7 @@ fn postgres_provider_cleanup_preserves_usage_history() {
#[test]
fn provider_cleanup_keeps_common_backends_in_sync() {
for path in [
"crates/aether-data/adapters/postgres/src/provider_catalog.rs",
"crates/aether-data/adapters/mysql/src/provider_catalog.rs",
"crates/aether-data/adapters/sqlite/src/provider_catalog.rs",
] {
for path in ["crates/aether-data/adapters/postgres/src/provider_catalog.rs"] {
let source = read_workspace_file(path);
for required in [
"UPDATE user_preferences SET default_provider_id = NULL WHERE default_provider_id =",
@@ -36,7 +36,7 @@ pub(super) fn assert_no_sqlx_queries(root_relative_path: &str) {
let source = fs::read_to_string(&path).expect("source file should be readable");
let hits = patterns
.iter()
.filter(|pattern| source.contains(**pattern))
.filter(|pattern| source_contains_sql_pattern(&source, pattern))
.copied()
.collect::<Vec<_>>();
if hits.is_empty() {
@@ -54,6 +54,32 @@ pub(super) fn assert_no_sqlx_queries(root_relative_path: &str) {
);
}
fn source_contains_sql_pattern(source: &str, pattern: &str) -> bool {
if pattern == "PostgresPool" {
source
.split(|character: char| !character.is_ascii_alphanumeric() && character != '_')
.any(|identifier| identifier == pattern)
} else {
source.contains(pattern)
}
}
#[test]
fn sql_pool_scan_distinguishes_pool_types_from_repository_names() {
assert!(source_contains_sql_pattern(
"pool: PostgresPool",
"PostgresPool"
));
assert!(source_contains_sql_pattern(
"PostgresPool::connect(url)",
"PostgresPool"
));
assert!(!source_contains_sql_pattern(
"PostgresPoolMemberScoreRepository::new(pool)",
"PostgresPool"
));
}
pub(super) fn assert_no_sensitive_log_patterns(root_relative_path: &str, patterns: &[&str]) {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join(root_relative_path);
let mut files = Vec::new();
File diff suppressed because it is too large Load Diff
@@ -40,58 +40,18 @@ fn pure_policy_crates_do_not_depend_on_runtime_adapters() {
#[test]
fn database_adapters_are_independent_driver_boundaries() {
let adapters = [
(
"crates/aether-data/adapters/postgres/Cargo.toml",
"features = [\"postgres\"",
["features = [\"mysql\"", "features = [\"sqlite\""],
),
(
"crates/aether-data/adapters/mysql/Cargo.toml",
"features = [\"mysql\"",
["features = [\"postgres\"", "features = [\"sqlite\""],
),
(
"crates/aether-data/adapters/sqlite/Cargo.toml",
"features = [\"sqlite\"",
["features = [\"postgres\"", "features = [\"mysql\""],
),
];
for (manifest_path, expected_driver, other_drivers) in adapters {
let manifest = read_workspace_file(manifest_path);
assert!(manifest.contains("aether-data-contracts.workspace = true"));
assert!(manifest.contains(expected_driver));
assert_manifest_excludes(
manifest_path,
&[other_drivers[0], other_drivers[1], "aether-gateway", "axum"],
);
}
let path = "crates/aether-data/adapters/postgres/Cargo.toml";
let manifest = read_workspace_file(path);
assert!(manifest.contains("aether-data-contracts.workspace = true"));
assert!(manifest.contains("features = [\"postgres\""));
assert_manifest_excludes(path, &["aether-gateway", "axum"]);
}
#[test]
fn data_facade_preserves_legacy_driver_paths_without_owning_driver_code() {
for (path, adapter) in [
(
"crates/aether-data/runtime/src/driver/postgres.rs",
"aether_data_postgres",
),
(
"crates/aether-data/runtime/src/driver/mysql.rs",
"aether_data_mysql",
),
(
"crates/aether-data/runtime/src/driver/sqlite.rs",
"aether_data_sqlite",
),
] {
let source = read_workspace_file(path);
assert!(
source.contains(&format!("pub use {adapter}::*;")),
"{path} should remain a thin compatibility facade"
);
assert!(!source.contains("sqlx::"));
}
let source = read_workspace_file("crates/aether-data/runtime/src/driver/postgres.rs");
assert!(source.contains("pub use aether_data_postgres::*;"));
assert!(!source.contains("sqlx::"));
}
#[test]
@@ -183,32 +143,16 @@ fn tunnel_binary_uses_shared_tunnel_boundary_without_gateway_runtime_dependency(
fn data_facade_defaults_to_postgres_and_gateway_selects_all_drivers_explicitly() {
let data_manifest = read_workspace_file("crates/aether-data/runtime/Cargo.toml");
assert!(data_manifest.contains("default = [\"postgres\"]"));
for dependency in [
"aether-data-postgres = { workspace = true, optional = true }",
"aether-data-mysql = { workspace = true, optional = true }",
"aether-data-sqlite = { workspace = true, optional = true }",
] {
assert!(
data_manifest.contains(dependency),
"aether-data should keep {dependency} optional"
);
}
assert!(
!data_manifest.contains("features = [\"postgres\", \"mysql\", \"sqlite\"\"]"),
"aether-data must not unconditionally enable every sqlx driver"
);
assert!(data_manifest.contains("aether-data-postgres = { workspace = true, optional = true }"));
assert!(data_manifest.contains("postgres = [\"dep:aether-data-postgres\", \"sqlx/postgres\"]"));
assert!(data_manifest.contains("all-drivers = [\"postgres\"]"));
let gateway_manifest = read_workspace_file("apps/aether-gateway/Cargo.toml");
assert!(gateway_manifest
.contains("aether-data = { workspace = true, features = [\"all-drivers\"] }"));
let data_lib = read_workspace_file("crates/aether-data/runtime/src/lib.rs");
for backend in ["PostgresBackend", "MysqlBackend", "SqliteBackend"] {
assert!(
data_lib.contains(&format!("pub use backend::{backend};")),
"aether-data should expose enabled backends symmetrically at its facade root"
);
}
assert!(data_lib.contains("pub use backend::PostgresBackend;"));
}
#[test]
@@ -219,58 +163,11 @@ fn data_query_helpers_belong_to_adapters_not_the_runtime_facade() {
"aether-data should not keep a direct query-helper dependency after SQL repositories move to adapters"
);
for adapter_manifest in [
"crates/aether-data/adapters/postgres/Cargo.toml",
"crates/aether-data/adapters/mysql/Cargo.toml",
"crates/aether-data/adapters/sqlite/Cargo.toml",
] {
let manifest = read_workspace_file(adapter_manifest);
assert!(
manifest.contains("aether-data-query.workspace = true"),
"{adapter_manifest} should own its query-helper dependency"
);
}
let manifest = read_workspace_file("crates/aether-data/adapters/postgres/Cargo.toml");
assert!(manifest.contains("aether-data-query.workspace = true"));
let query_helpers = read_workspace_file("crates/aether-data/query/src/lib.rs");
for dialect in ["Postgres", "MySql", "Sqlite"] {
assert!(
query_helpers.contains(dialect),
"aether-data-query should render the {dialect} dialect"
);
}
}
#[test]
fn sql_adapters_centralize_error_mapping_boilerplate() {
for (adapter, driver) in [
("aether-data-mysql", "mysql"),
("aether-data-sqlite", "sqlite"),
] {
let root = format!("crates/aether-data/adapters/{driver}/src");
let files = collect_workspace_rust_files(&root);
let trait_owners = files
.iter()
.filter(|path| {
std::fs::read_to_string(path)
.expect("adapter source should be readable")
.contains("trait SqlResultExt<T>")
})
.collect::<Vec<_>>();
assert_eq!(
trait_owners.len(),
1,
"{adapter} should have exactly one SqlResultExt owner, found: {trait_owners:?}"
);
assert_eq!(
trait_owners[0].file_name().and_then(|name| name.to_str()),
Some("error.rs"),
"{adapter} should keep SQL error mapping in src/error.rs"
);
let lib = read_workspace_file(&format!("crates/aether-data/adapters/{driver}/src/lib.rs"));
assert!(lib.contains("mod error;"));
}
assert!(query_helpers.contains("Postgres"));
}
#[test]