Repository navigation
Conversation
Port @supabase-labs/tanstack-db (supabase/tanstack-db@245600c) into the monorepo as @tanstack/supabase-db-collection. queryOnce, e2e tests and docs follow in separate PRs; the package stays private until the docs PR. On top of the upstream source: - syncMode: 'eager' option (default 'on-demand') - per-column parse/serialize converters - multi-page reads also order by the key columns, so ties in the query's order no longer repeat or skip rows; the oversized-IN URL budget accounts for that extra order - QueryClient registry is a WeakMap - X-Client-Info version injected from package.json at build time Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Joining composite key values with '-' gave distinct tuples the same collection key (['', '-'] and ['-', ''] both became '--'), so one row overwrote the other. Composite keys are now a JSON array of the values' string forms. Single-column keys are unchanged. A property test generates pairs of tuples cut from the same text and checks that keys are equal exactly when the tuples are; it fails on the '-' join. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The sync wrapper returned a new `sync: { sync }` object, dropping the
factory query-db-collection attaches to its sync config. Without it a
direct write such as `utils.writeUpsert` on an idle collection threw
SyncNotInitializedError instead of starting sync. Spread the original
sync config and replace only `sync`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Spreading the query collection's options dropped its DbClient config factory, so `collectionOptions(supabaseCollectionOptions(...))` read through the default QueryClient instead of the client's `queryClient` dependency, and a second DbClient could not reuse the descriptor. Register a factory that rebuilds the whole Supabase config, including the Realtime wrapper, with the client's `queryClient`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…l.any React Native (Hermes) and Safari before 17.4 have no AbortSignal.any, so every read split for an oversized IN list threw a TypeError there. The chunk controller now follows the caller's signal through a listener, keeping the caller's abort reason, and removes the listener when the load settles, including on failure. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The order param dropped each sort's `nulls`. TanStack DB puts nulls first unless told otherwise, while Postgres puts them last for ascending order, so a limited ascending window over a nullable column loaded the wrong rows (e.g. [3, 1] instead of the null rows [2, 4]). Every sort the query asks for now carries `.nullsfirst` or `.nullslast`; the key-column tiebreak is unchanged. A new test compares limited windows over a nullable column, for each direction and null placement, with the same live query over a local-only collection, served by a mock that orders like Postgres. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…stack Port the package's own e2e tests from supabase/tanstack-db (reads, filters, mutations, realtime and realtime filters) and run the shared @tanstack/db-collection-e2e suites against PostgREST and Realtime. - The local Supabase project lives in supabase/, on ports 553xx so it runs alongside Electric. max_rows = 2 makes reads page. - The filters cases read through live queries; the queryOnce cases come with experimental_queryOnce. - The shared case "should maintain query state during data changes" is skipped: local Realtime adds about 500 ms per change, so the live query under test is cleaned up mid-test. - CI runs the suite in a new `supabase` group of the E2E workflow. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Port queryOnce, the query IR serializer, and the IR-to-PostgREST translation from supabase/tanstack-db at 245600c, with their unit tests. The function is exported only as experimental_queryOnce because it reads TanStack DB's internal query representation. Adapted to this repo's IR: descriptor sources, conditional selects, and explicit null placement in the order param. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…executeQuery Restore the upstream queryOnce cases in the reads and filters e2e tests. Each filter case now checks the live query, experimental_queryOnce, and executeQuery paths, each through a fresh collection. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
experimental_queryOnce must resolve to what TanStack DB's queryOnce
resolves to for the same query over the same table state. The oracle
generates table states and aggregate-only queries, runs both against a
local Supabase stack with PostgREST aggregates enabled, and compares the
selected fields.
RED on the ported implementation:
SUPABASE_CLI="pnpm dlx supabase@2.116.0" pnpm exec vitest run \
--config vitest.e2e.config.ts e2e/query-once-aggregate-oracle.e2e.test.ts
Counterexample: rows [], sum(id)
expected [ { sum_id_0: null } ] to strictly equal []
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…anStack DB experimental_queryOnce returned PostgREST's aggregate row as-is, so its result depended on the route: SQL returns a row for an empty match, a null sum, and an average over non-null values, while TanStack DB returns no row, sums a null as 0, and averages over every row. min and max compared text by collation and ignored parse conversions, and paging or an unpushable filter changed the outcome. The server path now takes only count, sum, and avg of plain columns, with every filter pushable and no paging, ordering, or distinct. It requests a hidden row count and each average as a sum, and maps the result to TanStack DB's rules. Every other query runs through TanStack DB's queryOnce. - Law: experimental_queryOnce resolves to TanStack DB's queryOnce result over the same table state (IFCS-154). Rows are compared on the select's aliases (maintainer decision, 2026-11-11). - Reach (fixed seed 20261022, 40 histories): server-route 22, server-empty-match 12, server-null-value 8, server-find-one 10, server-subquery-source 12, client-route 33, converted-column 13, limit-or-offset 19, unpushable-filter 6. Four random campaigns reached every state. - RED (port, 173400d): rows [], sum(id): expected [ { sum_id_0: null } ] to strictly equal []. - GREEN: SUPABASE_CLI="pnpm dlx supabase@2.116.0" pnpm test:e2e passes 161 tests (1 skipped). - Mutants, each rejected by an assertion: SQL row on empty input, null sum, SQL avg, min/max pushed, no pushability check, sum over a parsed column, paging pushed, subquery filters dropped. - Limits: min/max are generated only over non-text columns, because TanStack DB's own result over text with nulls depends on row order (recorded in the reusable-law backlog); one local stack and table. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Contributor
Pre-merge checks:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 Changes
Adds
experimental_queryOnce(callback, supabase)to@tanstack/supabase-db-collection(IFCS-154). This is PR 3 of 4.experimental_queryOnce.private: true.What it adds
The port from
supabase/tanstack-dbat245600c:query-once.ts;serializeQueryIRand the query IR types inserialize.ts;postgrest-filters/query-ir-to-search.ts;query-once.test.ts,serialize.test.ts, and thequeryIrToSearchblock).It is exported only as
experimental_queryOnce, because it reads TanStack DB's internal query IR. Changes to fit this repo:ConditionalSelectin a select;order;X-Client-Infoname.The
queryOncee2e cases. Reads gets back its one-shot query. Each filters case now reads through three paths: a live query,experimental_queryOnce, andexecuteQuery. Each path reads through a fresh collection.A server-aggregate oracle against the local stack, plus a migration that enables PostgREST aggregates (
pgrst.db_aggregates_enabled) and adds thequery_once_rowstable.A fix the oracle found. The ported server path returned PostgREST's aggregate row as-is, so a query's answer depended on its route. The table shows each case.
queryOnce[{ count: 0 }][](findOne:undefined)sum/avgnull0avgof[1, null]10.5max(label)overa A b B'B'(collation)'b'parse'9' > '10')limit/offsetwithoutorderBy[]Per the decision on IFCS-154,
experimental_queryOncemust match TanStack DB exactly. The server path now takes onlycount,sum, andavgof plain columns. Every filter must be pushable, and the query can't page, order, or usedistinct. It requests a hiddencount()and asks for each average as a sum, then applies TanStack DB's rules. Every other query, includingmin/max, runs through TanStack DB'squeryOnce.Please look at these
$key,$origin,$hasPendingWrites, and$collectionId, and server rows do not. The oracle compares only the select's aliases. This is a maintainer decision, recorded in the coverage map.0beforemin/max, and'a' < 0and0 < 'a'are both false. Somin(label)over'a', null, nulldepends on row order. The oracle generatesmin/maxonly over columns that hold no text. The defect is in the coverage map's reusable-law backlog.sumgives0, andavgincludes nulls in its row count. The server path reproduces both. If core changes them, the oracle will flag the server path.#28(in245600c) already moved them to the client, and this PR keeps them there.🔬 Oracle Coverage
Owner: the new coverage-map row "Supabase
experimental_queryOnceserver aggregates" (e2e/query-once-aggregate-oracle.e2e.test.ts).Law:
experimental_queryOnceresolves to TanStack DB'squeryOnceresult over an eager Collection of the same table (IFCS-154). Results are compared at the settled promise. Rows are projected to the select's aliases, and a rejection must match a rejection.Reach, fixed seed
20261022, 40 histories:server-routeserver-empty-matchserver-null-valueserver-find-oneserver-subquery-sourceclient-routeconverted-columnlimit-or-offsetunpushable-filterFour random campaigns reached every state.
RED on the port (
173400d9b), withSUPABASE_CLI="pnpm dlx supabase@2.116.0" pnpm exec vitest run --config vitest.e2e.config.ts e2e/query-once-aggregate-oracle.e2e.test.ts. Counterexample: rows[]andsum(id)gaveexpected [ { sum_id_0: null } ] to strictly equal [].GREEN:
pnpm test:e2epassed 161 tests with 1 skipped (the existing skip from test(supabase-db-collection): add e2e tests against a local Supabase stack #2400).Mutants: each of these was rejected by an assertion failure:
sum;avg;min/maxpushed to the server;sumover a parsed column;Limits:
min/maxonly over columns that hold no text;✅ Checklist
pnpm test.Unit tests: 705 passed and 18 todo, with no type errors.
pnpm test:e2epassed 161 tests with 1 skipped.pnpm lint:changedis clean.🚀 Release Impact
private: trueuntil PR 4, which adds the changeset.Size: the package's minified gzip bundle grows from 5,482 to 8,142 bytes. The port adds 2,325 of those, mostly the faithfully ported serializer and join-embedding code, which
experimental_queryOncecannot reach. The fix adds 335.pnpm size:comparereports no change for published packages.🤖 Generated with Claude Code