OpSQLiteDriver: executeAsync result format not handled by extractRowsFromStatementResult, causing silent data loss on SELECT queries
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 3.9k
- Forks
- 266
- Avg merge
- 1d 4h
- Merged PRs (30d)
- 55
Description
- I've validated the bug against the latest version of DB packages
Describe the bug
OpSQLiteDriver in @tanstack/react-native-db-sqlite-persistence silently returns empty arrays for SELECT queries when op-sqlite's executeAsync method is used. This causes the persistence layer to think collections don't exist in collection_registry on app restart, leading to UNIQUE constraint violations.
Root cause
resolveExecuteMethod picks the first available method from [executeAsync, execute, executeRaw, execAsync]. On op-sqlite v14, executeAsync is available and gets selected.
The problem is that op-sqlite's executeAsync returns a different result format than execute:
executereturns:{ rows: Array<Record<string, Scalar>> }(object rows)executeAsyncreturns:{ rowsAffected: number, rawRows: unknown[][], columnNames: string[] }(raw columnar format)
extractRowsFromStatementResult only handles the { rows } and { resultRows } shapes. When it receives an executeAsync result:
toRowArray(value.rows)->value.rowsis undefined -> returns nulltoRowArray(value.resultRows)->value.resultRowsis undefined -> returns nullhasWriteResultMarker(value)->"rowsAffected" in value-> true -> returns[]
The SELECT result is silently treated as a write result with zero rows, even though the data is present in rawRows.
Impact
This causes a cascade of failures on app restart when the database already has data:
ensureCollectionReadyInternalSELECTs fromcollection_registry-> gets[]instead of the existing row- Code takes the INSERT branch -> fails with
UNIQUE constraint failed: collection_registry.tombstone_table_name getStreamPositionrejects -> the persistence runtime'sensureStartupMetadataLoadedfails- The Electric sync function is never called -> the collection stays in
loadingstate forever with no data
The ALTER TABLE ADD COLUMN errors in ensureInitialized are a separate but related symptom — executeAsync is used for those DDL statements too, and the existing error handling (isDuplicateColumnAddError) works but still logs errors.
To Reproduce
- Create a collection with
persistedCollectionOptionsandcreateReactNativeSQLitePersistenceusing op-sqlite v14+ - Let it sync data from an Electric shape
- Kill the app (full process kill, not hot reload)
- Relaunch the app
- The persistence layer crashes on startup — the collection never reaches
readystatus
Expected behavior
extractRowsFromStatementResult should handle the { rawRows, columnNames } format returned by executeAsync, converting it into the expected Array<Record<string, unknown>> shape. Alternatively, resolveExecuteMethod should prefer execute over executeAsync.
Smartphone (please complete the following information):
- Device: iOS Simulator & physical Android device
- OS: iOS 18, Android 14
- Version: N/A (React Native app, not browser)
Additional context
Package versions:
@tanstack/db-sqlite-persistence-core: 0.1.9@tanstack/react-native-db-sqlite-persistence: 0.1.9@op-engineering/op-sqlite: 14.1.4- React Native (Expo)
Current workaround:
Remove executeAsync from the database handle before passing it to createReactNativeSQLitePersistence, forcing the driver to fall back to execute:
const database = open({ name: 'my-db.sqlite', location: 'default' });
delete (database as any).executeAsync;
const persistence = createReactNativeSQLitePersistence({ database });
Suggested fix:
Either:
- Add
rawRows+columnNameshandling toextractRowsFromStatementResult:
function extractRowsFromStatementResult(value) {
// Handle op-sqlite executeAsync format: { rawRows, columnNames }
if (Array.isArray(value.rawRows) && Array.isArray(value.columnNames)) {
return value.rawRows.map((row) =>
Object.fromEntries(value.columnNames.map((col, i) => [col, row[i]]))
);
}
// ... existing logic
}
- Or change
resolveExecuteMethodto preferexecuteoverexecuteAsync, sinceexecutereturns the{ rows }format the driver already handles.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Locate extractRowsFromStatementResult and resolveExecuteMethod in the React Native SQLite persistence package, then inspect how statement results are normalized. Reproduce the restart scenario with op-sqlite 14.1.4 and verify that SELECT results preserve their rows and collection startup no longer reaches the UNIQUE constraint failure.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- database, mobile-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 72/100