Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions src-tauri/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ pub async fn init_pool() -> Result<SqlitePool, String> {
// Catch the "DB ahead of binary" case BEFORE letting sqlx fire its
// cryptic `migration N was previously applied but is missing in the
// resolved migrations` error. The setup hook in `lib.rs` matches the
// `DB_AHEAD` prefix to render a friendly recovery notice — typical
// user landing here ran a newer dev build against this DB and is now
// trying to launch an older installed binary.
// `DB_AHEAD_MARKER` token to render a friendly recovery notice — the
// typical user landing here ran a newer dev build against this DB
// and is now trying to launch an older installed binary.
check_db_is_not_ahead_of_binary(&pool)
.await
.map_err(|e| format!("DB_AHEAD::{e}"))?;
.map_err(|e| format!("{DB_AHEAD_MARKER}{e}"))?;

// Run migrations
sqlx::migrate!("src/db/migrations")
Expand All @@ -80,6 +80,17 @@ pub async fn init_pool() -> Result<SqlitePool, String> {
Ok(pool)
}

/// Token prepended to the error message when `check_db_is_not_ahead_of_binary`
/// detects the failure mode. The setup hook in `lib.rs` matches this exact
/// token via `find()` (not `strip_prefix`) so the consumer is robust to any
/// future wrapping the producer adds. Single source of truth on both sides:
/// changing the token here must compile-error if the consumer drifts.
///
/// Boundary string `::` chosen because it's syntactically unusual in plain
/// French error messages, so a false positive on `find()` is effectively
/// impossible.
pub const DB_AHEAD_MARKER: &str = "DB_AHEAD::";

/// Detect the "DB has been migrated by a newer version than this binary
/// knows about" failure mode BEFORE `sqlx::migrate!().run()` does. The
/// stock sqlx error message — `migration 18 was previously applied but
Expand Down Expand Up @@ -679,6 +690,34 @@ mod migration_tests {
.expect("matching-version DB must pass the pre-flight");
}

#[test]
fn db_ahead_marker_is_findable_even_when_wrapped() {
// Regression guard against the Sourcery review on PR #64: the
// consumer in lib.rs uses `find(db::DB_AHEAD_MARKER)` rather
// than `strip_prefix("Failed to init SQLite: DB_AHEAD::")`.
// This test simulates the marker wrapped under arbitrary
// prefixes and confirms the detection still works — so a
// future refactor of the error wrapping in lib.rs cannot
// silently break the STARTUP_BLOCKED.txt path.
let marker = super::DB_AHEAD_MARKER;
let wrappings = [
format!("Failed to init SQLite: {marker}payload"),
format!("Some new wrapping: {marker}payload"),
format!("{marker}payload"), // no wrapping at all
format!("Layer A → Layer B: {marker}payload"),
];
for wrapped in &wrappings {
let idx = wrapped
.find(marker)
.unwrap_or_else(|| panic!("marker must be findable in wrapping: {wrapped}"));
let extracted = &wrapped[idx + marker.len()..];
assert_eq!(
extracted, "payload",
"payload must be cleanly extractable after the marker"
);
}
}

#[tokio::test]
async fn db_ahead_check_blocks_when_db_has_future_migration() {
// Reproduces the v0.3.8 ← v0.3.9 dev workflow crash: the user
Expand Down
18 changes: 14 additions & 4 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,21 @@ pub fn run() {
// Special handler for the "DB ahead of binary" pre-flight
// failure surfaced by db::check_db_is_not_ahead_of_binary.
// The user can't read the log file from a crashed app; we
// write a self-explanatory text file to the data dir so
// double-clicking app.db's folder reveals the recovery
// path. Pure best-effort — if the write fails too we just
// write a self-explanatory text file to the data dir
// (same dir as `app.db`, NOT the log dir which lives
// under the bundle identifier on Windows) so double-
// clicking that folder reveals the recovery path.
// Pure best-effort — if the write fails too we just
// fall through to the normal log-and-exit.
if let Some(payload) = e.strip_prefix("Failed to init SQLite: DB_AHEAD::") {
//
// We `find()` the marker rather than `strip_prefix()`
// so the detection survives any future change to the
// upstream wrapping in `setup_result` (e.g. a different
// prefix than "Failed to init SQLite: "). The marker
// itself is the single source of truth, defined in
// `db::DB_AHEAD_MARKER` and prepended in `init_pool`.
if let Some(idx) = e.find(db::DB_AHEAD_MARKER) {
let payload = &e[idx + db::DB_AHEAD_MARKER.len()..];
write_startup_blocked_notice(payload);
}
return Err(e.into());
Expand Down
Loading