This commit is contained in:
Mateusz Gruszczyński committed 2026-10-31 14:36:07 +01:00
1 parent fc81ecafd6
commit dc57aa7089
89 files changed
+5290 -1551

No files matched your search

+78 -19
View File
@@ -20,10 +20,20 @@ const SQLITE_SCHEMA: &str = include_str!("../../migrations/sqlite/schema.sql");
const MYSQL_SCHEMA: &str = include_str!("../../migrations/mysql/schema.sql");
const PGSQL_SCHEMA: &str = include_str!("../../migrations/pgsql/schema.sql");
// Future migrations start at V047. Each entry must exist for every supported engine.
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[];
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[];
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[];
// The squashed legacy baseline is V046. Runtime migrations start at V047 and
// are stored as real files for every supported SQL engine.
const SQLITE_MIGRATIONS: &[(u32, &str)] = &[(
47,
include_str!("../../migrations/sqlite/V047__blob_keys_and_search_outbox.sql"),
)];
const MYSQL_MIGRATIONS: &[(u32, &str)] = &[(
47,
include_str!("../../migrations/mysql/V047__blob_keys_and_search_outbox.sql"),
)];
const PGSQL_MIGRATIONS: &[(u32, &str)] = &[(
47,
include_str!("../../migrations/pgsql/V047__blob_keys_and_search_outbox.sql"),
)];
const TENANT_TABLES: &[&str] = &[
"calendar_events",
@@ -48,6 +58,7 @@ const TENANT_TABLES: &[&str] = &[
"pgp_preferences",
"response_templates",
"saved_searches",
"search_outbox",
"special_folders",
"tags",
"trusted_senders",
@@ -79,6 +90,7 @@ const IMPORT_TABLES: &[&str] = &[
"pgp_preferences",
"response_templates",
"saved_searches",
"search_outbox",
"special_folders",
"trusted_senders",
"user_meta",
@@ -96,7 +108,7 @@ impl DatabaseBackend {
match value.trim().to_ascii_lowercase().as_str() {
"sqlite" => Ok(Self::Sqlite),
"mysql" => Ok(Self::Mysql),
"pgsql" | "postgres" | "postgresql" => Ok(Self::Pgsql),
"pgsql" => Ok(Self::Pgsql),
other => Err(format!("unsupported database backend: {other}")),
}
}
@@ -291,6 +303,46 @@ pub fn configured_backend() -> DatabaseBackend {
}
}
pub fn list_account_user_hashes() -> Result<Vec<String>, String> {
match DATABASE.get() {
Some(ConfiguredDatabase::Sqlite { path }) => {
let conn = rusqlite::Connection::open(path)
.map_err(|e| format!("failed to open SQLite account registry: {e}"))?;
let mut stmt = conn
.prepare("SELECT user_hash FROM rustbox_accounts ORDER BY user_hash")
.map_err(|e| format!("failed to prepare account registry query: {e}"))?;
let rows = stmt
.query_map([], |row| row.get::<_, String>(0))
.map_err(|e| format!("failed to query account registry: {e}"))?;
rows.map(|row| row.map_err(|e| format!("failed to read account registry row: {e}")))
.collect()
}
Some(ConfiguredDatabase::Mysql { pool, runtime }) => {
let pool = pool.clone();
runtime_wait(runtime, async move {
sqlx::query_scalar::<_, String>(
"SELECT user_hash FROM rustbox_accounts ORDER BY user_hash",
)
.fetch_all(&pool)
.await
.map_err(|e| format!("failed to query MySQL account registry: {e}"))
})
}
Some(ConfiguredDatabase::Pgsql { pool, runtime }) => {
let pool = pool.clone();
runtime_wait(runtime, async move {
sqlx::query_scalar::<_, String>(
"SELECT user_hash FROM rustbox_accounts ORDER BY user_hash",
)
.fetch_all(&pool)
.await
.map_err(|e| format!("failed to query PostgreSQL account registry: {e}"))
})
}
None => Err("database is not configured".to_string()),
}
}
#[macro_export]
macro_rules! dbq {
($module:ident::$name:ident) => {{
@@ -1562,12 +1614,16 @@ fn read_legacy_sqlite(path: &Path) -> Result<Vec<LegacyTableData>, String> {
.map_err(|e| format!("failed to read legacy SQLite schema version: {e}"))?;
if version < BASE_SCHEMA_VERSION {
return Err(format!(
"legacy SQLite schema version {version} is older than the RustBox 0.1.2 baseline {BASE_SCHEMA_VERSION}; upgrade with RustBox 0.1.0 first"
"legacy SQLite schema version {version} is older than the legacy RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with RustBox 0.1.0 first"
));
}
if version > BASE_SCHEMA_VERSION {
let latest_supported_sqlite = SQLITE_MIGRATIONS
.last()
.map(|(migration_version, _)| *migration_version)
.unwrap_or(BASE_SCHEMA_VERSION);
if version > latest_supported_sqlite {
return Err(format!(
"legacy SQLite schema version {version} is newer than the RustBox 0.1.2 baseline {BASE_SCHEMA_VERSION}"
"legacy SQLite schema version {version} is newer than the latest SQLite schema supported by this build ({latest_supported_sqlite})"
));
}
let mut output = Vec::new();
@@ -1904,7 +1960,7 @@ fn ensure_sqlite_tenant(
.map_err(|e| format!("failed to initialize SQLite tenant defaults: {e}"))?;
tx.execute(
"INSERT INTO rustbox_accounts(user_hash, table_prefix, schema_version) VALUES(?1, ?2, ?3)",
rusqlite::params![user_hash, prefix, BASE_SCHEMA_VERSION],
rusqlite::params![user_hash, prefix, latest_schema_version()],
)
.map_err(|e| format!("failed to register SQLite account schema: {e}"))?;
tx.commit().map_err(|e| e.to_string())?;
@@ -1922,7 +1978,7 @@ fn apply_sqlite_migrations(
) -> Result<(), String> {
if current < BASE_SCHEMA_VERSION {
return Err(format!(
"database schema version {current} is older than the RustBox 0.1.2 baseline {BASE_SCHEMA_VERSION}; upgrade with RustBox 0.1.0 first"
"database schema version {current} is older than the legacy RustBox baseline {BASE_SCHEMA_VERSION}; upgrade with RustBox 0.1.0 first"
));
}
let latest = latest_schema_version();
@@ -1967,7 +2023,8 @@ fn ensure_server_tenant(
user_hash: &str,
prefix: &str,
) -> Result<(), String> {
let legacy_path = Path::new(data_dir).join(user_hash).join("db.sqlite");
let legacy_path = (!data_dir.trim().is_empty())
.then(|| Path::new(data_dir).join(user_hash).join("db.sqlite"));
let imported_legacy = match configured {
ConfiguredDatabase::Mysql { pool, runtime } => {
@@ -2015,8 +2072,8 @@ fn ensure_server_tenant(
.bind(&prefix).fetch_optional(&pool).await.map_err(|e| e.to_string())?;
if collision.is_some() { return Err("database account-prefix collision detected".into()); }
let legacy_data = if legacy_path.is_file() {
read_legacy_sqlite(&legacy_path)?
let legacy_data = if let Some(path) = legacy_path.as_ref().filter(|path| path.is_file()) {
read_legacy_sqlite(path)?
} else {
Vec::new()
};
@@ -2035,7 +2092,7 @@ fn ensure_server_tenant(
return Err(format!("failed to initialize MySQL tenant defaults: {error}"));
}
if let Err(error) = sqlx::query("INSERT INTO rustbox_accounts(user_hash, table_prefix, schema_version) VALUES(?, ?, ?)")
.bind(&user_hash).bind(&prefix).bind(i64::from(BASE_SCHEMA_VERSION)).execute(&pool).await
.bind(&user_hash).bind(&prefix).bind(i64::from(latest_schema_version())).execute(&pool).await
{
drop_mysql_tenant(&pool, &prefix).await;
return Err(format!("failed to register MySQL account schema: {error}"));
@@ -2090,8 +2147,8 @@ fn ensure_server_tenant(
.bind(&prefix).fetch_optional(&pool).await.map_err(|e| e.to_string())?;
if collision.is_some() { return Err("database account-prefix collision detected".into()); }
let legacy_data = if legacy_path.is_file() {
read_legacy_sqlite(&legacy_path)?
let legacy_data = if let Some(path) = legacy_path.as_ref().filter(|path| path.is_file()) {
read_legacy_sqlite(path)?
} else {
Vec::new()
};
@@ -2110,7 +2167,7 @@ fn ensure_server_tenant(
return Err(format!("failed to initialize PostgreSQL tenant defaults: {error}"));
}
if let Err(error) = sqlx::query("INSERT INTO rustbox_accounts(user_hash, table_prefix, schema_version) VALUES($1, $2, $3)")
.bind(&user_hash).bind(&prefix).bind(i64::from(BASE_SCHEMA_VERSION)).execute(&pool).await
.bind(&user_hash).bind(&prefix).bind(i64::from(latest_schema_version())).execute(&pool).await
{
drop_pgsql_tenant(&pool, &prefix).await;
return Err(format!("failed to register PostgreSQL account schema: {error}"));
@@ -2128,7 +2185,9 @@ fn ensure_server_tenant(
};
if imported_legacy {
backup_legacy_sqlite(&legacy_path);
if let Some(path) = legacy_path.as_ref() {
backup_legacy_sqlite(path);
}
}
Ok(())
}
@@ -2215,7 +2274,7 @@ pub fn open_test_db() -> Connection {
.unwrap();
raw.execute(
"INSERT INTO rustbox_accounts(user_hash, table_prefix, schema_version) VALUES(?1, ?2, ?3)",
rusqlite::params![user_hash, prefix, BASE_SCHEMA_VERSION],
rusqlite::params![user_hash, prefix, latest_schema_version()],
)
.unwrap();
Connection {