Skip to content

fix(react-native): decode op-sqlite results losslessly - #1848

Open
KyleAMathews wants to merge 5 commits into
mainfrom
rfc-1659-ws3-red-oracle
Open

KyleAMathews wants to merge 5 commits into
mainfrom
rfc-1659-ws3-red-oracle

Conversation

@KyleAMathews

@KyleAMathews KyleAMathews commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

This fixes silent SELECT data loss in the React Native SQLite adapter by decoding op-sqlite v14 columnar results into exact object rows. Malformed or unknown result envelopes now fail with a configuration error, while persisted rows, registry identity, and stream position remain intact across restart.

Reviewer guidance

Root cause

OpSQLiteDriver understood direct row arrays and rows-style wrappers, but op-sqlite v14 can return executeAsync results as { rawRows, columnNames, rowsAffected }. The old extraction path did not reconstruct those rows and could normalize an unrecognized SELECT result to an empty result, making real data appear to disappear.

Approach

  • Decode columnar rows by zipping each rawRows entry with columnNames, preserving column order, aliases, row order, values, and multiplicity.
  • Keep direct row arrays, supported row-list carriers, write-result envelopes, and exactly one supported results wrapper distinct at the outer result boundary.
  • Reject missing or conflicting carriers, duplicate columns, row-width mismatches, unsupported wrapper depth (including cycles), and all other malformed or unknown envelopes with InvalidPersistedCollectionConfigError.
  • Exercise writes followed by exact SELECTs through the shared SQLite driver contract, then close and reopen a pre-populated registry using an ordinary-row writer and a columnar reader.

Key invariants

  • Unknown SELECT shapes never become an empty result.
  • SQL aliases such as rows, rawRows, and rowsAffected remain ordinary row data.
  • A restart reuses the single registry mapping and preserves the exact durable stream position and rows.
  • Oracle cleanup preserves the primary failure and reports cleanup failures separately.

Non-goals and trade-offs

  • The decoder intentionally supports one documented result wrapper and rejects deeper or ambiguous envelopes instead of guessing or normalizing them.
  • The better-sqlite3 fixture establishes adapter behavior with real SQLite semantics, but does not claim native iOS/Android bridge coverage; versioned native-device receipts remain a separate verification cell.
  • This PR does not integrate or depend on fix: preserve offline runtime correctness across replay and restart #1837. That PR was considered as evidence only and is outside this branch's ancestry and scope.

Verification

pnpm --filter @tanstack/react-native-db-sqlite-persistence test -- --maxWorkers=2
pnpm --filter @tanstack/db-sqlite-persistence-core test -- --maxWorkers=2
TANSTACK_DB_OP_SQLITE_ORACLE_SEED=165903 TANSTACK_DB_OP_SQLITE_ORACLE_PATH=0:0:0 TANSTACK_DB_OP_SQLITE_ORACLE_RUNS=1 pnpm exec vitest --run packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts --maxWorkers=2 -t "preserves generated legal SQL aliases"
pnpm --filter @tanstack/react-native-db-sqlite-persistence build
pnpm --filter @tanstack/db-sqlite-persistence-core build

Verified locally with 112/112 React Native persistence tests and 96/96 SQLite core tests passing, plus the exact seed/path replay, both package builds and typechecks, all shared SQLite-driver consumers, formatting, lint (zero errors), diff, and cleanup checks.

Files changed

  • packages/react-native-db-sqlite-persistence/src/op-sqlite-driver.ts — strictly classifies and decodes supported op-sqlite result envelopes.
  • packages/react-native-db-sqlite-persistence/tests/helpers/op-sqlite-test-db.ts — adds a v14-style executeAsync columnar fixture.
  • packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts — adds fixed witnesses, generated alias histories, malformed-envelope controls, replay, and cleanup assertions.
  • packages/react-native-db-sqlite-persistence/tests/react-native-persistence.test.ts — verifies pre-populated registry reuse and exact row/stream restoration after close and reopen.
  • packages/db-sqlite-persistence-core/tests/contracts/sqlite-driver-contract.ts — adds exact write-then-read and reserved-alias laws across shared drivers.
  • .changeset/fix-op-sqlite-result-decoding.md — records the React Native persistence patch.

Fixes #1499

Part of #1659

Summary by CodeRabbit

  • Bug Fixes
    • Improved SQLite result handling to preserve rows, column order, aliases, write-result fields, and insert IDs accurately.
    • Added validation for malformed, ambiguous, unsupported, or unknown result formats.
    • Added support for columnar query results and nested result envelopes.
    • Preserved persisted data and stream positions across database close-and-reopen cycles.
    • Improved error reporting when both an operation and its cleanup fail.

@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: c3e30ee6-a12a-4107-ae19-d9ba792149b1

📥 Commits

Reviewing files that changed from the base of the PR and between 6a79dcd and 3afe2df.

📒 Files selected for processing (2)
  • packages/db-sqlite-persistence-core/tests/contracts/sqlite-driver-contract.ts
  • packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/db-sqlite-persistence-core/tests/contracts/sqlite-driver-contract.ts
  • packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The driver now decodes op-sqlite columnar executeAsync results, validates result envelopes, and rejects malformed shapes. Tests cover exact rows, aliases, cleanup errors, shared driver contracts, and persistence after restart.

Changes

Op-SQLite result decoding

Layer / File(s) Summary
Validated result decoding
packages/react-native-db-sqlite-persistence/src/op-sqlite-driver.ts, .changeset/fix-op-sqlite-result-decoding.md
The driver decodes rawRows with columnNames, validates result carriers, and rejects malformed or unsupported envelopes.
Columnar fixtures and driver contracts
packages/react-native-db-sqlite-persistence/tests/helpers/op-sqlite-test-db.ts, packages/db-sqlite-persistence-core/tests/contracts/sqlite-driver-contract.ts
Test databases expose columnar executeAsync results. Contract tests verify exact rows, aliases, and preserved cleanup failures.
Op-SQLite decoding validation tests
packages/react-native-db-sqlite-persistence/tests/op-sqlite-driver.test.ts
Tests cover reordered rows, reserved aliases, empty results, malformed envelopes, nested wrappers, property-based checks, and the shared columnar driver contract.
Restart persistence verification
packages/react-native-db-sqlite-persistence/tests/react-native-persistence.test.ts
The restart test verifies collection registry data, stream position, cleanup handling, and row recovery after reopening with columnar executeAsync results.

Priority: ⬆️ High

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: High

Sequence Diagram(s)

sequenceDiagram
  participant Persistence
  participant OpSQLiteDriver
  participant executeAsync
  participant SQLiteDatabase
  Persistence->>OpSQLiteDriver: query collection_registry
  OpSQLiteDriver->>executeAsync: execute SQL
  executeAsync->>SQLiteDatabase: read registry rows
  SQLiteDatabase-->>executeAsync: rawRows and columnNames
  executeAsync-->>OpSQLiteDriver: columnar result envelope
  OpSQLiteDriver-->>Persistence: decoded registry row
  Persistence->>OpSQLiteDriver: load persisted subset
  OpSQLiteDriver-->>Persistence: decoded persisted rows
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: lossless decoding of op-sqlite results in the React Native adapter.
Description check ✅ Passed The description is comprehensive. It explains the root cause, implementation, invariants, non-goals, verification commands, test results, changed files, issue linkage, and changeset. It does not use t…
Linked Issues check ✅ Passed Issue [#1499] requires decoding op-sqlite v14 { rawRows, columnNames } SELECT results into object rows. The PR adds columnar decoding, strict envelope validation, and explicit errors for malformed o…
Out of Scope Changes check ✅ Passed The changes remain within [#1499]. The fixture models the reported executeAsync result shape. Contract tests, cleanup handling, malformed-result tests, and restart coverage support verification of t…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Sep 18, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1848

@tanstack/browser-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/browser-db-sqlite-persistence@1848

@tanstack/capacitor-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/capacitor-db-sqlite-persistence@1848

@tanstack/cloudflare-durable-objects-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/cloudflare-durable-objects-db-sqlite-persistence@1848

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1848

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1848

@tanstack/db-sqlite-persistence-core

npm i https://pkg.pr.new/@tanstack/db-sqlite-persistence-core@1848

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1848

@tanstack/electron-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/electron-db-sqlite-persistence@1848

@tanstack/expo-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/expo-db-sqlite-persistence@1848

@tanstack/node-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/node-db-sqlite-persistence@1848

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1848

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1848

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1848

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1848

@tanstack/react-native-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/react-native-db-sqlite-persistence@1848

@tanstack/react-router-with-db

npm i https://pkg.pr.new/@tanstack/react-router-with-db@1848

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1848

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1848

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1848

@tanstack/tauri-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/tauri-db-sqlite-persistence@1848

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1848

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1848

commit: 3afe2df

@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 165 kB

ℹ️ View Unchanged
Filename Size
packages/db/dist/esm/client.js 3.66 kB
packages/db/dist/esm/collection-options.js 236 B
packages/db/dist/esm/collection/change-events.js 1.44 kB
packages/db/dist/esm/collection/changes.js 2.25 kB
packages/db/dist/esm/collection/cleanup-queue.js 794 B
packages/db/dist/esm/collection/events.js 481 B
packages/db/dist/esm/collection/index.js 4.63 kB
packages/db/dist/esm/collection/indexes.js 1.99 kB
packages/db/dist/esm/collection/lifecycle.js 2.15 kB
packages/db/dist/esm/collection/mutations.js 2.61 kB
packages/db/dist/esm/collection/state.js 6.51 kB
packages/db/dist/esm/collection/subscription.js 8.72 kB
packages/db/dist/esm/collection/sync.js 4.62 kB
packages/db/dist/esm/collection/transaction-metadata.js 144 B
packages/db/dist/esm/deferred.js 207 B
packages/db/dist/esm/errors.js 5.26 kB
packages/db/dist/esm/event-emitter.js 964 B
packages/db/dist/esm/index.js 3.71 kB
packages/db/dist/esm/indexes/auto-index.js 829 B
packages/db/dist/esm/indexes/base-index.js 1.14 kB
packages/db/dist/esm/indexes/basic-index.js 2.07 kB
packages/db/dist/esm/indexes/btree-index.js 2.26 kB
packages/db/dist/esm/indexes/index-registry.js 820 B
packages/db/dist/esm/indexes/reverse-index.js 376 B
packages/db/dist/esm/live-query-adapter.js 318 B
packages/db/dist/esm/live-query-observer.js 3.69 kB
packages/db/dist/esm/live-query-options.js 702 B
packages/db/dist/esm/live-query-window-controller.js 4.36 kB
packages/db/dist/esm/local-only.js 989 B
packages/db/dist/esm/local-storage.js 2.17 kB
packages/db/dist/esm/optimistic-action.js 359 B
packages/db/dist/esm/paced-mutations.js 496 B
packages/db/dist/esm/proxy.js 3.32 kB
packages/db/dist/esm/query/builder/functions.js 1.47 kB
packages/db/dist/esm/query/builder/index.js 6.69 kB
packages/db/dist/esm/query/builder/query-ir.js 116 B
packages/db/dist/esm/query/builder/ref-proxy.js 1.24 kB
packages/db/dist/esm/query/compiler/evaluators.js 1.92 kB
packages/db/dist/esm/query/compiler/expressions.js 560 B
packages/db/dist/esm/query/compiler/group-by.js 4.13 kB
packages/db/dist/esm/query/compiler/index.js 9.06 kB
packages/db/dist/esm/query/compiler/joins.js 2.95 kB
packages/db/dist/esm/query/compiler/lazy-targets.js 1.1 kB
packages/db/dist/esm/query/compiler/order-by.js 1.91 kB
packages/db/dist/esm/query/compiler/parent-routes.js 319 B
packages/db/dist/esm/query/compiler/route-metadata.js 1.24 kB
packages/db/dist/esm/query/compiler/select.js 1.58 kB
packages/db/dist/esm/query/effect.js 4.6 kB
packages/db/dist/esm/query/equality-value-identity.js 591 B
packages/db/dist/esm/query/expression-helpers.js 1.43 kB
packages/db/dist/esm/query/ir-stable-identity.js 4.04 kB
packages/db/dist/esm/query/ir.js 1.59 kB
packages/db/dist/esm/query/live-query-collection.js 391 B
packages/db/dist/esm/query/live/bucket-facade-adapter.js 2.73 kB
packages/db/dist/esm/query/live/collection-config-builder.js 6.97 kB
packages/db/dist/esm/query/live/collection-registry.js 264 B
packages/db/dist/esm/query/live/collection-subscriber.js 2.25 kB
packages/db/dist/esm/query/live/internal.js 145 B
packages/db/dist/esm/query/live/materialized-pipeline.js 2.32 kB
packages/db/dist/esm/query/live/ordered-source-loader.js 3.14 kB
packages/db/dist/esm/query/live/subset-demand-controller.js 1.26 kB
packages/db/dist/esm/query/live/utils.js 1.14 kB
packages/db/dist/esm/query/optimizer.js 2.91 kB
packages/db/dist/esm/query/query-once.js 359 B
packages/db/dist/esm/query/runtime-reference-identity.js 572 B
packages/db/dist/esm/query/subset-dedupe.js 486 B
packages/db/dist/esm/scheduler.js 1.34 kB
packages/db/dist/esm/SortedMap.js 1.3 kB
packages/db/dist/esm/strategies/debounceStrategy.js 247 B
packages/db/dist/esm/strategies/queueStrategy.js 428 B
packages/db/dist/esm/strategies/throttleStrategy.js 246 B
packages/db/dist/esm/transactions.js 3.71 kB
packages/db/dist/esm/utils.js 1.08 kB
packages/db/dist/esm/utils/array-utils.js 270 B
packages/db/dist/esm/utils/browser-polyfills.js 304 B
packages/db/dist/esm/utils/btree.js 4.51 kB
packages/db/dist/esm/utils/callbacks.js 174 B
packages/db/dist/esm/utils/comparison.js 1.49 kB
packages/db/dist/esm/utils/cursor.js 676 B
packages/db/dist/esm/utils/error.js 167 B
packages/db/dist/esm/utils/get-or-create.js 155 B
packages/db/dist/esm/utils/index-optimization.js 2.42 kB
packages/db/dist/esm/utils/type-guards.js 230 B
packages/db/dist/esm/utils/uuid.js 449 B
packages/db/dist/esm/virtual-props.js 360 B

compressed-size-action::db-package-size

@github-actions

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 7.34 kB

ℹ️ View Unchanged
Filename Size
packages/react-db/dist/esm/DbProvider.js 317 B
packages/react-db/dist/esm/HydrationBoundary.js 263 B
packages/react-db/dist/esm/index.js 330 B
packages/react-db/dist/esm/live-query-internals.js 282 B
packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.9 kB
packages/react-db/dist/esm/useLiveQuery.js 2.68 kB
packages/react-db/dist/esm/useLiveQueryEffect.js 355 B
packages/react-db/dist/esm/useLiveSuspenseQuery.js 812 B
packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpSQLiteDriver: executeAsync result format not handled by extractRowsFromStatementResult, causing silent data loss on SELECT queries

1 participant