Files
Aether/crates/aether-data/runtime/src/lifecycle/backfill/sqlite.rs
T
elky 778cfb1a5c feat(data): complete portable SQL backend parity
Align MySQL and SQLite schemas, migrations, usage, stats, export, and backfill behavior with the shared data contracts. Extend gateway startup and maintenance support across all SQL drivers.
2026-07-25 21:28:21 +08:00

255 lines
7.3 KiB
Rust

use std::collections::{HashMap, HashSet};
use sqlx::{
migrate::{Migrate, MigrateError, Migrator},
query, Connection, Row, SqliteConnection,
};
use tracing::{error, info, warn};
use super::types::PendingBackfillInfo;
use crate::driver::sqlite::SqlitePool;
static BACKFILL_MIGRATOR: Migrator = sqlx::migrate!("./backfills/sqlite");
const ENSURE_SCHEMA_BACKFILLS_TABLE_SQL: &str = r#"
CREATE TABLE IF NOT EXISTS schema_backfills (
version INTEGER NOT NULL PRIMARY KEY,
description TEXT NOT NULL,
success INTEGER NOT NULL DEFAULT 1,
checksum BLOB NOT NULL,
execution_time INTEGER NOT NULL DEFAULT 0,
applied_at INTEGER NOT NULL DEFAULT (CAST(strftime('%s', 'now') AS INTEGER))
)
"#;
const LIST_APPLIED_BACKFILLS_SQL: &str = r#"
SELECT version, checksum
FROM schema_backfills
WHERE success = 1
ORDER BY version ASC
"#;
const INSERT_APPLIED_BACKFILL_SQL: &str = r#"
INSERT INTO schema_backfills (
version,
description,
success,
checksum,
execution_time,
applied_at
) VALUES (
?,
?,
1,
?,
?,
CAST(strftime('%s', 'now') AS INTEGER)
)
ON CONFLICT(version) DO NOTHING
"#;
#[derive(Debug, Clone, PartialEq, Eq)]
struct AppliedBackfill {
version: i64,
checksum: Vec<u8>,
}
pub async fn run_backfills(pool: &SqlitePool) -> Result<(), MigrateError> {
let mut conn = pool.acquire().await?;
if BACKFILL_MIGRATOR.locking {
conn.lock().await?;
}
let result = run_backfills_locked(&mut conn).await;
if BACKFILL_MIGRATOR.locking {
match conn.unlock().await {
Ok(()) => {}
Err(unlock_error) if result.is_ok() => return Err(unlock_error),
Err(unlock_error) => {
warn!(
error = %unlock_error,
"sqlite database backfill lock release failed after backfill error"
);
}
}
}
result
}
pub async fn pending_backfills(
pool: &SqlitePool,
) -> Result<Vec<PendingBackfillInfo>, MigrateError> {
let mut conn = pool.acquire().await?;
pending_backfills_locked(&mut conn).await
}
async fn run_backfills_locked(conn: &mut SqliteConnection) -> Result<(), MigrateError> {
ensure_schema_backfills_table(conn).await?;
let applied_backfills = list_applied_backfills(conn).await?;
validate_applied_backfills(&applied_backfills)?;
let applied_by_version: HashMap<_, _> = applied_backfills
.iter()
.map(|backfill| (backfill.version, backfill))
.collect();
let pending_backfills: Vec<_> = BACKFILL_MIGRATOR
.iter()
.filter(|backfill| backfill.migration_type.is_up_migration())
.filter(|backfill| !applied_by_version.contains_key(&backfill.version))
.collect();
if pending_backfills.is_empty() {
info!(
driver = "sqlite",
pending_backfills = 0,
"database backfills already up to date"
);
return Ok(());
}
info!(
driver = "sqlite",
pending_backfills = pending_backfills.len(),
"database backfills pending"
);
for (index, backfill) in pending_backfills.iter().enumerate() {
let current = index + 1;
let total = pending_backfills.len();
info!(
driver = "sqlite",
current,
total,
version = backfill.version,
description = %backfill.description,
"applying database backfill"
);
let mut tx = conn.begin().await?;
let started_at = std::time::Instant::now();
sqlx::raw_sql(&backfill.sql).execute(&mut *tx).await?;
let elapsed_ms = i64::try_from(started_at.elapsed().as_millis()).unwrap_or(i64::MAX);
query(INSERT_APPLIED_BACKFILL_SQL)
.bind(backfill.version)
.bind(backfill.description.as_ref())
.bind(backfill.checksum.as_ref())
.bind(elapsed_ms)
.execute(&mut *tx)
.await?;
tx.commit().await?;
info!(
driver = "sqlite",
current,
total,
version = backfill.version,
description = %backfill.description,
elapsed_ms,
"applied database backfill"
);
}
info!(
driver = "sqlite",
pending_backfills = 0,
"database backfills complete"
);
Ok(())
}
async fn pending_backfills_locked(
conn: &mut SqliteConnection,
) -> Result<Vec<PendingBackfillInfo>, MigrateError> {
ensure_schema_backfills_table(conn).await?;
let applied_backfills = list_applied_backfills(conn).await?;
validate_applied_backfills(&applied_backfills)?;
Ok(pending_backfills_from_applied(&applied_backfills))
}
async fn ensure_schema_backfills_table(conn: &mut SqliteConnection) -> Result<(), MigrateError> {
query(ENSURE_SCHEMA_BACKFILLS_TABLE_SQL)
.execute(&mut *conn)
.await?;
Ok(())
}
async fn list_applied_backfills(
conn: &mut SqliteConnection,
) -> Result<Vec<AppliedBackfill>, MigrateError> {
let rows = query(LIST_APPLIED_BACKFILLS_SQL)
.fetch_all(&mut *conn)
.await?;
rows.into_iter()
.map(|row| {
Ok(AppliedBackfill {
version: row.try_get("version")?,
checksum: row.try_get("checksum")?,
})
})
.collect::<Result<Vec<_>, sqlx::Error>>()
.map_err(MigrateError::from)
}
fn validate_applied_backfills(applied_backfills: &[AppliedBackfill]) -> Result<(), MigrateError> {
if BACKFILL_MIGRATOR.ignore_missing {
return Ok(());
}
let known_versions: HashSet<_> = BACKFILL_MIGRATOR
.iter()
.map(|backfill| backfill.version)
.collect();
for applied_backfill in applied_backfills {
if !known_versions.contains(&applied_backfill.version) {
error!(
driver = "sqlite",
version = applied_backfill.version,
"applied database backfill is missing from embedded backfills"
);
return Err(MigrateError::VersionMissing(applied_backfill.version));
}
}
for backfill in BACKFILL_MIGRATOR
.iter()
.filter(|backfill| backfill.migration_type.is_up_migration())
{
let Some(applied) = applied_backfills
.iter()
.find(|applied| applied.version == backfill.version)
else {
continue;
};
if backfill.checksum != applied.checksum {
warn!(
driver = "sqlite",
version = backfill.version,
description = %backfill.description,
"applied database backfill checksum differs from embedded backfill; skipping strict enforcement"
);
}
}
Ok(())
}
fn pending_backfills_from_applied(
applied_backfills: &[AppliedBackfill],
) -> Vec<PendingBackfillInfo> {
let applied_versions: HashSet<_> = applied_backfills
.iter()
.map(|backfill| backfill.version)
.collect();
BACKFILL_MIGRATOR
.iter()
.filter(|backfill| backfill.migration_type.is_up_migration())
.filter(|backfill| !applied_versions.contains(&backfill.version))
.map(|backfill| PendingBackfillInfo {
version: backfill.version,
description: backfill.description.to_string(),
})
.collect()
}