fix: align Firestore repository semantics - #37
Open
leoafarias wants to merge 3 commits into
Open
Conversation
This was referenced Aug 8, 2026
leoafarias
force-pushed
the
agent/rockets-auth-hardening
branch
from
August 8, 2026 23:53
a089fdb to
a080064
Compare
leoafarias
force-pushed
the
agent/rockets-firestore-parity
branch
from
August 8, 2026 23:53
4b84dcd to
967b45b
Compare
leoafarias
force-pushed
the
agent/rockets-auth-hardening
branch
from
August 8, 2026 23:56
a080064 to
f2f18fc
Compare
leoafarias
force-pushed
the
agent/rockets-firestore-parity
branch
from
August 8, 2026 23:56
967b45b to
4c27233
Compare
This was referenced Aug 9, 2026
leoafarias
force-pushed
the
agent/rockets-auth-hardening
branch
from
August 9, 2026 16:14
22887b2 to
06769e3
Compare
leoafarias
force-pushed
the
agent/rockets-firestore-parity
branch
from
August 9, 2026 16:14
4c27233 to
094e9e5
Compare
leoafarias
marked this pull request as ready for review
August 9, 2026 18:26
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.
Summary
Make the Firestore adapter's two execution paths agree with each other and with Firestore's real semantics.
The adapter runs queries through the Admin SDK backend or the in-memory backend, plus a local post-filter stage for any predicate Firestore can't evaluate server-side. Each of those three places had grown its own private copy of "how do I read a field", "how do I compare two values", and "how do I sort" — and they disagreed with each other and with Firestore.
Two of these produced silently wrong result sets, not rough edges:
postFilters; the pushdownfilterswere never evaluated. A query likeid = X AND ownerId = Yreturned the document even whenownerIddid not match — so adding an ID to a query quietly widened it instead of narrowing it.ANDwas broken in both directions. A seconddocumentIdsilently overwrote the first (last-write-wins), while anydocumentIdsin anANDbranch threwdocumentIds in an AND branch is not supported — restructure the where clause.Stack: based on #36 (
agent/rockets-auth-hardening); #35 sits on top. Review this diff againstagent/rockets-auth-hardening, notmain.What changed and why
1. Document-ID predicates compose instead of colliding
mergeAndBranchnow intersects the ID sets contributed by each child of anAND, rather than overwriting or throwing. A contradictory query (id = 'a' AND id = 'b') intersects to the empty set and short-circuits to zero rows instead of degrading into a full collection scan.This is why every
branch.documentIds && branch.documentIds.length > 0check becamebranch.documentIds !== undefined: an empty array now means "no document matches", which is semantically different fromundefined("this branch has no ID constraint"). Treating the two alike is exactly the full-scan bug.2. Branch filters apply on the ID path
applyFirestoreFilters(rows, branch.filters)now runs on the ID-selected path in both backends, closing the widening bug described above.3.
createis part of the backend contractFirestoreBackend.create()is new: the Admin backend uses the SDK's atomic.create(), the in-memory backend rejects a duplicate id. Previously a create went throughset(), so re-creating an existing id silently overwrote the document.4. One implementation of Firestore value semantics
firestore-value.tsis now the single owner, replacing three divergent copies:is_nullnow requires the field to exist and benull; a missing field is no longer reported as null.null < boolean < number < timestamp < string— matching Firestore's ordering rather than JavaScript's<.===reference comparison.betweenrequires the value and both bounds to share a scalar kind, so a number field no longer compares against a string bound via coercion.Timestampfields becameDate; timestamps nested inside maps and arrays leaked SDK objects to callers.5. Sorting and comparison deduplicated
firestore-sort.tsreplaces the three near-identical sort implementations in the Admin backend, the in-memory backend, and the query runner.Breaking changes
These are behavior changes to local query evaluation. They move the adapter toward Firestore semantics, so a caller relying on the old behavior was relying on a bug — but the change is observable.
is_null/is_not_nullrequire the field to existis_null.ninexcludes missing and null fieldsnin, matching Firestore'snot-in.betweenno longer matches across typesdocumentIdsinsideANDno longer throwsset().Review focus
Ranked by blast radius.
1.
documentIds: []vsundefined—admin-firestore.backend.ts,in-memory-firestore.backend.ts,firestore-where.translator.tsThe entire ID-intersection fix rests on this distinction, and it is easy to regress: any future
documentIds?.lengthguard reintroduces the full-scan bug, because an empty intersection would again read as "no ID constraint". All five call sites currently use!== undefined. Worth confirming that reads as deliberate rather than accidental, and deciding whether the interface should carry a comment making the invariant explicit.2.
compareFirestoreValuesthrows from inside a sort comparator —firestore-value.tsOrdering by a field that holds an array or a map now throws mid-sort, failing the whole read. That is defensible (an arbitrary order is worse than an error) but it converts a previously-degraded query into a runtime failure on a repository read path. Confirm that's the posture we want for 1.0, versus falling back to a documented ordering.
3.
firestoreValuesEqualduck-types onisEqual—firestore-value.tsAny object exposing an
isEqual(other)method takes that branch, not just Firestore SDK values. That is how the SDK's own types are matched without importing them, but it will also catch unrelated domain objects that happen to defineisEqual.4.
createfailure modes differ between backends —in-memory-firestore.backend.ts:63The in-memory backend throws a plain
Error; the Admin backend surfaces the SDK'sALREADY_EXISTSerror. Since parity between these two backends is this PR's whole point, a caller writingcatchlogic against one backend won't match the other. Neither is a typed repository exception.5.
asDateaccepts anything with atoDate()methodSame duck-typing tradeoff as (3), applied to timestamp normalization.
Type of Change
Verification
Run against this branch's tip after rebasing onto the current #36 head:
corepack yarn buildcorepack yarn typecheck:speccorepack yarn lint:allNew coverage:
firestore-value.spec.ts(missing vs. null, canonical type ordering, structural equality, recursive normalization),firestore-where.translator.spec.ts(ID intersection including the contradictory case), andfirestore-repository.module.spec.ts.corepack yarn build)corepack yarn test:e2e— verified at the chore: enforce release readiness #35 tip, 33 files / 167 tests)corepack yarn lint:all)Checklist
Deferred to #35
The emulator contract that proves this PR's parity claim lives in #35, not here.
packages/rockets-repository-firestore/src/__tests__/firestore-backend.emulator-spec.tsruns both backends against a real Java-backed Firestore emulator and asserts identical result sets. It is in #35 because it needsfirebase-tools,firebase.json,firestore.rules, and the emulator CI job, which are that PR's scope.The practical consequence for review: the assertions in this PR are verified against the in-memory double only. If you want the real-Firestore signal before approving,
corepack yarn test:firestore-emulatorat the #35 tip covers three of the claims here — explicit-null vs. missing nested fields, cross-type range exclusion, and nested ordering with recursive timestamp normalization. Document-ID intersection, filters-on-the-ID-path, duplicate-create rejection, and thenin/betweenpost-filter changes are not yet covered by an emulator assertion.