TanStack / TanStack/db

SQLite persistence: recover or provide helper for corrupt local DB files

Open
#1,567 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
3.9k
Forks
266
Avg merge
1d 4h
Merged PRs (30d)
55

Description

Summary

A downstream Electron app hit a startup failure when its TanStack DB SQLite persistence file became corrupt/truncated. The app uses TanStack DB with SQLite persistence as a local sync/cache DB; once the SQLite file was malformed, every launch failed until the file was manually removed.

Downstream PR with repro context and a proposed app-level workaround: https://github.com/superset-sh/superset/pull/5085

Observed downstream failure

After an app auto-update/restart, the local tanstack-db.sqlite file was truncated/corrupt. On subsequent launches, TanStack DB persistence initialization threw via better-sqlite3, for example:

SqliteError: database disk image is malformed
code: SQLITE_CORRUPT

Because this occurred during app startup, the downstream Electron app failed before creating a window and remained as a windowless process. That windowless-process behavior is app-specific, but the underlying persistence failure mode is relevant here: a corrupt local TanStack DB SQLite persistence file can permanently brick startup until the DB file is manually deleted/quarantined.

Why this may belong in TanStack DB

For SQLite-backed persistence that functions as a rebuildable sync/cache store, corruption recovery could be handled or at least made easier at the TanStack DB persistence layer. Downstream apps should not each need to rediscover the same recovery pattern for SQLITE_CORRUPT / SQLITE_NOTADB cache files.

Proposed fix / API shape

Consider adding one of these to the SQLite persistence package:

  1. Built-in corruption recovery option, e.g.
createNodeSQLitePersistence({
  database,
  // or dbPath, depending on API direction
  recoverCorruptDatabase: true,
  quarantineCorruptDatabase: true,
})
  1. A helper/wrapper that downstream apps can use around SQLite persistence initialization:
openSqliteWithRecovery(dbPath, label, () => {
  const database = new Database(dbPath)
  const persistence = createNodeSQLitePersistence({ database })
  return { database, persistence }
})

Recommended recovery behavior:

  • Detect SQLite corruption-style errors:
    • SQLITE_CORRUPT
    • SQLITE_CORRUPT_*
    • SQLITE_NOTADB
  • Close any opened DB handle before file operations.
  • Rename/quarantine the corrupt DB rather than deleting it, e.g. <db>.corrupt-<timestamp>.
  • Also quarantine sidecar files if present:
    • <db>-wal
    • <db>-shm
  • Retry opening once against a fresh DB path.
  • Rethrow non-corruption errors.
  • Rethrow if the retry also fails, to avoid infinite retry loops.

Implementation note

If the helper creates the better-sqlite3 handle, make sure construction is inside the try and close the handle in catch if one was created:

let opened: Database.Database | undefined

try {
  opened = new Database(dbPath)
  const persistence = createNodeSQLitePersistence({ database: opened })
  return { database: opened, persistence }
} catch (error) {
  opened?.close()
  throw error
}

This avoids relying on native cleanup behavior if better-sqlite3 throws during construction and helps on Windows where open handles can prevent renaming/quarantining the corrupt file.

Acceptance criteria

  • SQLite corruption errors can be identified consistently.
  • A corrupt persistence DB can be quarantined and recreated without manual deletion.
  • -wal and -shm sidecars are handled with the main DB.
  • Recovery retries once, not indefinitely.
  • Non-corruption errors are not swallowed.
  • Tests cover corruption detection, quarantine behavior, retry-once behavior, non-corruption rethrow, and second-failure propagation.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in the SQLite persistence package at createNodeSQLitePersistence and inspect how better-sqlite3 initialization errors are currently handled. Define the recovery approach from the acceptance criteria, then add tests for corruption detection, quarantine and sidecars, one retry, non-corruption rethrow, and a second failure.

Written by the indexing model from the issue text.

Assessment

Tech stack
sqlite, typescript
Domain
databases
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.