Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
70a9b31
fix(react-native): decode op-sqlite results losslessly
KyleAMathews Sep 17, 2026
e0b02a0
Merge remote-tracking branch 'github/main' into rfc-1659-ws3-red-oracle
KyleAMathews Sep 17, 2026
adc9ed0
Apply WS3 review fixes
KyleAMathews Sep 18, 2026
6a79dcd
docs: add op-sqlite decoding changeset
KyleAMathews Sep 18, 2026
3afe2df
docs(test): explain op-sqlite driver oracles
KyleAMathews Sep 21, 2026
c0b1238
Merge remote-tracking branch 'github/main' into rfc-1659-ws3-red-oracle
KyleAMathews Sep 21, 2026
2cefc47
fix(react-native): decode op-sqlite result envelopes
KyleAMathews Sep 22, 2026
7aa912d
fix(react-native): preserve write-marker query rows
KyleAMathews Sep 22, 2026
0fb27a4
test(react-native): add result carrier refinement law
KyleAMathews Sep 22, 2026
a9a60dd
fix(react-native): require ambiguous array result mode
KyleAMathews Sep 22, 2026
c4572bc
test(react-native): declare statement-array result mode
KyleAMathews Sep 22, 2026
8c8d818
test(react-native): freeze OP-SQLite provider receipts
KyleAMathews Sep 22, 2026
fc67799
fix(react-native): honor declared OP-SQLite envelopes
KyleAMathews Sep 22, 2026
6997f35
fix(react-native): validate op-sqlite row carriers
KyleAMathews Sep 22, 2026
6a89556
fix(react-native): harden op-sqlite result contracts
KyleAMathews Sep 23, 2026
9643523
Merge remote-tracking branch 'origin/main' into rfc-1659-ws3-red-oracle
KyleAMathews Sep 23, 2026
30799c0
test(react-native): preserve constructor column data
KyleAMathews Sep 23, 2026
fdb9929
test(react-native): strengthen op-sqlite oracle replay
KyleAMathews Sep 23, 2026
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
6 changes: 6 additions & 0 deletions .changeset/fix-op-sqlite-result-decoding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/react-native-db-sqlite-persistence': patch
---

Decode op-sqlite row arrays and columnar results losslessly, and reject malformed or unknown result envelopes instead of treating them as empty results.
Custom wrappers that return a genuinely ambiguous bare array can set `arrayResultMode` to `rows` or `statement-results`; without that authoritative mode, the driver rejects instead of silently reshaping data.
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,36 @@ async function withHarness<T>(
fn: (harness: SQLiteDriverContractHarness) => Promise<T>,
): Promise<T> {
const harness = createHarness()
let outcome: { ok: true; value: T } | { ok: false; error: unknown }
try {
return await fn(harness)
} finally {
await Promise.resolve(harness.cleanup())
outcome = { ok: true, value: await fn(harness) }
} catch (error) {
outcome = { ok: false, error }
}

let cleanupOutcome: { ok: true } | { ok: false; error: unknown } = {
ok: true,
}
try {
await harness.cleanup()
} catch (error) {
cleanupOutcome = { ok: false, error }
}

if (!outcome.ok) {
if (!cleanupOutcome.ok) {
throw new AggregateError(
[outcome.error, cleanupOutcome.error],
`SQLite driver contract and cleanup failed`,
{ cause: outcome.error },
)
}
throw outcome.error
}
if (!cleanupOutcome.ok) {
throw cleanupOutcome.error
}
return outcome.value
}

export function runSQLiteDriverContractSuite(
Expand Down Expand Up @@ -70,6 +95,89 @@ export function runSQLiteDriverContractSuite(
})
})

it(`preserves exact query rows after exec and run write results`, async () => {
await withHarness(createHarness, async ({ driver }) => {
await driver.exec(
`CREATE TABLE write_then_read (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
score INTEGER NOT NULL
)`,
)
await driver.run(
`INSERT INTO write_then_read (id, title, score) VALUES (?, ?, ?)`,
[`run-row`, `Inserted by run`, 17],
)
await driver.exec(
`INSERT INTO write_then_read (id, title, score)
VALUES ('exec-row', 'Inserted by exec', 29)`,
)

expect(
await driver.query<{ id: string; title: string; score: number }>(
`SELECT title, id, score
FROM write_then_read
ORDER BY score DESC`,
),
).toEqual([
{ title: `Inserted by exec`, id: `exec-row`, score: 29 },
{ title: `Inserted by run`, id: `run-row`, score: 17 },
])
})
})

it(`preserves SQL aliases that match statement-result field names`, async () => {
await withHarness(createHarness, async ({ driver }) => {
const rows = await driver.query<{
rows: string
resultRows: string
rawRows: string
columnNames: string
results: string
rowsAffected: number
changes: number
insertId: number
lastInsertRowId: number
}>(
`SELECT
'rows-value' AS "rows",
'resultRows-value' AS "resultRows",
'rawRows-value' AS "rawRows",
'columnNames-value' AS "columnNames",
'results-value' AS "results",
7 AS "rowsAffected",
8 AS "changes",
9 AS "insertId",
10 AS "lastInsertRowId"`,
)

expect(rows).toEqual([
{
rows: `rows-value`,
resultRows: `resultRows-value`,
rawRows: `rawRows-value`,
columnNames: `columnNames-value`,
results: `results-value`,
rowsAffected: 7,
changes: 8,
insertId: 9,
lastInsertRowId: 10,
},
])
expect(Object.keys(rows[0] ?? {})).toEqual([
`rows`,
`resultRows`,
`rawRows`,
`columnNames`,
`results`,
`rowsAffected`,
`changes`,
`insertId`,
`lastInsertRowId`,
])
})
})

it(`rolls back transaction when callback throws`, async () => {
await withHarness(createHarness, async ({ driver }) => {
await driver.exec(
Expand Down Expand Up @@ -249,11 +357,9 @@ export function runSQLiteDriverContractSuite(
it(`requires transaction callbacks to accept a driver argument`, async () => {
await withHarness(createHarness, async ({ driver }) => {
await expect(
driver.transaction(
(async () => undefined) as unknown as (
transactionDriver: SQLiteDriver,
) => Promise<void>,
),
driver.transaction((() => Promise.resolve()) as unknown as (
transactionDriver: SQLiteDriver,
) => Promise<void>),
).rejects.toThrow(`transaction driver argument`)
})
})
Expand Down
6 changes: 6 additions & 0 deletions packages/react-native-db-sqlite-persistence/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,9 @@ export const todosCollection = createCollection(
- `createReactNativeSQLitePersistence` is shared across collections.
- Mode defaults (`sync-present` vs `sync-absent`) are inferred from whether a
`sync` config is present in `persistedCollectionOptions`.
- Published OP-SQLite methods return self-describing object envelopes and need
no result-shape option. If a custom database wrapper instead returns a bare
array, set `arrayResultMode: 'rows'` for an array of data rows or
`arrayResultMode: 'statement-results'` for an array containing one statement
result. Undeclared arrays that also look like statement envelopes are rejected
rather than being reshaped heuristically.
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ import { runMobilePersistedCollectionConformanceSuite } from './mobile-persisted

runMobilePersistedCollectionConformanceSuite(
`expo persisted collection conformance`,
(database) => createReactNativeSQLitePersistence({ database }),
(database) =>
createReactNativeSQLitePersistence({
database,
arrayResultMode: `statement-results`,
}),
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@ import { runMobilePersistedCollectionConformanceSuite } from './mobile-persisted

runMobilePersistedCollectionConformanceSuite(
`react-native persisted collection conformance`,
(database) => createReactNativeSQLitePersistence({ database }),
(database) =>
createReactNativeSQLitePersistence({
database,
arrayResultMode: `statement-results`,
}),
)
1 change: 1 addition & 0 deletions packages/react-native-db-sqlite-persistence/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { createReactNativeSQLitePersistence } from './react-native'
export type {
OpSQLiteArrayResultMode,
OpSQLiteDatabaseLike,
ReactNativeSQLitePersistenceOptions,
ReactNativeSQLiteSchemaMismatchPolicy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ import type {
SQLiteCoreAdapterOptions,
SQLiteDriver,
} from '@tanstack/db-sqlite-persistence-core'
import type { OpSQLiteDatabaseLike } from './op-sqlite-driver'
import type {
OpSQLiteArrayResultMode,
OpSQLiteDatabaseLike,
} from './op-sqlite-driver'

export type { OpSQLiteDatabaseLike } from './op-sqlite-driver'
export type {
OpSQLiteArrayResultMode,
OpSQLiteDatabaseLike,
} from './op-sqlite-driver'

type MobileSQLiteCoreSchemaMismatchPolicy =
| `sync-present-reset`
Expand All @@ -30,6 +36,7 @@ type MobileSQLitePersistenceBaseOptions = Omit<
`driver` | `schemaVersion` | `schemaMismatchPolicy`
> & {
database: OpSQLiteDatabaseLike
arrayResultMode?: OpSQLiteArrayResultMode
coordinator?: PersistedCollectionCoordinator
schemaMismatchPolicy?: MobileSQLiteSchemaMismatchPolicy
}
Expand Down Expand Up @@ -71,6 +78,7 @@ function createInternalSQLiteDriver(
): SQLiteDriver {
return new OpSQLiteDriver({
database: options.database,
arrayResultMode: options.arrayResultMode,
})
}

Expand Down
Loading
Loading