Skip to content

Commit e589ab4

Browse files
committed
feat(firestore): export aggregateQuerySnapshotEqual for SDK parity
1 parent 7a4874e commit e589ab4

7 files changed

Lines changed: 148 additions & 13 deletions

File tree

.github/scripts/compare-types/configs/firestore.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,6 @@ const config: PackageConfig = {
2626
// ---------------------------------------------------------------------------
2727
missingInRN: [
2828
// --- Functions ---
29-
{
30-
name: 'aggregateQuerySnapshotEqual',
31-
reason:
32-
'Equality comparison helper for AggregateQuerySnapshot instances. Not ' +
33-
'yet implemented in RN Firebase.',
34-
},
3529
{
3630
name: 'documentSnapshotFromJSON',
3731
reason:

okf-bundle/testing/compare-types-work-queue.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ timestamp: 2026-07-03T00:00:00Z
88

99
# Compare-types parity — work queue
1010

11-
> **IN PROGRESS:** **C1.2**`aggregateQuerySnapshotEqual`; implementer dispatched.
11+
> **IN PROGRESS:** **C1.3–C1.7 / C1.2b (Phase D)**next document-only batch, or **C10** native sync.
1212
> **Stack:** `main``new-architecture` ([#9080](https://github.com/invertase/react-native-firebase/pull/9080)) → `pipeline-continue-workqueue` ([#9086](https://github.com/invertase/react-native-firebase/pull/9086)) → **`compare-types-work-queue`** (frontier).
1313
> **Goal:** shrink `.github/scripts/compare-types/configs/*.ts` by fixing real drift or tightening intractable documentation — not blanket parity for native-only surfaces. Machinery: [compare-types README](../../../.github/scripts/compare-types/README.md). Term ids: [iteration vocabulary](iteration-vocabulary.md). Policy: [documentation policy](../documentation-policy.md).
1414
@@ -54,7 +54,7 @@ Before any item's `implementation`:
5454
| ----- | ----- | ------ | ------- |
5555
| **A** | Tier 1 — types/docs only | **partial** | A1–A5, A7–A8, A10 committed; A6/A9 deferred |
5656
| **B** | Tier 2 — moderate / Phase S | **complete** | B1–B9 committed (B3 ✅ 2026-07-05) |
57-
| **C** | Tier 3 — hard / structural | **in progress** | C1.1 ✅ committed; C1.2 implementation dispatched |
57+
| **C** | Tier 3 — hard / structural | **in progress** | C1.1 ✅ committed; C1.2 review closed, ready for commit |
5858
| **D** | Tier 4 — document intractable | **queued** ||
5959

6060
---
@@ -63,9 +63,9 @@ Before any item's `implementation`:
6363

6464
**Label:** `c1.2-implementation-2026-07-05`
6565

66-
**Next item:** **C1.2** `implementation` (`aggregateQuerySnapshotEqual`).
66+
**Next item:** **C1.3–C1.7** (Phase D document-only batch) or pick from queue.
6767

68-
**Current gates:** C1.1 all gates **closed**. C1.2b documented (user-accepted). C1.2 `implementation_gate` **open** — implementer dispatched.
68+
**Current gates:** C1.1, C1.2, C1.2b all gates **closed**.
6969

7070
---
7171

@@ -96,7 +96,7 @@ Before any item's `implementation`:
9696
| **B8** | remote-config | `refactor!(types): align modular APIs with firebase-js-sdk sync signatures` | closed | closed | closed || `unit-focused` | RemoteConfigOptions on getRemoteConfig |
9797
| **B9** | firestore | `refactor!(types): align modular APIs with firebase-js-sdk sync signatures` | closed | closed | closed || `unit-focused` | aggregateFieldEqual exported |
9898
| **C0** | Phase C scope || open | open | open | `gap-analysis` | `none` | [Grilling](#phase-c--tier-3-hard--structural) |
99-
| **C1.2** | firestore | | open | open | open | `implementation` | `unit-focused` | `aggregateQuerySnapshotEqual` (pure JS); implementer dispatched 2026-07-05 |
99+
| **C1.2** | firestore | `feat(firestore): export aggregateQuerySnapshotEqual for SDK parity` | closed | closed | closed | | `unit-focused` | Committed 2026-07-05: pure JS helper; jest 315/315 |
100100
| **C1.2b** | firestore || closed | closed | closed || `none` | User-accepted: FieldValue `maximum`/`minimum` — iOS Firebase Firestore 12.15.0 lacks APIs; config hardened 2026-07-05 |
101101
| **C1.3** | firestore || open | open | open || `none` | Local-cache config (15 exports) — **document-only** (Phase D): web/N/A on native |
102102
| **C1.4** | firestore || open | open | open || `none` | Snapshot serialization (3 exports) — **document-only** (Phase D): web-only |
@@ -282,7 +282,7 @@ Gap-analysis 2026-07-05: 30 `missingInRN` entries + `differentShape: runTransact
282282
| Item | Exports | Verdict | Proposed fix |
283283
| ---- | ------- | ------- | ------------ |
284284
| **C1.1**| `TransactionOptions`, `runTransaction` | **implement** | Wire `maxAttempts` through JS → `transactionBegin` → native `runTransactionWithOptions` / `TransactionOptions` |
285-
| **C1.2** | `aggregateQuerySnapshotEqual` | **implement** | Pure JS equality helper (mirror B9 `aggregateFieldEqual`) |
285+
| **C1.2** | `aggregateQuerySnapshotEqual` | **implement** | Pure JS equality helper (mirror B9 `aggregateFieldEqual`) |
286286
| **C1.2b**| `maximum`, `minimum` | **document-only** | FieldValue set/update sentinels — user-accepted gap; iOS SDK lacks APIs; see Phase C Notes |
287287
| **C1.3**| Local-cache factories + 9 types (15 exports) | **document-only** | Web/local-cache API; RN uses native `persistence`/`cacheSizeBytes` — accept gap (Phase D) |
288288
| **C1.4**| `documentSnapshotFromJSON`, `querySnapshotFromJSON`, `onSnapshotResume` | **document-only** | Web-only serialization API — accept gap (Phase D) |
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, expect, it } from '@jest/globals';
2+
3+
import {
4+
aggregateFieldEqual,
5+
aggregateQuerySnapshotEqual,
6+
collection,
7+
count,
8+
getFirestore,
9+
query,
10+
sum,
11+
where,
12+
} from '../lib';
13+
import { AggregateField, AggregateQuerySnapshot } from '../lib/FirestoreAggregate';
14+
15+
describe('aggregate equality helpers', function () {
16+
const db = getFirestore();
17+
const coll = collection(db, 'aggregate-equal-test');
18+
const queryA = query(coll, where('author', '==', 'authorA'));
19+
const queryB = query(coll, where('author', '==', 'authorA'));
20+
const queryC = query(coll, where('author', '==', 'authorB'));
21+
22+
describe('aggregateFieldEqual()', function () {
23+
it('returns true for matching aggregate fields', function () {
24+
expect(aggregateFieldEqual(count(), count())).toBe(true);
25+
expect(aggregateFieldEqual(sum('age'), sum('age'))).toBe(true);
26+
});
27+
28+
it('returns false for different aggregate types or field paths', function () {
29+
expect(aggregateFieldEqual(count(), sum('age'))).toBe(false);
30+
expect(aggregateFieldEqual(sum('age'), sum('score'))).toBe(false);
31+
expect(aggregateFieldEqual(count(), {} as AggregateField<number>)).toBe(false);
32+
});
33+
});
34+
35+
describe('aggregateQuerySnapshotEqual()', function () {
36+
it('returns true when queries and count data match', function () {
37+
const left = new AggregateQuerySnapshot(queryA, { count: 2 }, true);
38+
const right = new AggregateQuerySnapshot(queryA, { count: 2 }, true);
39+
const equivalentQuery = new AggregateQuerySnapshot(queryB, { count: 2 }, true);
40+
41+
expect(aggregateQuerySnapshotEqual(left, right)).toBe(true);
42+
expect(aggregateQuerySnapshotEqual(left, equivalentQuery)).toBe(true);
43+
});
44+
45+
it('returns false when aggregate data differs', function () {
46+
const left = new AggregateQuerySnapshot(queryA, { count: 2 }, true);
47+
const right = new AggregateQuerySnapshot(queryA, { count: 3 }, true);
48+
49+
expect(aggregateQuerySnapshotEqual(left, right)).toBe(false);
50+
});
51+
52+
it('returns false when underlying queries differ', function () {
53+
const left = new AggregateQuerySnapshot(queryA, { count: 2 }, true);
54+
const right = new AggregateQuerySnapshot(queryC, { count: 2 }, true);
55+
56+
expect(aggregateQuerySnapshotEqual(left, right)).toBe(false);
57+
});
58+
59+
it('compares aggregate spec data from getAggregateFromServer snapshots', function () {
60+
const left = new AggregateQuerySnapshot(
61+
queryA,
62+
{ totalAge: 100, avgAge: 50, count: 2 },
63+
false,
64+
);
65+
const right = new AggregateQuerySnapshot(
66+
queryA,
67+
{ totalAge: 100, avgAge: 50, count: 2 },
68+
false,
69+
);
70+
const different = new AggregateQuerySnapshot(
71+
queryA,
72+
{ totalAge: 100, avgAge: null, count: 2 },
73+
false,
74+
);
75+
76+
expect(aggregateQuerySnapshotEqual(left, right)).toBe(true);
77+
expect(aggregateQuerySnapshotEqual(left, different)).toBe(false);
78+
});
79+
80+
it('returns false for non-AggregateQuerySnapshot arguments', function () {
81+
const snapshot = new AggregateQuerySnapshot(queryA, { count: 2 }, true);
82+
83+
expect(aggregateQuerySnapshotEqual(snapshot, {} as AggregateQuerySnapshot)).toBe(false);
84+
expect(aggregateQuerySnapshotEqual({} as AggregateQuerySnapshot, snapshot)).toBe(false);
85+
});
86+
});
87+
});

packages/firestore/consumer-type-test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
runTransaction,
2626
getCountFromServer,
2727
getAggregateFromServer,
28+
aggregateQuerySnapshotEqual,
2829
sum,
2930
average,
3031
count,
@@ -454,6 +455,13 @@ getAggregateFromServer(modQuery1, aggSpec).then(
454455
},
455456
);
456457

458+
// ----- aggregateQuerySnapshotEqual -----
459+
getCountFromServer(modQuery1).then(snap1 => {
460+
getCountFromServer(modQuery1).then(snap2 => {
461+
void aggregateQuerySnapshotEqual(snap1, snap2);
462+
});
463+
});
464+
457465
// ----- getDoc, getDocFromCache, getDocFromServer -----
458466
getDoc(modDoc).then(snap => snap.data());
459467
getDocFromCache(modDoc).then(snap => snap.data());

packages/firestore/lib/FirestoreAggregate.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
Query as FirestoreQuery,
2424
} from './types/firestore';
2525
import { FieldPath, fromDotSeparatedString } from './FieldPath';
26+
import { queryEqual } from './modular/query';
2627

2728
import type FirestorePath from './FirestorePath';
2829
import type { Query as QueryImplementation } from './FirestoreQuery';
@@ -137,3 +138,39 @@ export function aggregateFieldEqual(
137138
left._internalFieldPath?._toPath() === right._internalFieldPath?._toPath()
138139
);
139140
}
141+
142+
function aggregateSpecDataEqual(
143+
left: AggregateSpecData<AggregateSpec>,
144+
right: AggregateSpecData<AggregateSpec>,
145+
): boolean {
146+
const leftKeys = Object.keys(left);
147+
const rightKeys = Object.keys(right);
148+
if (leftKeys.length !== rightKeys.length) {
149+
return false;
150+
}
151+
for (const key of leftKeys) {
152+
if (!Object.prototype.hasOwnProperty.call(right, key) || left[key] !== right[key]) {
153+
return false;
154+
}
155+
}
156+
return true;
157+
}
158+
159+
/**
160+
* Compares two `AggregateQuerySnapshot` instances for equality.
161+
*/
162+
export function aggregateQuerySnapshotEqual<
163+
AggregateSpecType extends AggregateSpec,
164+
AppModelType,
165+
DbModelType extends DocumentData,
166+
>(
167+
left: AggregateQuerySnapshot<AggregateSpecType, AppModelType, DbModelType>,
168+
right: AggregateQuerySnapshot<AggregateSpecType, AppModelType, DbModelType>,
169+
): boolean {
170+
return (
171+
left instanceof AggregateQuerySnapshot &&
172+
right instanceof AggregateQuerySnapshot &&
173+
queryEqual(left.query, right.query) &&
174+
aggregateSpecDataEqual(left.data(), right.data())
175+
);
176+
}

packages/firestore/lib/modular.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { version } from './version';
2525
import {
2626
AggregateField,
2727
aggregateFieldEqual,
28+
aggregateQuerySnapshotEqual,
2829
fieldPathFromArgument,
2930
AggregateQuerySnapshot,
3031
} from './FirestoreAggregate';
@@ -503,7 +504,7 @@ export function count(): AggregateField<number> {
503504
return new AggregateField('count');
504505
}
505506

506-
export { aggregateFieldEqual };
507+
export { aggregateFieldEqual, aggregateQuerySnapshotEqual };
507508

508509
export function loadBundle(
509510
firestore: Firestore,

packages/firestore/type-test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
runTransaction,
2525
getCountFromServer,
2626
getAggregateFromServer,
27+
aggregateQuerySnapshotEqual,
2728
sum,
2829
average,
2930
count,
@@ -428,6 +429,13 @@ getAggregateFromServer(modQuery1, aggSpec).then(
428429
},
429430
);
430431

432+
// ----- aggregateQuerySnapshotEqual -----
433+
getCountFromServer(modQuery1).then(snap1 => {
434+
getCountFromServer(modQuery1).then(snap2 => {
435+
console.log(aggregateQuerySnapshotEqual(snap1, snap2));
436+
});
437+
});
438+
431439
// ----- getDoc, getDocFromCache, getDocFromServer -----
432440
getDoc(modDoc).then(snap => snap.data());
433441
getDocFromCache(modDoc).then(snap => snap.data());

0 commit comments

Comments
 (0)