docs/sync-and-op-log/sqlite-migration.md
Status (July 2026): Foundation implemented and tested in CI; native rollout not wired. IndexedDB remains the live op-log backend on every platform.
This document is the single status, rationale, and rollout contract for the
native SQLite work associated with #7892 and #7931. The former
sqlite-migration-followup.md path is only a
compatibility pointer.
On Capacitor Android, critical op-log state currently lives in WebView
IndexedDB (SUP_OPS), which can be lost if WebView storage is evicted. The goal
is to move that database to app-private SQLite on native iOS/Android while
preserving the operation log's atomicity and recovery behavior.
This is not a global storage rewrite:
The mobile local-backup safeguards from #7924/#7925 are already active. They reduce the blast radius but do not replace durable app-private storage.
OpLogDbAdapter / OpLogTx define the backend-neutral persistence and
transaction contract; OP_LOG_DB_SCHEMA describes the stores and indexes.IndexedDbOpLogAdapter is the production backend. Both
OperationLogStoreService and ArchiveStoreService obtain adapters through
OP_LOG_DB_ADAPTER_FACTORY.adapter.init() and do not open the
WebView database.SqliteOpLogAdapter implements the port against a minimal SqliteDb
interface. It is covered by the in-memory translation tests, a real sql.js
contract pass, and a store-level integration pass.SqliteDb also share a FIFO queue
keyed by that connection, preventing overlapping BEGIN statements and
statements leaking into another transaction.migrateOpLogBackend() copies all op-log stores into an empty destination
transaction and verifies operation count, last sequence, and vector clock
before commit. It is validated in CI for real IndexedDB to sql.js.local-rules/no-adapter-in-tx enforces the SQLite re-entrancy rule: code in a
transaction callback must use its tx handle, not enqueue another adapter
call behind its own transaction.SqliteDb
wrapper.OP_LOG_DB_ADAPTER_FACTORY still returns IndexedDbOpLogAdapter everywhere.migrateOpLogBackend() has no startup trigger or completion marker.SqliteOpLogAdapter still falls back to sequence 0 if SqliteDb.run()
omits lastId; native rollout must replace that invalid fallback with a
positive-integer assertion.Nothing in the landed SQLite foundation changes runtime storage behavior for current users.
The SQLite backend must preserve the same observable guarantees as IndexedDB:
ops.seq is a positive, monotonically allocated primary key and op.id is
unique.appendWithVectorClockOverwrite() writes the operation and vector clock
atomically.OpLogTx. Re-entering a
public adapter method would wait behind the transaction's own queue slot.The native wrapper must return the inserted row ID from the same write. An absent, zero, non-integer, or separately queried ID must fail before it can become an operation sequence.
The first native rollout must treat backend migration as a high-risk state replacement, not as a best-effort copy:
SUP_OPS.migrateOpLogBackend() implements the copy and verify-before-commit core.
Startup quiescence, detection, marker/fallback policy, and lifecycle handling
remain caller responsibilities.
Complete these in order:
SqliteDb wrapper over one
app-private database connection.migrateOpLogBackend(), the completion
marker, retained-source fallback, and interrupted-migration recovery.adoptConnection bridge only after the
retained-source window and rollback evidence are complete.Do not expand the migration to non-critical IndexedDB databases as part of these gates.
| Concern | Owner |
|---|---|
| Persistence port and transaction rules | src/app/op-log/persistence/op-log-db-adapter.ts |
| Backend DI default | src/app/op-log/persistence/op-log-db-adapter.token.ts |
| IndexedDB backend | src/app/op-log/persistence/indexed-db-op-log-adapter.ts |
| SQLite backend and shared-connection queue | src/app/op-log/persistence/sqlite-op-log-adapter.ts |
| Backend migration core | src/app/op-log/persistence/op-log-backend-migration.ts |
| Schema | src/app/op-log/persistence/op-log-db-schema.ts |
Focused CI checks:
npm run test:file src/app/op-log/persistence/sqlite-op-log-adapter.spec.ts
npm run test:file src/app/op-log/persistence/op-log-backend-migration.spec.ts
npm run test:file src/app/op-log/testing/integration/remote-apply-store-port.integration.spec.ts
CI proves adapter and SQLite-engine semantics, not the Capacitor bridge or device lifecycle. The rollout remains blocked until the on-device gates above are reproducible.