Skip to content

feat(query-db-collection): support runtime QueryClient binding#1663

Closed
KyleAMathews wants to merge 3 commits into
mainfrom
query-runtime-binding
Closed

feat(query-db-collection): support runtime QueryClient binding#1663
KyleAMathews wants to merge 3 commits into
mainfrom
query-runtime-binding

Conversation

@KyleAMathews

@KyleAMathews KyleAMathews commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Adds defineQueryCollectionOptions so query collection definitions can be created without a QueryClient and bound later with a request-local/runtime client. This keeps the existing queryCollectionOptions({ queryClient, ... }) API working while supporting SSR/TanStack Start-style per-request clients.

Approach

  • Introduce defineQueryCollectionOptions(config) as an additive helper for unbound query collection definitions.
  • Return a small definition object with bind({ queryClient }), which delegates to the existing queryCollectionOptions({ ...config, queryClient }) lifecycle path.
  • Type the definition config so queryClient is rejected at definition time and must be supplied during binding.
  • Export the new helper and document request-local usage in the query collection docs.

Key invariants

  • No global QueryClient is introduced.
  • Existing direct queryCollectionOptions({ queryClient, ... }) usage remains supported.
  • A bound collection uses the runtime QueryClient passed to .bind(...).
  • Query option inference for queryFn, getKey, and schema usage is preserved.

Non-goals

  • This does not rewrite query collection lifecycle internals.
  • This does not add a process-wide or implicit default QueryClient.
  • This does not change existing direct-binding behavior.

Trade-offs

The new helper is intentionally a thin wrapper over queryCollectionOptions rather than a separate runtime path. That keeps the API small and avoids duplicating query lifecycle behavior, while still separating definition-time configuration from runtime QueryClient binding.

Verification

pnpm --filter @tanstack/db-ivm build
pnpm --filter @tanstack/electric-db-collection build
pnpm --filter @tanstack/db build
pnpm --filter @tanstack/query-db-collection test tests/query.test.ts -t "bind|QueryClient|beforeLoad|runtime"
pnpm --filter @tanstack/query-db-collection test -- query.test-d.ts query.test.ts
pnpm --filter @tanstack/query-db-collection test tests/query.test.ts

Files changed

  • packages/query-db-collection/src/query.ts — adds the runtime binding helper and types.
  • packages/query-db-collection/src/index.ts — exports the new helper/types.
  • packages/query-db-collection/tests/query.test.ts — covers runtime binding and separate QueryClient instances.
  • packages/query-db-collection/tests/query.test-d.ts — covers inference and rejects queryClient at definition time.
  • docs/collections/query-collection.md — documents request-local/runtime binding usage.
  • .changeset/runtime-query-binding.md — records the package patch change.

Related RFC: #1643

Addresses #436

Summary by CodeRabbit

  • New Features
    • Added defineQueryCollectionOptions for defining query collections without an immediately available QueryClient.
    • Bind a request-specific client later when creating the collection, supporting SSR and request-scoped usage.
    • Preserved type inference across schemas, query functions, and selection mappings.
    • Runtime bindings remain isolated, preventing query data from leaking between clients.
  • Documentation
    • Added guidance and examples for runtime QueryClient binding.
  • Tests
    • Added coverage for runtime binding, type safety, initial synchronization, and client isolation.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds defineQueryCollectionOptions for defining query collections without an initial QueryClient, binding a client later through .bind({ queryClient }). The API is exported, typed, tested for runtime isolation and inference, documented, and recorded in a patch changeset.

Changes

Runtime QueryClient Binding

Layer / File(s) Summary
Define and bind query collection options
packages/query-db-collection/src/query.ts, packages/query-db-collection/src/index.ts
Adds typed schema and schema-less overloads, forbids direct queryClient configuration, binds clients at runtime, and exports the new API and type.
Runtime and type validation
packages/query-db-collection/tests/query.test.ts, packages/query-db-collection/tests/query.test-d.ts
Verifies initial fetching, per-client cache isolation, type preservation, and compile-time rejection of direct client configuration.
Documentation and release metadata
docs/collections/query-collection.md, .changeset/runtime-query-binding.md
Documents request-scoped binding and adds a patch release note.

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

Suggested reviewers: kevin-dp

Sequence Diagram(s)

sequenceDiagram
  participant Application
  participant defineQueryCollectionOptions
  participant bind
  participant queryCollectionOptions
  participant QueryClient
  Application->>defineQueryCollectionOptions: Define collection config
  Application->>bind: bind({ queryClient })
  bind->>queryCollectionOptions: Add the runtime QueryClient
  queryCollectionOptions->>QueryClient: Create and fetch the collection
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main change: adding runtime QueryClient binding for query collections.
Description check ✅ Passed The description covers the change, approach, invariants, tests, and files, but it does not follow the template headings or checklist format.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch query-runtime-binding

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 Jul 10, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-db

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

@tanstack/browser-db-sqlite-persistence

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

@tanstack/capacitor-db-sqlite-persistence

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

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

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/db-sqlite-persistence-core

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

@tanstack/electric-db-collection

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

@tanstack/electron-db-sqlite-persistence

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

@tanstack/expo-db-sqlite-persistence

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

@tanstack/node-db-sqlite-persistence

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

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

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/tauri-db-sqlite-persistence

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: c7ccbea

@github-actions

Copy link
Copy Markdown
Contributor

Size Change: 0 B

Total Size: 125 kB

ℹ️ View Unchanged
Filename Size
packages/db/dist/esm/collection/change-events.js 1.43 kB
packages/db/dist/esm/collection/changes.js 1.38 kB
packages/db/dist/esm/collection/cleanup-queue.js 810 B
packages/db/dist/esm/collection/events.js 434 B
packages/db/dist/esm/collection/index.js 3.62 kB
packages/db/dist/esm/collection/indexes.js 1.99 kB
packages/db/dist/esm/collection/lifecycle.js 1.69 kB
packages/db/dist/esm/collection/mutations.js 2.47 kB
packages/db/dist/esm/collection/state.js 5.48 kB
packages/db/dist/esm/collection/subscription.js 3.74 kB
packages/db/dist/esm/collection/sync.js 2.88 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.1 kB
packages/db/dist/esm/event-emitter.js 748 B
packages/db/dist/esm/index.js 3.16 kB
packages/db/dist/esm/indexes/auto-index.js 829 B
packages/db/dist/esm/indexes/base-index.js 767 B
packages/db/dist/esm/indexes/basic-index.js 2.06 kB
packages/db/dist/esm/indexes/btree-index.js 2.19 kB
packages/db/dist/esm/indexes/index-registry.js 820 B
packages/db/dist/esm/indexes/reverse-index.js 557 B
packages/db/dist/esm/live-query-adapter.js 318 B
packages/db/dist/esm/local-only.js 916 B
packages/db/dist/esm/local-storage.js 2.12 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.75 kB
packages/db/dist/esm/query/builder/functions.js 1.47 kB
packages/db/dist/esm/query/builder/index.js 5.84 kB
packages/db/dist/esm/query/builder/ref-proxy.js 1.24 kB
packages/db/dist/esm/query/compiler/evaluators.js 1.89 kB
packages/db/dist/esm/query/compiler/expressions.js 430 B
packages/db/dist/esm/query/compiler/group-by.js 3.56 kB
packages/db/dist/esm/query/compiler/index.js 6.67 kB
packages/db/dist/esm/query/compiler/joins.js 2.5 kB
packages/db/dist/esm/query/compiler/lazy-targets.js 923 B
packages/db/dist/esm/query/compiler/order-by.js 1.74 kB
packages/db/dist/esm/query/compiler/select.js 1.53 kB
packages/db/dist/esm/query/effect.js 4.77 kB
packages/db/dist/esm/query/expression-helpers.js 1.43 kB
packages/db/dist/esm/query/ir.js 1.25 kB
packages/db/dist/esm/query/live-query-collection.js 360 B
packages/db/dist/esm/query/live/collection-config-builder.js 9.1 kB
packages/db/dist/esm/query/live/collection-registry.js 264 B
packages/db/dist/esm/query/live/collection-subscriber.js 1.93 kB
packages/db/dist/esm/query/live/internal.js 145 B
packages/db/dist/esm/query/live/utils.js 1.81 kB
packages/db/dist/esm/query/optimizer.js 2.92 kB
packages/db/dist/esm/query/predicate-utils.js 2.97 kB
packages/db/dist/esm/query/query-once.js 359 B
packages/db/dist/esm/query/subset-dedupe.js 960 B
packages/db/dist/esm/scheduler.js 1.3 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.04 kB
packages/db/dist/esm/utils.js 927 B
packages/db/dist/esm/utils/array-utils.js 273 B
packages/db/dist/esm/utils/browser-polyfills.js 304 B
packages/db/dist/esm/utils/btree.js 5.61 kB
packages/db/dist/esm/utils/comparison.js 1.11 kB
packages/db/dist/esm/utils/cursor.js 457 B
packages/db/dist/esm/utils/index-optimization.js 2.39 kB
packages/db/dist/esm/utils/type-guards.js 157 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: 4.22 kB

ℹ️ View Unchanged
Filename Size
packages/react-db/dist/esm/index.js 249 B
packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.32 kB
packages/react-db/dist/esm/useLiveQuery.js 1.33 kB
packages/react-db/dist/esm/useLiveQueryEffect.js 355 B
packages/react-db/dist/esm/useLiveSuspenseQuery.js 567 B
packages/react-db/dist/esm/usePacedMutations.js 401 B

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/query-db-collection/src/query.ts (1)

362-365: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider guarding against mutation of the shared config reference.

DefinedQueryCollectionOptions.config exposes the raw config object by reference, and bind() spreads it fresh on every call ({ ...config, queryClient }). If a consumer mutates definition.config (or a nested object/array within it) between bind() calls — intentionally or by accident — every subsequent binding silently picks up the mutation, which can be surprising in multi-tenant/request-scoped usage (the exact scenario this API targets).

Consider freezing the config before returning it, or exposing it via a getter/clone instead of a raw mutable reference, to keep the definition immutable once created.

🛡️ Proposed defensive fix
   return {
-    config,
+    config: Object.freeze(config),
     bind: ({ queryClient }) =>
       queryCollectionOptions({
         ...config,
         queryClient,
       }),
   }

As per coding guidelines, "Encapsulate implementation details within responsible classes; use delegation to maintain clean boundaries between components" and "Avoid exposing internal properties directly; instead add public methods that delegate to internal implementations."

Also applies to: 540-547

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/query-db-collection/src/query.ts` around lines 362 - 365, Make
DefinedQueryCollectionOptions.config immutable rather than exposing the shared
mutable reference. Update the configuration creation and binding logic
associated with DefinedQueryCollectionOptions and bind() to return or use a
defensive deep clone/frozen snapshot, including nested objects and arrays, so
mutations to definition.config cannot affect later bindings.

Source: Coding guidelines

packages/query-db-collection/tests/query.test.ts (1)

191-246: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for repeated/concurrent bind() calls on the same definition.

Current tests cover a single bind with sync, and isolation across two distinct QueryClients — good baseline. Missing: calling bind() twice on the same QueryClient/definition (to confirm no shared mutable state leaks between the two resulting collections), and a case exercising bind happening while the first fetch is still in flight.

As per coding guidelines, "/*.test.{ts,tsx,js}: Test corner cases including... async race conditions."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/query-db-collection/tests/query.test.ts` around lines 191 - 246, Add
tests covering repeated and concurrent bind calls on the same definition: create
two collections from one definition bound to the same QueryClient and verify
their state does not leak or share mutable collection state, then invoke a
second bind while the first query fetch remains pending and assert both
collections initialize correctly after the fetch resolves. Place these cases
alongside the existing binding tests and use deferred promises or equivalent
synchronization to exercise the in-flight race.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/query-db-collection/src/query.ts`:
- Around line 362-365: Make DefinedQueryCollectionOptions.config immutable
rather than exposing the shared mutable reference. Update the configuration
creation and binding logic associated with DefinedQueryCollectionOptions and
bind() to return or use a defensive deep clone/frozen snapshot, including nested
objects and arrays, so mutations to definition.config cannot affect later
bindings.

In `@packages/query-db-collection/tests/query.test.ts`:
- Around line 191-246: Add tests covering repeated and concurrent bind calls on
the same definition: create two collections from one definition bound to the
same QueryClient and verify their state does not leak or share mutable
collection state, then invoke a second bind while the first query fetch remains
pending and assert both collections initialize correctly after the fetch
resolves. Place these cases alongside the existing binding tests and use
deferred promises or equivalent synchronization to exercise the in-flight race.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 484448fe-fbff-4bd8-834f-654b6ecab88e

📥 Commits

Reviewing files that changed from the base of the PR and between eabcea7 and c7ccbea.

📒 Files selected for processing (6)
  • .changeset/runtime-query-binding.md
  • docs/collections/query-collection.md
  • packages/query-db-collection/src/index.ts
  • packages/query-db-collection/src/query.ts
  • packages/query-db-collection/tests/query.test-d.ts
  • packages/query-db-collection/tests/query.test.ts

@KyleAMathews

Copy link
Copy Markdown
Collaborator Author

Closing this implementation direction for now. The API shape feels like unnecessary sugar; we'll first document the request-local QueryClient factory pattern and only add API if the docs-only approach proves insufficient.

@KyleAMathews

Copy link
Copy Markdown
Collaborator Author

Follow-up: closed this implementation direction because the bind-style API feels like unnecessary sugar at this point. We'll first document the plain request-local QueryClient factory pattern and only add API if that proves insufficient.

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.

1 participant