Skip to content

feat(PowerSync): add attachments support#9

Open
Chriztiaan wants to merge 60 commits into
mainfrom
feat/powersync-attachments
Open

feat(PowerSync): add attachments support#9
Chriztiaan wants to merge 60 commits into
mainfrom
feat/powersync-attachments

Conversation

@Chriztiaan

@Chriztiaan Chriztiaan commented Jun 18, 2026

Copy link
Copy Markdown

Derived from Steven's efforts in powersync-ja/powersync-js#983, and addresses TanStack#1563.

Problem

PowerSync ships an attachment helper for syncing files (photos, documents) between local and remote storage. It's separate from regular synced tables: a local-only attachments table tracks each file's lifecycle (QUEUED_UPLOAD, SYNCED, QUEUED_DELETE), and an AttachmentQueue drives uploads/downloads in the background.

TanStackDB, on the other hand, gives you an optimistic, reactive, joinable view over synced data. For users who want to use the attachment helper alongside the PowerSync+TanstackDB integration there are blockers. Saving a file (in the local-only attachments table) and associating it with a record (e.g. setting user.photo_id) are two independent writes which could make data races and fatal errors a problem for data consistency.

The original POC (powersync-js#983) proved this integration was viable. This PR productionises a a subset of it as reusable functionality.

Solution

A TanStackDBAttachmentQueue that extends the SDK's AttachmentQueue (for saving and deleting a file) and backs it with a TanStack DB collection.

The package owns the collection-backed saveFile/delete implementation and leaves the wiring to the application (covered in documentation).

Future Work

After this has been released, we can merge the changes made to the PowerSync JS TanstackDB demo.

AI Disclosure

I used Claude Opus to help investigate, implement, and verify this work.

kevin-dp and others added 7 commits June 17, 2026 14:42
…#1593)

* fix(db): use safe randomUUID helper for non-secure browser contexts (TanStack#1541)

* fix(db-sqlite-persistence-core): use safe randomUUID helper (TanStack#1541)

* fix(browser-db-sqlite-persistence): use safe randomUUID helper (TanStack#1541)

* fix(electron-db-sqlite-persistence): use safe randomUUID helper (TanStack#1541)

* fix(offline-transactions): use safe randomUUID helper (TanStack#1541)

* ci: apply automated fixes

* refactor: rename randomUUID helper to safeRandomUUID and add crypto-undefined test

---------

Co-authored-by: Kevin De Porre <kevin-dp@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…ck#1151)

* feat(db): add subtract, multiply, divide math functions

Add missing math functions that were implemented in evaluators but not
exported. These enable computed columns in orderBy for ranking algorithms
like HN-style scoring that balances recency and rating.

- Add subtract(a, b) function
- Add multiply(a, b) function
- Add divide(a, b) function (with null on divide-by-zero)
- Export from query/index.ts
- Add to operators list
- Add comprehensive tests including orderBy usage

* docs: document subtract, multiply, divide math functions

- Add documentation for new math functions in live-queries.md
- Include example of computed columns in orderBy for ranking algorithms
- Add changeset for the new minor feature

* ci: apply automated fixes

* fix: align math function return types

* ci: apply automated fixes

* docs: clarify ranking snapshot semantics

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@Chriztiaan
Chriztiaan marked this pull request as ready for review June 23, 2026 12:02
@Chriztiaan Chriztiaan changed the title feat: add attachments support feat(PowerSync): add attachments support Jun 23, 2026

@stevensJourney stevensJourney left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Overall I'm happy with the approach here. Left a few comments, mostly nits.

Comment thread packages/powersync-db-collection/src/attachments.ts Outdated
Comment thread packages/powersync-db-collection/src/attachments.ts Outdated
Comment thread docs/collections/powersync-collection.md Outdated
kevin-dp and others added 3 commits June 23, 2026 10:58
* test: cover re-preloading a live query after cleanup

Add a regression test asserting that a live query loads its data again
when it is preloaded after the live query and its source collection were
cleaned up (e.g. when switching data sets at runtime).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: reset live query error state on sync restart so preload recovers after cleanup (TanStack#1576)

A source collection that is cleaned up while a live query depends on it
pushes the live query into an error state and latches `isInErrorState`.
That flag was never reset, so when sync restarted via preload() after
cleanup (e.g. switching profiles without a page refresh),
updateLiveQueryStatus() returned early, markReady() was never called and
the preload promise hung forever.

Reset `isInErrorState` at the start of each sync session so the live
query can become ready again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@autofix-troubleshooter

Copy link
Copy Markdown

Hi! I'm the autofix logoautofix.ci troubleshooter bot.

It looks like you correctly set up a CI job that uses the autofix.ci GitHub Action, but the autofix.ci GitHub App has not been installed for this repository. This means that autofix.ci unfortunately does not have the permissions to fix this pull request. If you are the repository owner, please install the app and then restart the CI workflow! 😃

Comment thread packages/powersync-db-collection/src/attachments.ts
stevensJourney
stevensJourney previously approved these changes Jun 24, 2026
github-actions Bot and others added 8 commits June 25, 2026 08:50
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…sage (TanStack#1518)

The README described the package as "Phase 7 single-tab browser wiring",
which suggested multi-tab was not supported. In fact, single-tab is just
the default — passing a `BrowserCollectionCoordinator` via the
`coordinator` option enables multi-tab coordination today.

Drop the internal phase reference, list `BrowserCollectionCoordinator` in
the public API, split the quick start into single-tab and multi-tab
sections, and link to the offline-transactions example for the multi-tab
case.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#1580)

- Add a "materialize" subsection to the Includes guide in live-queries.md
  with array vs singleton (findOne) examples and notes on reactivity and
  expression-context restrictions.
- Add the materialize() entry to docs/reference/index.md. The generated
  functions/materialize.md page is produced by the release workflow's
  generate-docs step, so it is not committed here.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ry + regression test (closes TanStack#1587) (TanStack#1594)

* fix(react-db): defer eager onStoreChange to a microtask in useLiveQuery

Closes TanStack#1587.

`useLiveQuery`'s `subscribeRef` calls `onStoreChange()` synchronously
inside the `useSyncExternalStore` subscribe function when the
underlying collection is already `ready`. That synchronous notification
lands during the render-to-commit window when subscribe runs under
StrictMode double-render or cold/throttled loads, which React surfaces
as:

  Can't perform a React state update on a component that hasn't
  mounted yet. This indicates that you have a side-effect in your
  render function that asynchronously tries to update the component.
  Move this work to useEffect instead.

The fix is to defer the eager notification to a microtask so it lands
after the current commit. While doing so, also guard the late notify
path against an in-flight `subscribeChanges` callback firing after
React unsubscribes — track a local `unsubscribed` flag and drop both
the eager microtask and any in-flight subscription event after teardown,
so React never sees a state update post-unsubscribe.

No public API change; the contract of `useLiveQuery` is preserved (an
already-ready collection still notifies React once after mount, just
asynchronously instead of mid-commit).

Verified `pnpm test` in packages/react-db — 94/94 pass, no type
errors. Existing tests don't cover the race directly (it's a
StrictMode-double-render / cold-load condition observed via Lighthouse
in the issue), so the existing suite is the regression guard for
existing behavior and the issue's repro is the behavioral validation.

* test(react-db): add regression test for useLiveQuery eager onStoreChange (TanStack#1587)

Captures the subscribe callback that useLiveQuery passes to
React.useSyncExternalStore and asserts that onStoreChange is not invoked
synchronously when the collection is already in the 'ready' state — it
is instead deferred to a microtask. Without the fix, the eager notify
lands during the render-to-commit window and React surfaces:

  Can't perform a React state update on a component that hasn't mounted
  yet. ... Move this work to useEffect instead.

* chore(react-db): tighten comments around deferred onStoreChange

* chore(react-db): drop TanStack#1587 reference from comment

* chore(react-db): drop issue refs from eager-onStoreChange test

---------

Co-authored-by: tsushanth <78000697+tsushanth@users.noreply.github.com>
Co-authored-by: Kevin <kevin-dp@users.noreply.github.com>
…ck#1584) (TanStack#1595)

* test: reproduce prototype pollution via select() alias (TanStack#1584)

Adds a failing test demonstrating that .select() alias paths like
`__proto__.polluted` or `constructor.prototype.polluted` are split
on '.' and walked into the result object without sanitization,
allowing prototype pollution through queryOnce().

This commit intentionally fails CI to demonstrate the vulnerability;
the next commit fixes it.

* fix(db): reject unsafe alias path segments in select() compiler

Adds a new `UnsafeAliasPathError` (extends QueryCompilationError) and an
`assertSafeAliasSegments` helper invoked in three places in
packages/db/src/query/compiler/select.ts:

- `addFromObject` validates each non-spread key at compile time, including
  dotted keys, before recording any select operation.
- `processNonMergeOp` validates the split alias path before walking into
  the result object.
- `processMerge` validates `targetPath` for the same reason.

Segments matching `__proto__`, `prototype`, or `constructor` are rejected,
which prevents prototype pollution via aliases like `__proto__.polluted`
or `constructor.prototype.polluted` going through queryOnce() /
createLiveQueryCollection().

Fixes TanStack#1584

* ci: apply automated fixes

* test: address CodeRabbit review on prototype-pollution tests

- Import UnsafeAliasPathError and assert that the rejection is exactly
  that error class instead of a permissive .rejects.toThrow().
- Drop the `({} as any).polluted` pattern in favour of
  Object.prototype.hasOwnProperty.call(Object.prototype, 'polluted'),
  which is type-safe and a more explicit assertion that
  Object.prototype itself was not mutated.

* chore: add changeset for TanStack#1584 fix

* ci: apply automated fixes

* test: move prototype-pollution tests into select.test.ts

Fold the select() alias prototype-pollution cases into the existing
select.test.ts integration suite, reusing its createUsers() fixture,
and drop the standalone select-prototype-pollution.test.ts file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: drop issue number from prototype-pollution describe

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: kevin-dp <kevin-dp@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ves to (TanStack#1614)

* test: lazy-join index warning must not blame an already-indexed collection

Add a failing reproduction: when a subquery used in a JOIN clause selects
its join key from the joined side of the subquery, the outer join key
resolves to a collection that is already indexed. The lazy-join loader
should load through that index, but today it emits a "Join requires an
index" warning naming the already-indexed collection (and falls back to a
full load). Data is still correct via the fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: apply automated fixes

* test: fix BTreeIndex import path and collection typing

Import BTreeIndex from src/indexes/btree-index.js (not collection/index)
and type the collections via factory-wrapper ReturnType so the test passes
typecheck. The index-warning assertion still fails as intended.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(query): drive lazy-join loading through the resolved collection

When a subquery used in a JOIN clause selects its join key from a joined
source rather than its own from clause, followRef traced the index
requirement to the resolved collection while the lazy loader still
subscribed to the subquery's from alias. The two diverged, producing a
misleading "Join requires an index" warning that named an already-indexed
collection and an unnecessary full-load fallback.

followRef now also returns the alias of the source the ref resolves to;
getLazyLoadTargets uses it as the subscription alias (falling back to the
from-clause remapping when the key resolves directly to the from source),
so lazy loading drives through the correct collection's index.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: apply automated fixes

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…anStack#1582)

* test: add failing tests for index-optimized queries mixing indexed and non-indexed conditions

These tests assert the expected results of currentStateAsChanges for
AND/OR where clauses that combine conditions on indexed fields with
conditions that cannot be served by an index. They currently fail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: add failing tests for range query boundary handling

Adds expected-behaviour tests for range conditions:
- compound ranges sharing a boundary value must apply the strictest
  bound regardless of argument order, including for date values
- one-sided compound ranges must return the matching rows
- strict comparisons (gt) on date fields must exclude the boundary row

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: add failing test for compound range query with undefined bound

A compound range condition where one bound is undefined (e.g.
gt(score, undefined) AND lt(score, 90)) must match nothing, since a
comparison against undefined is never true. The index-optimized path
must agree with a full scan. This test currently fails.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: add failing tests for nullish values in indexed eq/in/range queries

A comparison against null/undefined is never true, but BTree indexes
store and return rows with nullish indexed values (they sort as the
smallest key). These tests assert that the index-optimized snapshot
matches a full predicate scan for:
- eq against undefined
- IN with an undefined member
- a range comparison over a field that has rows with undefined values
- an upper-bounded compound range over such a field

They currently fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: add failing tests for locale string range and NaN index queries

Two more cases where an index-optimized snapshot must match a full
predicate scan:
- a string range predicate (e.g. name > 'z') must return a row whose
  value satisfies the JS relational comparison ('ö' > 'z'), even though
  a locale-collated index orders that value differently
- eq and IN against NaN must not match a NaN-valued row, since NaN is
  never equal to itself

They currently fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: add failing tests for range predicates over non-orderable index domains

Three more cases where an index-optimized range query must match a
full predicate scan:
- an array-valued field (the evaluator compares with standard relational
  operators, which differ from the index's recursive array ordering)
- a field indexed with a custom comparator
- a numeric field that also contains a NaN value

They currently fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: add failing tests for ordering values that have no natural order

NaN and invalid Dates have no natural order. They should still get a
consistent, well-defined position (alongside nulls) so that:
- the comparator produces a stable total order;
- ordering a collection by such a field is deterministic;
- a range query on a field that contains such a value can still be
  served by the index rather than falling back to a full scan.

These tests currently fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: enforce all where conditions when index optimization is partial

OR expressions now require every disjunct to be index-optimizable;
otherwise the query falls back to a full scan, since rows matched only
by a non-optimizable disjunct cannot be recovered from index lookups.

AND expressions keep partial index optimization but the optimizer now
reports whether the matching keys are exact. When they are a superset
(some conjuncts could not use an index, or a compound range was
combined with other conditions), currentStateAsChanges re-checks each
candidate row against the full where expression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: apply strictest bound in compound range queries and fix related range edge cases

Compound range conditions sharing a boundary value (gte(x,5) AND
gt(x,5)) now keep the strict bound regardless of argument order. Bound
values are compared with the same comparator the indexes use so dates
and locale strings behave correctly.

Two further issues surfaced by the regression tests:

- One-sided compound ranges passed an explicit undefined bound to
  rangeQuery, which treats present-but-undefined as the undefined
  sentinel and returned an empty result. Bounds are now only passed
  when they exist.

- BTreeIndex's exclusive lower bound check compared the normalized
  indexed value against the raw query value, so gt on date fields
  included the boundary row. It now compares against the normalized
  key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: add failing test for exclusive lower bound without a from bound

rangeQuery with only an upper bound but fromInclusive: false must not
drop the minimum key, as there is no lower bound to exclude against.
This regression was introduced when the exclusive lower-bound check
started comparing against the normalized fromKey (which defaults to
minKey when no from bound is given). This test currently fails.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: re-filter compound range queries that use a null/undefined bound

A comparison against null/undefined is never true, but in an index
those values sort as the smallest key, so an index range query cannot
represent such a bound. Compound range optimization now tracks
selected bounds with explicit hasFromBound/hasToBound flags (separate
from the bound values) and marks the result inexact when any bound
value is null/undefined, so the caller re-filters against the full
expression. The inexactness now also propagates through the AND
combiner, which previously ignored the compound range's exactness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: only exclude exclusive lower bound when a from bound is provided

BTreeIndex.rangeQuery dropped the minimum key when called with
fromInclusive: false but no from bound, because fromKey defaults to
the minimum key and the exclusion check did not verify a lower bound
was actually given. The exclusion is now guarded by hasFrom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: re-filter index results that can include nullish-keyed rows

BTree indexes store and return rows with a null/undefined indexed
value (they sort as the smallest key), but a comparison against
null/undefined is never true. The simple-comparison, IN, and
compound-range optimizers now report such results as inexact so the
caller re-checks candidates against the full expression:

- eq/gt/gte: inexact when the query value is nullish (gt/gte with a
  non-null bound stay exact, since the bound excludes the bottom-sorted
  nullish rows)
- lt/lte: conservatively inexact, as the open lower bound includes
  nullish-keyed rows
- IN: inexact when any listed value is nullish
- compound range: exact only when a non-null lower bound is present to
  exclude the nullish rows

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: apply automated fixes

* fix: avoid locale string range index lookups and re-filter NaN results

Two more index-optimization correctness issues:

- A BTree index orders strings with localeCompare under the default
  'locale' collation, but the WHERE evaluator compares strings with JS
  relational operators (code-point order). For range predicates these
  orders disagree (e.g. 'oe-umlaut' > 'z' is true in JS but sorts before
  'z' under locale), so an index range lookup can omit matching rows -
  which re-filtering cannot recover. Locale-backed string range
  predicates are now left for a full scan (eq/IN use exact equality and
  are unaffected).

- eq/IN against NaN returned isExact: true, but NaN is never equal to
  itself while the index still returns NaN-keyed rows (SameValueZero map
  equality). NaN is now treated like a nullish value for exactness, so
  such results are re-filtered against the full expression.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: only use indexes for range predicates when ordering is trustworthy

Range optimization assumed the index orders values the same way the
WHERE evaluator's relational operators do. That holds for numbers,
booleans, bigints, lexical strings and valid Dates, but not for:

- non-primitive operands (arrays, plain objects, Temporal, invalid
  Dates), which the evaluator compares via string coercion / identity
  while the index compares recursively;
- indexes created with a custom comparator, whose order is opaque;
- fields containing a NaN or invalid Date, which compare equal to every
  value and break the strict-weak-ordering range traversal relies on.

In all three cases an index range lookup can omit genuine matches, which
re-filtering cannot recover, so they now fall back to a full scan. The
index exposes a supportsRangeOptimization capability (false for custom
comparators or when an unorderable value is stored), and the optimizer
additionally checks the operand domain. eq/IN are unaffected (exact
equality, not ordering).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: apply automated fixes

* fix: give NaN and invalid Dates a stable sort position

The comparator returned 0 for NaN against any value (and NaN from
invalid-Date subtraction), so NaN had no consistent order. That made
ordering by a field containing NaN non-deterministic and, worse,
corrupted the strict-weak-ordering that B-tree range traversal relies
on, so a stored NaN could make a range query drop genuinely matching
rows.

ascComparator now places NaN and invalid Dates alongside nulls, giving
a well-defined total order. With a valid order the index traversal is
correct again, so range queries on a field containing such values no
longer deopt to a full scan: NaN simply sorts to the nulls end, where
the existing exactness logic excludes it from lower-bounded ranges and
re-filters it out of open-bottom (lt/lte) ranges. The index capability
therefore only needs to deopt for custom comparators, so the
stored-value check is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: adopt PostgreSQL float semantics for NaN (supersedes TanStack#1617)

Fold the NaN/invalid-Date semantics from the TanStack#1617 proposal into this PR
so the index-optimization correctness work is built on the final, coherent
contract instead of the interim JS-semantics handling.

Previously this branch gave NaN/invalid Dates a stable sort position *for
sorting only* while the WHERE evaluator still rejected them (NaN != NaN),
relying on re-filtering to drop index-returned NaN rows. That left a
JS/SQL hybrid where joins/groupBy/distinct (which match NaN = NaN via the
hash index) disagreed with WHERE/ordering.

Following PostgreSQL, NaN (and invalid Dates, whose timestamp is NaN) is
now equal to itself and greater than every other non-null value:

- comparison.ts: ascComparator orders NaN/invalid Dates as the greatest
  non-null value (was: alongside nulls); isUnorderable is exported.
- evaluators.ts: eq/gt/gte/lt/lte/in implement the same via valuesEqual.
- index-optimization.ts: because the index and evaluator now agree on
  NaN/invalid Dates, they are treated as exact (no re-filter) and invalid
  Dates are no longer range-divergent. This resolves the reviewer's
  invalid-Date eq/IN issue (indexed == full-scan) and simplifies the
  NaN-specific defensiveness down to the remaining nullish cases.

null/undefined are unchanged: still three-valued logic (UNKNOWN).

Tests: new nan-semantics.test.ts (numeric NaN + invalid Date, asserting
indexed == full-scan), PG-semantics comparison.test.ts and evaluator NaN
block; updated the NaN tests in collection-indexes.test.ts and
deterministic-ordering.test.ts that encoded the old JS behavior. Docs and
a minor changeset added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: mark NaN-semantics changeset as patch (no minor before 1.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: fold nan-semantics tests into existing well-suited test files

Move the unique NaN/invalid-Date coverage (lt on NaN, invalid-Date
eq/IN/range index parity) into collection-indexes.test.ts alongside the
existing NaN index tests, and drop the standalone nan-semantics.test.ts
whose other cases were already covered by collection-indexes,
deterministic-ordering and evaluators tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
kevin-dp and others added 30 commits July 7, 2026 12:20
A shared, framework-agnostic conformance suite for the useLiveQuery adapters: one behavioral spec runs against React, Vue, Svelte, Solid, and Angular through a thin per-adapter test harness. Documents current behavior and guards later refactors.
…anStack#1644)

* docs: design query collection persistence PR

* docs: clarify RFC cluster context

* docs: avoid citation syntax in RFC spec

* docs: draft query collection RFC

* fix(query-db-collection): keep on-demand query meta clone-safe

* chore: remove local RFC drafts from PR

* Apply query metadata review fixes
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…#1623 step 2) (TanStack#1641)

refactor(db): extract shared live-query adapter helpers

Add isCollection, isSingleResultCollection, and getLiveQueryStatusFlags to
@tanstack/db and migrate all five adapters to use them.

- isCollection: one structural, multi-realm-safe collection guard replacing the
  per-adapter duck-typing (React/Vue/Svelte/Angular) and Solid's
  `instanceof CollectionImpl` (which gives false negatives across dual-package
  boundaries — the same hazard the conformance suite hit).
- isSingleResultCollection: shared findOne cardinality check.
- getLiveQueryStatusFlags: status → {isLoading,isReady,isIdle,isError,isCleanedUp};
  used by React's snapshot path (the reactive adapters derive each flag as its own
  signal/computed, so a shared object-returning helper doesn't fit them — that
  duplication is reactivity-coupled and belongs to the observer step).

No behavior change; guarded by the conformance suite. First slice of the RFC
TanStack#1623 extraction (step 2). Status derivation, input/disabled classification, and
change→state projection remain — they are reactivity-coupled and land with the
observer.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A disabled query (callback returning null/undefined) crashed: the reactive-getter
unwrapping (`toValue`) called the callback, and the resulting null was spread into
createLiveQueryCollection → throw in getQueryIR. Now a null/undefined unwrapped
value is treated as a disabled query, matching React/Vue/Solid/Angular.

Clears the svelte-db conformance knownGaps (disabled-explicit, disabled-transition);
both now pass as normal tests.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nStack#1638)

The `{ query }` config-object branch called createLiveQueryCollection(opts)
without defaulting startSync, unlike the query-fn and reactive-options branches
(which force startSync: true). A bare `{ query }` therefore never synced and
returned empty. Default startSync: true and gcTime: 0 while honoring explicit
values in the config.

Clears the angular-db conformance knownGap (config-object-input).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Stack#1639)

Solid surfaces query errors by throwing (CollectionStateError) on read, for an
<ErrorBoundary> to catch, rather than exposing a readable isError flag like
React/Vue/Svelte/Angular. That's a framework idiom, not a bug — so instead of
forcing Solid into the flag model, parametrize the error-status scenario.

Adds driver.errorSurface ('flag' | 'throw', default 'flag'). The error-status
scenario asserts a readable isError/status for 'flag', and that reading the
errored result throws for 'throw'. Solid declares 'throw' and clears its
error-status knownGap; the other four keep the default flag assertions.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k#1655)

* test(query-db-collection): cover invalidation behavior matrix

* chore: add query invalidation changeset

* ci: apply automated fixes

* test(query-db-collection): clean up invalidation observers

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* docs: add query select semantics design

* docs(query-db-collection): clarify select row extraction

* Apply review simplifications

* Add changeset for query select semantics

* Strengthen select cache preservation test
…ack#1653)

* docs(query-db-collection): document query option compatibility

* docs(query-db-collection): clarify option compatibility wording

* docs(query-db-collection): add changeset

* ci: apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
)

* docs: add write status contract test design

* docs: clarify local write status contract

* docs: tighten transaction settlement wording

* chore: remove planning spec from PR

* chore: add changeset

* docs: clarify synced write status wording
…ack#1664)

* refactor(query-db-collection): extract query ownership helpers

* chore: add query ownership helpers changeset
…tack#1665)

* Apply query options review fixes

* ci: apply automated fixes

* Use flat query option fields

* Mount query client during collection sync

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…rn (TanStack#1667)

* docs(query-db-collection): document runtime QueryClient factory pattern

* Add changeset for runtime QueryClient docs

* ci: apply automated fixes

* docs(query-db-collection): clarify collection instance lifetime

* docs: remove changeset for docs-only update

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…esume (TanStack#1493)

* fix electric progressive persisted resume

* test(electric): cover changes during persisted resume

---------

Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com>
…Stack#1660)

The live query config builder used `this.config.gcTime || 5000`, which
treats an explicit `gcTime: 0` (disable GC) as unset and replaces it with
the 5s default. The collection is then garbage collected instead of being
kept alive. Use `??` so only `undefined` falls back to the default.

Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com>
…up (TanStack#1673)

* test: characterize query cancellation cleanup

* test: cover query cleanup review findings

* Apply query readiness simplification

* Add query cleanup changeset
* fix(electric): bound refresh wait for on-demand subsets

* ci: apply automated fixes

* test(electric): cover bounded refresh completion

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…tack#1671)

* docs(query-db-collection): document scoped collection factories

* docs: simplify scoped collection guidance

* docs: add scoped factory changeset

* docs: remove unnecessary changeset
* docs: add contributing guide

* docs: address contributing review feedback

* docs: order development setup steps
…anStack#1678)

* test(db): add failing type tests for generic collection row types (TanStack#1677)

Queries over a Collection<T> where T is an unresolved generic type
parameter stopped typechecking in 0.6.6: refs inside where/join/select
callbacks resolve to a union including RefLeaf, which exposes no row
properties, even ones guaranteed by T's constraint.

These tests currently fail and should pass once the ref typing is fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(db): restore query typechecking for generic collection row types

Fixes TanStack#1677. Since 0.6.6, refs inside where/join/select callbacks broke
when the collection's row type is an unresolved generic type parameter:
TypeScript defers the IsPlainObject conditional in RefForContextValue and
degrades the ref to a union that includes RefLeaf, which exposes no row
properties.

Two changes:

- Make RefForContextValue distribute over T so that, when deferred,
  TypeScript resolves its constraint by instantiating T with T's own
  constraint, picking the Ref branch instead of the Ref | RefLeaf union.
- Use GetRawResult instead of GetResult in SchemaFromSource for subquery
  sources: GetResult's Prettify wrapper produces a mapped type that the
  conditional-constraint resolution above cannot see through. Final query
  results still go through GetResult, so IDE display is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: add changeset for generic row type fix

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* test(query-db): characterize ownership cleanup lifecycle

* test(query-db): cover ownership hydration and cache expiry

* fix(query-db): remove empty ownership entries

* fix(query-db): bound ownership resolution tracking

* fix(query-db): clear retained ownership marker

* refactor(query-db): simplify ownership cleanup

* chore: add query ownership cleanup changeset

* chore: remove workflow artifact

* refactor: simplify query ownership bookkeeping

* fix(query-db): clean retained ownership on cleanup
* docs(query-db): design initial data semantics

* feat(query-db): support eager initial data

* Apply code simplification and review fixes

* fix(query-db): revalidate persisted initial data

* ci: apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
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.

10 participants