Skip to content

fix(db): support optional nested single-row refs - #1850

Merged
KyleAMathews merged 3 commits into
mainfrom
codex/ts-cluster-07-query-refs
Sep 18, 2026
Merged

KyleAMathews merged 3 commits into
mainfrom
codex/ts-cluster-07-query-refs

Conversation

@KyleAMathews

@KyleAMathews KyleAMathews commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

SingleRowRefProxy now lets collection index and change-filter callbacks traverse optional or nullable nested plain objects with schema-required guards. Dates, arrays, Maps, functions, and other built-ins remain scalar query leaves instead of exposing nonsensical method/property paths.

Root cause

The mapped proxy checked T[K] extends Record<string, any> before separating nullish members. An optional object such as Timestamp | undefined therefore collapsed to an opaque leaf, even though the runtime proxy records timestamp.seconds correctly.

Normalizing with NonNullable fixed that case but made the Record check too permissive: built-ins such as Date | null became recursive proxies. That disagreed with the query builder's established plain-object traversal rule and allowed paths such as updatedAt.getTime to typecheck.

Approach

  • Normalize each field with NonNullable<V> before classifying it.
  • Reuse the query builder's IsPlainObject classifier so all query refs share one leaf/traversal boundary.
  • Restore declared null | undefined members outside nested proxies so TypeScript still requires a guard.
  • Keep exact null and exact undefined fields as leaves with a non-distributive never guard.
  • Cover the behavior with a declaration-test matrix and paired runtime path witnesses.

Key invariants

  • Optional and nullable plain objects expose declared nested fields only after a guard.
  • Dates, arrays, Maps, functions, and other built-ins remain leaves for required, optional, and nullable fields.
  • Exact-nullish and mixed plain-object/built-in fields stay opaque.
  • Generic-constrained rows and all public callback owners use the same projection.
  • The runtime proxy records valid nested plain-object paths; bypassing the type boundary demonstrates why built-in paths must be rejected statically.

Non-goals

Trade-offs

Exporting and reusing the internal IsPlainObject type adds a small shared dependency between the two query-ref implementations. It avoids duplicating a subtle classifier and preserves the leaf behavior established for Dates and other built-ins.

Verification

node_modules/.bin/tsc -p packages/db/tsconfig.json --pretty false
pnpm --filter @tanstack/db test
pnpm build
  • Exact reviewed-head RED: four declaration-test cells failed while the runtime path witness passed.
  • Focused declaration matrix and the fix(db): preserve nullable and generic query refs #1843 query-ref algebra suite pass after the fix.
  • Full @tanstack/db suite: 206 files, 6,257 tests, no type errors.
  • Full workspace build passes.
  • Hostile controls reject the legacy Record classifier, an all-objects classifier, and removal of the exact-nullish guard.

Files changed

  • packages/db/src/query/builder/ref-proxy.ts: classify normalized fields with the shared plain-object rule.
  • packages/db/src/query/builder/types.ts: export the existing classifier for internal reuse.
  • packages/db/tests/single-row-ref-proxy.test-d.ts: cover nullish plain objects, built-in leaves, unions, generics, expression consumers, and public callback paths.
  • packages/db/tests/query/builder/ref-proxy.test.ts: verify valid nested paths and the forced built-in-path failure mode.
  • .changeset/fix-single-row-optional-ref-proxy.md: document both optional traversal and preserved built-in leaves.

Fixes #1726

Summary by CodeRabbit

  • Bug Fixes
    • Fixed collection indexes and change filters so optional and nullable nested objects can be safely traversed with optional chaining.
    • Preserved correct query behavior for built-in values such as dates, arrays, maps, and functions.
  • Tests
    • Added coverage for nested optional fields and compile-time type behavior across nullable, built-in, union, and scalar values.

@coderabbitai

coderabbitai Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 62a9003e-ff3c-48f1-8c6f-ec3a8fcd9a93

📥 Commits

Reviewing files that changed from the base of the PR and between 65b0f23 and bf3b78d.

📒 Files selected for processing (5)
  • .changeset/fix-single-row-optional-ref-proxy.md
  • packages/db/src/query/builder/ref-proxy.ts
  • packages/db/src/query/builder/types.ts
  • packages/db/tests/query/builder/ref-proxy.test.ts
  • packages/db/tests/single-row-ref-proxy.test-d.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/fix-single-row-optional-ref-proxy.md

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


📝 Walkthrough

Walkthrough

The pull request updates SingleRowRefProxy to traverse optional and nullable nested plain objects. It preserves built-in values and functions as leaves. Runtime tests, type tests, and a patch changeset document the change.

Changes

Optional reference proxy

Layer / File(s) Summary
Nullable proxy type projection
packages/db/src/query/builder/ref-proxy.ts, packages/db/src/query/builder/types.ts
SingleRowField uses IsPlainObject to distinguish traversable objects from built-in values and functions. Nullable plain-object fields retain their nullish members.
Runtime and type-level validation
packages/db/tests/query/builder/ref-proxy.test.ts, packages/db/tests/single-row-ref-proxy.test-d.ts
Tests verify optional nested paths and type projections for built-ins, functions, unions, nullish fields, expression helpers, and constrained generics.
Release metadata
.changeset/fix-single-row-optional-ref-proxy.md
The changeset declares a patch release for @tanstack/db and describes the fix.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: kevin-dp

Merge Risk: ⚪ Minimal · up to bf3b7

The optional nested-reference behavior is covered without an identified remaining regression risk.

🚥 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 2 functions across 4 files. (1 skipped: 1 … 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: support for optional nested single-row references in the database package.
Description check ✅ Passed The description is detailed and covers the change, motivation, approach, testing, release impact, scope, and changed files. It does not use the exact template headings or checklist boxes, but it provi…
Linked Issues check ✅ Passed Issue #1726 requires nested access for optional and nullable plain-object fields in SingleRowRefProxy callbacks. SingleRowField evaluates IsPlainObject<NonNullable<V>>, creates a nested proxy fo…
Out of Scope Changes check ✅ Passed The source change directly implements issue #1726. The added declaration and runtime tests verify the required projection and path behavior. The changeset documents the package fix. The reviewed chang…
Full details: Docstring Coverage

Explanation

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 2 functions across 4 files. (1 skipped: 1 unsupported.)

  • 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@1850

@tanstack/browser-db-sqlite-persistence

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

@tanstack/capacitor-db-sqlite-persistence

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

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

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/db-sqlite-persistence-core

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

@tanstack/electric-db-collection

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

@tanstack/electron-db-sqlite-persistence

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

@tanstack/expo-db-sqlite-persistence

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

@tanstack/node-db-sqlite-persistence

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

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

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

@tanstack/react-router-with-db

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/tauri-db-sqlite-persistence

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: bf3b78d

@github-actions

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.62 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

@KyleAMathews
KyleAMathews merged commit fc1adde into main Sep 18, 2026
11 checks passed
@KyleAMathews
KyleAMathews deleted the codex/ts-cluster-07-query-refs branch September 18, 2026 21:10
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.

createIndex() row proxy does not support nested optional property access

1 participant