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:
execute returns: { rows: Array<Record<string, Scalar>> } (object rows)
executeAsync returns: { 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.rows is undefined -> returns null
toRowArray(value.resultRows) -> value.resultRows is undefined -> returns null
hasWriteResultMarker(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:
ensureCollectionReadyInternal SELECTs from collection_registry -> gets [] instead of the existing row
- Code takes the INSERT branch -> fails with
UNIQUE constraint failed: collection_registry.tombstone_table_name
getStreamPosition rejects -> the persistence runtime's ensureStartupMetadataLoaded fails
- The Electric sync function is never called -> the collection stays in
loading state 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
persistedCollectionOptions and createReactNativeSQLitePersistence using 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
ready status
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 + columnNames handling to extractRowsFromStatementResult:
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
resolveExecuteMethod to prefer execute over executeAsync, since execute returns the { rows } format the driver already handles.
Describe the bug
OpSQLiteDriverin@tanstack/react-native-db-sqlite-persistencesilently returns empty arrays for SELECT queries when op-sqlite'sexecuteAsyncmethod is used. This causes the persistence layer to think collections don't exist incollection_registryon app restart, leading to UNIQUE constraint violations.Root cause
resolveExecuteMethodpicks the first available method from[executeAsync, execute, executeRaw, execAsync]. On op-sqlite v14,executeAsyncis available and gets selected.The problem is that op-sqlite's
executeAsyncreturns a different result format thanexecute:executereturns:{ rows: Array<Record<string, Scalar>> }(object rows)executeAsyncreturns:{ rowsAffected: number, rawRows: unknown[][], columnNames: string[] }(raw columnar format)extractRowsFromStatementResultonly handles the{ rows }and{ resultRows }shapes. When it receives anexecuteAsyncresult: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 rowUNIQUE constraint failed: collection_registry.tombstone_table_namegetStreamPositionrejects -> the persistence runtime'sensureStartupMetadataLoadedfailsloadingstate forever with no dataThe
ALTER TABLE ADD COLUMNerrors inensureInitializedare a separate but related symptom —executeAsyncis used for those DDL statements too, and the existing error handling (isDuplicateColumnAddError) works but still logs errors.To Reproduce
persistedCollectionOptionsandcreateReactNativeSQLitePersistenceusing op-sqlite v14+readystatusExpected behavior
extractRowsFromStatementResultshould handle the{ rawRows, columnNames }format returned byexecuteAsync, converting it into the expectedArray<Record<string, unknown>>shape. Alternatively,resolveExecuteMethodshould preferexecuteoverexecuteAsync.Smartphone (please complete the following information):
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.4Current workaround:
Remove
executeAsyncfrom the database handle before passing it tocreateReactNativeSQLitePersistence, forcing the driver to fall back toexecute:Suggested fix:
Either:
rawRows+columnNameshandling toextractRowsFromStatementResult:resolveExecuteMethodto preferexecuteoverexecuteAsync, sinceexecutereturns the{ rows }format the driver already handles.