Skip to content

fix(firestore, pipelines): send Timestamp and Date constants as timestamps - #9416

Open
Mastersam07 wants to merge 3 commits into
mainfrom
fix/firestore-pipeline-timestamp-constants
Open

Mastersam07 wants to merge 3 commits into
mainfrom
fix/firestore-pipeline-timestamp-constants

Conversation

@Mastersam07

Copy link
Copy Markdown
Contributor

Description

A Timestamp passed to a Firestore pipeline as a constant was serialized as a plain { seconds, nanoseconds } map, and a JS Date as {}. Nothing on iOS, Android or web turned either back into a timestamp, so the backend received map constants: comparisons against them never matched and constant(ts) read back as a map.

Changes:

  • JS: serializeValue sends Timestamp and Date as { __kind: 'timestamp', seconds, nanoseconds }, with nanoseconds truncated to microseconds to match firebase-js-sdk constant(). Out-of-range or non-integer values and invalid Dates are rejected before execute. Timestamps are detected structurally as well as with instanceof, because the e2e harness loads firestore/pipelines from lib and the main entry from dist.
  • iOS / Android: the pipeline parsers rebuild the SDK Timestamp from the tag, including rawStage params, and validate the same bounds, so a malformed value is a validation error rather than a native exception.
  • Web (macOS): the pipeline builder revives the tag into the web SDK Timestamp with the same validation.
  • Known limitation: the Android SDK's Stage.withOption has no Timestamp overload (firebase-firestore 27.0.1), so a Timestamp rawStage option is rejected on Android with a validation error. No documented pipeline stage takes a timestamp-valued option. Recorded in the platform parity registry.
  • Docs (OKF): the timestamp wire shape, and the e2e dual-copy instanceof gotcha, in okf-bundle/packages/firestore/pipelines.md.

Bytes and VectorValue constants have the same root cause and are tracked separately.

Related issues

Release Summary

fix(firestore, pipelines): Timestamp and Date pipeline constants (and rawStage params) are now sent as timestamps instead of maps

Checklist

  • I read the Contributor Guide and followed the process outlined there for submitting PRs.
    • Yes
  • My change supports the following platforms;
    • Android
    • iOS
    • Other (macOS, web)
  • My change includes tests;
    • e2e tests added or updated in packages/\*\*/e2e
    • jest tests added or updated in packages/\*\*/__tests__
  • I have updated TypeScript types that are affected by my change.
  • This is a breaking change;
    • Yes
    • No

Test Plan

  • Jest: yarn tests:jest packages/firestore/__tests__ (373/373). Covers the tagged shape, Date conversion, microsecond truncation, pre-1970 and invalid Dates, range validation, nested maps/arrays, where / select / addFields / comparison operands, rawStage params and options, and web revive.
  • Types and lint: yarn tsc:compile, yarn tsc:compile:consumer, yarn lint:js, yarn lint:deps, yarn compare:types; type-test.ts covers constant(Timestamp) and constant(Date).
  • Builds: yarn tests:ios:build, yarn tests:android:build, yarn tests:android:unit.
  • e2e (Pipeline.e2e.js): new tests read back Timestamp / Date constants, compare a stored timestamp against Timestamp and Date constants (equal, greaterThan, lessThan), read back Timestamp / Date values passed as rawStage params, and expect a malformed timestamp-shaped map to be rejected. Run locally on macOS and the iOS simulator; Android runs in CI.

Maintainer note: Fixes internal CPRN-610

…tamps

A Timestamp passed to a pipeline as a constant was serialized as a plain
`{ seconds, nanoseconds }` map, and a Date as `{}`, so the backend
received map constants and comparisons against them never matched.

Serialize both as `{ __kind: 'timestamp', seconds, nanoseconds }`,
truncating nanoseconds to microseconds like firebase-js-sdk, and reject
out-of-range values and invalid Dates. The iOS and Android pipeline
parsers and the web builder rebuild the SDK Timestamp, including for
rawStage params, and validate the same bounds.

The Android SDK has no Timestamp Stage.withOption overload, so a
Timestamp rawStage option is rejected on Android with a validation
error; this is recorded in the platform parity registry.
@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.45794% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.75%. Comparing base (c65e1ee) to head (1b21050).

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #9416      +/-   ##
============================================
+ Coverage     75.07%   76.75%   +1.69%     
============================================
  Files           620      465     -155     
  Lines         41243    30164   -11079     
  Branches       5242     3617    -1625     
============================================
- Hits          30958    23149    -7809     
+ Misses         8703     6141    -2562     
+ Partials       1582      874     -708     
Flag Coverage Δ
android-native ?
e2e-ts-android ?
e2e-ts-ios 53.19% <65.22%> (+0.20%) ⬆️
e2e-ts-macos 49.58% <82.98%> (+0.28%) ⬆️
ios-native 79.50% <92.99%> (+0.49%) ⬆️
ios-ruby 100.00% <ø> (ø)
jest 52.09% <94.00%> (+0.25%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@russellwheatley russellwheatley left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @Mastersam07, looks good overall and CI is green everywhere. A few small things inline, mostly cleanup.

Comment thread packages/firestore/e2e/Pipeline.e2e.js Outdated
),
'options.selections[0].expr.value to be a timestamp',
);
await expectAsyncError(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This runs the same execute twice to check two halves of the message. One call with the full substring would cover both, e.g. 'options.selections[0].expr.value to be a timestamp with integer seconds between -62135596800 and 253402300799 and integer nanoseconds between 0 and 999999999.', so the second call can go.

@Mastersam07 Mastersam07 Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Merged into a single expectAsyncError with the full shared substring (the path prefix differs between iOS and Android/web, but options.selections[0].expr.value to be a timestamp with … is common to all three). Fixed in 1b21050.

}

export function revivePipelineValue(value: unknown): unknown {
export function revivePipelineValue(value: unknown, fieldName = 'stage.options'): unknown {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

fieldName isn't passed by the only caller (pipeline_bridge_factory.ts), so I think this param can go.

@Mastersam07 Mastersam07 Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the param; revivePipelineValue now passes 'stage.options' straight to the validator. Fixed in 1b21050.

return value !== null && typeof value === 'object' && !Array.isArray(value);
}

const MIN_TIMESTAMP_SECONDS = -62135596800;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same bounds are defined in pipeline_runtime.ts, and FirestoreTimestamp.ts already has MIN_SECONDS. Might be worth exporting one set so they don't drift.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. FirestoreTimestamp.ts now exports the min/max seconds bounds and both the pipeline serializer and the web builder import them, so there's one definition. Fixed in 1b21050.

guard let rawValue = value else {
return []
}
let value = try RNFBFirestorePipelineParser.reviveSerializedTimestamps(rawValue, fieldName: fieldName)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

coerceRawParamValue already walks this tree iteratively, so this adds a second full traversal plus its own frame enum and box class in the parser. Might be worth checking for the timestamp tag in coerceRawParamValue's map branch instead. coerceRawOptions would need the same check in coerceExpressionTree though, so not sure it's worth it. Up to you.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I looked at folding it into coerceRawParamValue. I'd prefer to keep the separate walk: the same reviveSerializedTimestamps helper also handles rawStage options, which go through coerceExpressionTree, so folding it in would mean adding the tag check in two coercion paths instead of one shared helper. The extra pass is only over rawStage params/options, which are small. Happy to revisit if you feel strongly.

…he malformed-tag e2e

Export the Timestamp seconds bounds from FirestoreTimestamp and use them
in the pipeline serializer and the web builder instead of local copies.
Drop the unused fieldName parameter from revivePipelineValue, and assert
the malformed timestamp error with a single execute.

This branch has not been deployed

No deployments
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.

[🐛] Firestore pipelines: Timestamp (and Date) constants are sent as plain maps

2 participants