diff --git a/.github/workflows/e2e_pipeline.yml b/.github/workflows/e2e_pipeline.yml index ad449f9d..59a473f8 100644 --- a/.github/workflows/e2e_pipeline.yml +++ b/.github/workflows/e2e_pipeline.yml @@ -97,13 +97,13 @@ jobs: # Same Enterprise-edition database FlutterFire's pipeline suite # targets; not a secret, it is hardcoded in FlutterFire's own tests. FIRESTORE_PIPELINE_E2E_DATABASE_ID: firestore-pipeline-test - # --concurrency=1: every test seeds and deletes its own documents in a - # shared live database, matching how scripts/firestore-coverage.sh runs - # the other prod-tagged suites. + # --concurrency=1: every file under test/e2e/pipeline/ seeds and + # deletes its own documents in a shared live database, matching how + # scripts/firestore-coverage.sh runs the other prod-tagged suites. # # Prerequisite: the target database needs the `pipeline_e2e_books` # vector index described in test/e2e/README.md, otherwise the - # findNearest test fails. It is created once, out of band. + # findNearest tests fail. It is created once, out of band. run: | set -o pipefail if [ -z "${FIRESTORE_PIPELINE_E2E_PROJECT_ID:-}" ]; then @@ -111,7 +111,7 @@ jobs: exit 1 fi log="$RUNNER_TEMP/pipeline-e2e.log" - dart test -P prod test/e2e/pipeline_e2e_test.dart \ + dart test -P prod test/e2e/pipeline/ \ --concurrency=1 --reporter expanded 2>&1 | tee "$log" # `dart test` exits 0 when every test is skipped, which is exactly # what a missing project or database ID produces. Treat that as a diff --git a/.github/workflows/pipeline_api_snapshot.yml b/.github/workflows/pipeline_api_snapshot.yml new file mode 100644 index 00000000..4d766bb8 --- /dev/null +++ b/.github/workflows/pipeline_api_snapshot.yml @@ -0,0 +1,55 @@ +name: Pipeline API snapshot + +# Checks that test/fixtures/node_pipeline_api.json, which +# test/pipeline_api_parity_test.dart compares the Dart Pipelines API against, +# is what tool/pipeline_api_parity extracts from the pinned Node SDK. The Dart +# test itself runs with the other google_cloud_firestore tests in build.yml. +# +# A separate workflow so that it only runs for google_cloud_firestore changes: +# build.yml cannot filter paths per job. +on: + pull_request: + paths: + - "packages/google_cloud_firestore/**" + - ".github/workflows/pipeline_api_snapshot.yml" + push: + branches: + - main + paths: + - "packages/google_cloud_firestore/**" + - ".github/workflows/pipeline_api_snapshot.yml" + + workflow_dispatch: + +permissions: + contents: read + +jobs: + snapshot: + name: Node Pipeline API snapshot is up to date + runs-on: ubuntu-latest + timeout-minutes: 5 + + defaults: + run: + working-directory: packages/google_cloud_firestore/tool/pipeline_api_parity + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + node-version: 24 + + - run: npm ci --ignore-scripts --no-audit --no-fund + + - run: npm run extract + + - name: Fail if the snapshot is stale + run: | + if ! git diff --exit-code -- ../../test/fixtures/node_pipeline_api.json; then + echo "::error::test/fixtures/node_pipeline_api.json is stale. Regenerate it with 'npm ci && npm run extract' in packages/google_cloud_firestore/tool/pipeline_api_parity." + exit 1 + fi diff --git a/.github/workflows/pipeline_golden.yml b/.github/workflows/pipeline_golden.yml new file mode 100644 index 00000000..62b5fc93 --- /dev/null +++ b/.github/workflows/pipeline_golden.yml @@ -0,0 +1,62 @@ +name: Pipeline Golden Fixtures + +on: + pull_request: + # The fixtures only depend on google_cloud_firestore's generator and the + # Node SDK it pins, so there is nothing to check for other changes. + paths: + - "packages/google_cloud_firestore/**" + - ".github/workflows/pipeline_golden.yml" + push: + branches: + - main + paths: + - "packages/google_cloud_firestore/**" + - ".github/workflows/pipeline_golden.yml" + + workflow_dispatch: + +permissions: + contents: read + +jobs: + pipeline-golden: + name: Pipeline golden fixtures are up to date + runs-on: ubuntu-latest + timeout-minutes: 10 + + defaults: + run: + working-directory: packages/google_cloud_firestore/tool/pipeline_golden + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 + with: + # The pinned @google-cloud/firestore requires Node.js 22 or later. + node-version: "24" + + - name: Install the pinned Node SDK + run: npm ci --no-audit --no-fund + + - name: Regenerate the fixtures + run: npm run generate + + # test/pipeline_golden_test.dart compares against the committed fixtures + # in the regular `test` job of build.yml; this only proves they are what + # the pinned Node SDK produces for tool/pipeline_golden/cases.js. + - name: Fail if the committed fixtures are stale + working-directory: . + run: | + fixtures=packages/google_cloud_firestore/test/fixtures/pipeline_golden + if [ -n "$(git status --porcelain -- "$fixtures")" ]; then + git status --short -- "$fixtures" + git --no-pager diff --stat -- "$fixtures" + git --no-pager diff -- "$fixtures" | head -n 200 + echo "::error::The Pipeline golden fixtures are stale. Run 'npm ci && npm run generate' in packages/google_cloud_firestore/tool/pipeline_golden and commit the result." + exit 1 + fi + git diff --exit-code -- "$fixtures" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be18518c..d309dee2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -137,6 +137,23 @@ No setup needed: dart test ``` +#### `google_cloud_firestore` Pipeline golden fixtures + +`test/pipeline_golden_test.dart` is a unit test that compares every Pipeline +request against the request the Node.js SDK sends for the same Pipeline. The +expected requests in `test/fixtures/pipeline_golden/` are generated by the +pinned Node SDK; regenerate them (Node.js 22+) whenever you add a case or bump +that SDK: + +```bash +# From packages/google_cloud_firestore/tool/pipeline_golden +npm ci && npm run generate +``` + +See +[`tool/pipeline_golden/README.md`](packages/google_cloud_firestore/tool/pipeline_golden/README.md) +for how capture works and how to record intended differences. + #### `firebase_admin_sdk` emulator tests Requires the [Firebase CLI](https://firebase.google.com/docs/cli) and Java 21+. @@ -307,13 +324,22 @@ in that project: | Job | Trigger | What it does | |-----|---------|--------------| -| `pipeline-e2e` | PRs (non-fork) touching `packages/google_cloud_firestore/**`, pushes to `main`, schedule & manual | Runs `test/e2e/pipeline_e2e_test.dart` against live Firestore | +| `pipeline-e2e` | PRs (non-fork) touching `packages/google_cloud_firestore/**`, pushes to `main`, schedule & manual | Runs `test/e2e/pipeline/` against live Firestore | It is a separate workflow so the live-quota cost is only paid for changes that can affect it. See [`packages/google_cloud_firestore/test/e2e/README.md`](packages/google_cloud_firestore/test/e2e/README.md) for the required secrets and the one-time vector index setup. +**pipeline_golden.yml** keeps the Node-generated Pipeline fixtures honest: + +| Job | Trigger | What it does | +|-----|---------|--------------| +| `pipeline-golden` | PRs touching `packages/google_cloud_firestore/**`, pushes to `main` & manual | Regenerates `test/fixtures/pipeline_golden/` with the pinned Node SDK and fails if the committed fixtures differ | + +The comparison itself, `test/pipeline_golden_test.dart`, runs with the other +unit tests in `build.yml`. + Tests run against both `stable` and `beta` Dart SDK channels. Coverage is reported as a PR comment and uploaded to Codecov. The minimum threshold is **40%**. ## License diff --git a/packages/google_cloud_firestore/CHANGELOG.md b/packages/google_cloud_firestore/CHANGELOG.md index 5743d79e..c76aaef4 100644 --- a/packages/google_cloud_firestore/CHANGELOG.md +++ b/packages/google_cloud_firestore/CHANGELOG.md @@ -6,16 +6,37 @@ - **Breaking:** `PipelineFunctions.join` and `PipelineFunctions.split` now require their delimiter, matching `PipelineExpression` and the Node Admin SDK. A one-argument call was always rejected by the backend with `INVALID_ARGUMENT`. - **Breaking:** `PipelineFunctions.mapRemove` and `PipelineExpression.mapRemove` take a single key (a `String` or an expression) and send `map_remove(map, key)`, like the Node Admin SDK, instead of an `Iterable` of keys. Chain `.mapRemove('a').mapRemove('b')` to remove several keys. - **Breaking:** `PipelineResult.get` now resolves dot-separated paths such as `'metadata.lang'` as nested fields, like `DocumentSnapshot.get`, and also accepts a `FieldPath`. A top-level field whose name contains a dot, which `get('a.b')` used to return, is now read with `get(FieldPath(['a.b']))`. A string that is empty, starts or ends with `.`, or contains `..` or any of `*~/[]` now throws an `ArgumentError` instead of returning `null`. +- **Breaking:** `PipelineFunctions.arrayTransform` no longer takes a trailing index alias. Use the new `PipelineFunctions.arrayTransformWithIndex(array, elementAlias, indexAlias, transform)`, which matches `PipelineExpression.arrayTransformWithIndex` and the Node Admin SDK and sends the same `array_transform` function. - Fixed `PipelineFunctions.arrayMaximum`, `arrayMaximumN`, `arrayMinimum`, `arrayMinimumN` and `arraySum` sending function names (`array_maximum`, ...) that the backend rejects. They now send `maximum`, `maximum_n`, `minimum`, `minimum_n` and `sum`, like their `PipelineExpression` counterparts. - Fixed `Pipeline.addFields` sending one `alias` function per field instead of a single map keyed by alias, which the backend rejected. - Fixed `PipelineSource.collectionGroup()`, and `createFrom()` on a collection group query, omitting the leading root ancestor argument of the `collection_group` stage. - Fixed `PipelineValueType.double` sending `'double'` instead of `'float64'`, which made `isType` fail. Added the `int32`, `decimal128`, `maxKey`, `minKey`, `objectId` and `regex` value types. - Fixed `cosineDistance`, `dotProduct`, `euclideanDistance` and `Pipeline.findNearest` sending a plain list of numbers as an array instead of a vector. - Fixed Pipeline functions such as `equalAny`, `notEqualAny`, `arrayContainsAll`, `arrayContainsAny` and `mapMerge` failing when a `List` or `Map` argument holds expressions. Collection arguments are now sent as `array(...)` / `map(...)` functions, like the Node Admin SDK, and the static and fluent forms encode identically. +- Fixed the top-level `ascending()` and `descending()` sorting by a string constant when given a field name. A `String` now names a field, like everywhere else in the Pipeline API and in the Node Admin SDK. - `Pipeline.select`, `addFields`, `aggregate` and `distinct` now throw an `ArgumentError` on a duplicate field name or alias instead of silently keeping the last one, like the Node Admin SDK. - `PipelineExpression.substring` and `substringLiteral` now take `(position, [length])` like `PipelineFunctions.substring`: the second argument is a length, not an end index, and can be omitted. - `PipelineExpression.round` now accepts the optional `decimalPlaces` argument. - Exported the top-level `variable()` Pipeline helper. +- Every Pipeline function now has a single implementation shared by all of its forms. The fluent `PipelineExpression` method, the top-level helper (`equal`, `and`, `not`, ...) and the `Expression` alias forward to the `PipelineFunctions` function (and `field`, `constant` and `variable` to `Expression`), so every form sends the same request. +- `PipelineFunctions.arraySlice` now takes an optional `length`, like `PipelineExpression.arraySlice` and the Node Admin SDK. +- `PipelineFunctions.raw` now accepts `options`, like `Expression.raw`. +- `PipelineExpression.arrayContainsAll` and `arrayContainsAny` now also accept an array expression, like their `PipelineFunctions` forms and the Node Admin SDK. +- Fixed Pipeline field names that are not simple identifiers being sent unquoted. They are now backtick-quoted like the Node Admin SDK, so `field('first-name')` sends `` `first-name` ``. `field()` and `Expression.field()` also accept a `FieldPath`. `PipelineField.path` returns the quoted path, and `field('')` and `field('a..b')` now throw. +- Fixed `Pipeline.select`, `distinct` and the `groups` of `aggregate` keying a `String` field name that is not an identifier, such as `'last name'`, by the raw string, which the backend rejects as an invalid property path. It is now keyed by its quoted path, `` `last name` ``, like a `PipelineField`, so `select(['x-y', field('x-y')])` now throws for a duplicate key. An alias is still keyed as written, and must be a valid field path. +- Pipeline stages and functions now accept a `FieldPath` wherever they accept a `String` field name, as `field()` does: the target of every `PipelineFunctions` function, `ascending()` and `descending()`, the entries of `select`, `distinct`, `aggregate` groups and `removeFields`, `unnest`, `replaceWith`, and `findNearest`'s `vectorField` and `distanceResultField`. A `FieldPath` there used to be sent as a constant, which failed to encode. +- Fixed field paths containing a backtick being escaped as a lone backslash, which named a different field, in queries, field masks and Pipelines. +- Fixed `Pipeline.createFrom()` and `DocumentSnapshot` query cursors leaving `!=` and `not-in` fields out of the implicit ordering, and ordering inequality fields by their quoted names instead of segment by segment, unlike the backend and the Node Admin SDK. A snapshot cursor now needs a value for every inequality field. +- Fixed `Pipeline.createFrom()` of a query with a cursor on a descending ordering keeping the wrong side of the bound. +- Fixed `Pipeline.createFrom()` of a `VectorQuery` sending its `distanceThreshold` as an undocumented `find_nearest` option. It is now applied as a filter on the distance. +- Fixed `indexMode` being sent as an `index_mode` option the backend rejects; it is deprecated. `Pipeline.execute()` and `Transaction.executePipeline()` now ignore `indexMode`, and `PipelineIndexMode` is deprecated too: its only value, `recommended`, is already the backend's default. +- Fixed `Pipeline.findNearest(distanceThreshold:)` sending a `distance_threshold` option, which the backend rejects. It now adds a `where` stage after `find_nearest` that keeps the results within the threshold, as `Pipeline.createFrom()` does for a `VectorQuery`. +- Fixed Pipeline results failing to decode when a reference that names no document, such as the database root returned by `parent()`, is nested in a map or array. It now decodes to its resource name at every depth, as it already did for top-level fields. +- Pipeline raw options now match the Node Admin SDK. Every stage and source takes an optional `rawOptions`. Keys in `rawOptions` and `rawStage(options:)` are dot-separated paths merged into the typed options, and keys with an empty segment throw an `ArgumentError`. `rawStage` sends collections nested in a `Map` argument as `map(...)` / `array(...)` functions. +- `PipelineFunctions.add` and `multiply` take an optional trailing list of further operands, and `PipelineExpression.add` and `multiply` take `(second, [others])`, like the variadic Node Admin SDK functions. Further operands are sent as nested two-operand calls, `add(add(a, b), c)`, since the backend's `add` and `multiply` take exactly two operands. +- `PipelineSource.documents()` also accepts document paths such as `'books/book1'`, validated like `Firestore.doc()`. +- `PipelineFunctions.map()` also accepts a Dart `Map`, as in `map({'title': field('title')})`. +- Added the `PipelineOrdering.expr` and `PipelineOrdering.direction` getters. ## 0.5.5 diff --git a/packages/google_cloud_firestore/README.md b/packages/google_cloud_firestore/README.md index 12182e5a..cf5bff1a 100644 --- a/packages/google_cloud_firestore/README.md +++ b/packages/google_cloud_firestore/README.md @@ -197,7 +197,7 @@ A pipeline starts from exactly one source, via `firestore.pipeline()`. | `collectionReference(ref)` | Same, from a `CollectionReference`. | | `collectionGroup(id)` | Every collection with the given ID. | | `database()` | Every document in the database. | -| `documents([refs])` | An explicit set of documents. | +| `documents([refsOrPaths])` | An explicit set of documents, as references or paths. | | `createFrom(query)` | An existing `Query` or `VectorQuery`. | ```dart @@ -206,6 +206,7 @@ firestore.pipeline().collectionReference(firestore.collection('books')); firestore.pipeline().collectionGroup('books'); firestore.pipeline().database(); firestore.pipeline().documents([firestore.doc('books/book-1')]); +firestore.pipeline().documents(['books/book-1', 'books/book-2']); ``` #### Stages @@ -312,19 +313,32 @@ expression. So does the second argument of `cosineDistance`, `dotProduct` and `euclideanDistance`; either way it is sent as a vector. **`rawStage`** — escape hatch for preview stages this SDK does not yet wrap. -`search` is a thin wrapper over the same mechanism. +`search` is a thin wrapper over the same mechanism. As in the Node.js SDK, a +`Map` argument is sent as a literal map, but a `List` or `Map` nested in it is +sent as the `array(...)` or `map(...)` function that builds it, so it may hold +expressions. ```dart .rawStage('sample', [10, 'documents'], options: {'stable': true}) ``` +Every stage and source also takes `rawOptions`, for stage options this SDK +does not wrap yet; they take precedence over the typed ones. A key may be a +dot-separated path into a map option: `{'outer.inner': 1}` sends +`outer: {inner: 1}`, merged with anything else set inside `outer`. + +```dart +.limit(10, rawOptions: {'some_option.enabled': true}) +``` + Query-level options are passed to `execute()` rather than built onto the Pipeline, so a Pipeline value stays a pure description of what to fetch. Use -`rawOptions` for options this SDK does not wrap yet; they take precedence. +`rawOptions` for options this SDK does not wrap yet; they take precedence, and +dotted keys merge into the typed options, so `explain` plus +`{'explain_options.output_format': 'json'}` sends one `explain_options` map. ```dart .execute( - indexMode: PipelineIndexMode.recommended, explain: const PipelineExplainOptions( mode: PipelineExplainMode.analyze, outputFormat: PipelineExplainOutputFormat.text, @@ -351,6 +365,23 @@ PipelineFunctions.toUpper('title'); for the top-level `field` / `constant` / `variable`, for callers who prefer a namespaced entry point. +**Field paths.** `field` takes a `String` or a `FieldPath`. A `String` is a +dot-separated path, so `field('address.city')` reads `city` inside the +`address` map; use a `FieldPath` for a field whose name contains a dot. Names +that are not simple identifiers need no escaping: they are sent +backtick-quoted, as the Node.js SDK sends them. The same rules apply to every +field name a stage or function takes, which may be a `FieldPath` wherever it +may be a `String`, and `select`, `distinct` and `aggregate` key each field by +its quoted path. An alias given to `as()` is sent as written, and the backend +reads it as a field path too, so backtick-quote an alias that is not an +identifier. + +```dart +field('first-name'); // sent as `first-name` +field('author.last name'); // sent as author.`last name` +field(FieldPath(['a.b'])); // a single field named "a.b", sent as `a.b` +``` + `variable` references a name bound by the enclosing expression, such as the element alias of `arrayFilter` / `arrayTransform`: @@ -396,7 +427,7 @@ available on `PipelineFunctions`; most also exist as a fluent method. | Logical | `and`, `or`, `xor`, `nor`, `not`, `conditional`, `ifNull`, `coalesce`, `switchOn`, `equalAny`, `notEqualAny` | `and`, `or`, `xor`, `nor`, `not`, `conditional`, `if_null`, `coalesce`, `switch_on`, `equal_any`, `not_equal_any` | | Aggregate | `count`, `countAll`, `countIf`, `countDistinct`, `sum`, `average`, `minimum`, `maximum`, `first`, `last`, `arrayAgg`, `arrayAggDistinct` | `count`, `count_if`, `count_distinct`, `sum`, `average`, `minimum`, `maximum`, `first`, `last`, `array_agg`, `array_agg_distinct` | | Arithmetic | `add`, `subtract`, `multiply`, `divide`, `mod`, `abs`, `ceil`, `floor`, `round`, `trunc`, `sqrt`, `pow`, `exp`, `ln`, `log`, `log10`, `rand`, `logicalMinimum`, `logicalMaximum` | `add`, `subtract`, `multiply`, `divide`, `mod`, `abs`, `ceil`, `floor`, `round`, `trunc`, `sqrt`, `pow`, `exp`, `ln`, `log`, `log10`, `rand`, `minimum`, `maximum` | -| Array | `array`, `arrayConcat`, `arrayContains`, `arrayContainsAll`, `arrayContainsAny`, `arrayFilter`, `arrayGet`, `arrayLength`, `arrayReverse`, `arrayFirst`, `arrayFirstN`, `arrayLast`, `arrayLastN`, `arrayIndexOf`, `arrayIndexOfAll`, `arrayLastIndexOf`, `arraySlice`, `arrayTransform`, `arrayMaximum`, `arrayMaximumN`, `arrayMinimum`, `arrayMinimumN`, `arraySum`, `maximumN`, `minimumN`, `join` | `array`, `array_concat`, `array_contains`, `array_contains_all`, `array_contains_any`, `array_filter`, `array_get`, `array_length`, `array_reverse`, `array_first`, `array_first_n`, `array_last`, `array_last_n`, `array_index_of`, `array_index_of_all`, `array_index_of`, `array_slice`, `array_transform`, `maximum`, `maximum_n`, `minimum`, `minimum_n`, `sum`, `maximum_n`, `minimum_n`, `join` | +| Array | `array`, `arrayConcat`, `arrayContains`, `arrayContainsAll`, `arrayContainsAny`, `arrayFilter`, `arrayGet`, `arrayLength`, `arrayReverse`, `arrayFirst`, `arrayFirstN`, `arrayLast`, `arrayLastN`, `arrayIndexOf`, `arrayIndexOfAll`, `arrayLastIndexOf`, `arraySlice`, `arrayTransform`, `arrayTransformWithIndex`, `arrayMaximum`, `arrayMaximumN`, `arrayMinimum`, `arrayMinimumN`, `arraySum`, `maximumN`, `minimumN`, `join` | `array`, `array_concat`, `array_contains`, `array_contains_all`, `array_contains_any`, `array_filter`, `array_get`, `array_length`, `array_reverse`, `array_first`, `array_first_n`, `array_last`, `array_last_n`, `array_index_of`, `array_index_of_all`, `array_index_of`, `array_slice`, `array_transform`, `array_transform`, `maximum`, `maximum_n`, `minimum`, `minimum_n`, `sum`, `maximum_n`, `minimum_n`, `join` | | String | `byteLength`, `charLength`, `startsWith`, `endsWith`, `like`, `regexContains`, `regexMatch`, `regexFind`, `regexFindAll`, `stringConcat`, `stringContains`, `stringIndexOf`, `toUpper`, `toLower`, `substring`, `stringReverse`, `stringRepeat`, `stringReplaceAll`, `stringReplaceOne`, `trim`, `ltrim`, `rtrim`, `split` | `byte_length`, `char_length`, `starts_with`, `ends_with`, `like`, `regex_contains`, `regex_match`, `regex_find`, `regex_find_all`, `string_concat`, `string_contains`, `string_index_of`, `to_upper`, `to_lower`, `substring`, `string_reverse`, `string_repeat`, `string_replace_all`, `string_replace_one`, `trim`, `ltrim`, `rtrim`, `split` | | Generic | `length`, `reverse`, `concat` | `length`, `reverse`, `concat` | | Map | `map`, `mapGet`, `getField`, `mapSet`, `mapRemove`, `mapMerge`, `mapKeys`, `mapValues`, `mapEntries` | `map`, `map_get`, `get_field`, `map_set`, `map_remove`, `map_merge`, `map_keys`, `map_values`, `map_entries` | @@ -413,6 +444,15 @@ maps; `charLength`/`stringReverse`/`stringConcat` and `minimum`/`maximum` are the aggregate forms; use `logicalMinimum` / `logicalMaximum` to compare several operands element-wise. +`add` and `multiply` take extra operands as a trailing list, sent as nested +two-operand calls (the backend's `add` and `multiply` take exactly two), and +`map` takes a `Map` or a list alternating keys and values: + +```dart +field('price').add(field('tax'), [field('shipping')]); +PipelineFunctions.map({'title': field('title'), 'rating': 5}); +``` + Anything not yet wrapped is reachable via `PipelineFunctions.raw` or `pipelineFunction`. Their arguments are sent as-is, so build a collection that holds expressions with `PipelineFunctions.array` / `PipelineFunctions.map`: @@ -524,16 +564,24 @@ Two behavioural notes: #### E2E Testing -Real-project Pipeline E2E tests live in `test/e2e/pipeline_e2e_test.dart`. -They are skipped unless you provide a project and credentials: +Real-project Pipeline E2E tests live in `test/e2e/pipeline/`, one file per +area (sources, stages, results, execution, aggregates, and one per function +family). They are skipped unless you provide a project and credentials: ```bash export FIRESTORE_PIPELINE_E2E_PROJECT_ID="your-project-id" export FIRESTORE_PIPELINE_E2E_DATABASE_ID="your-enterprise-database-id" export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json" -dart test -P prod test/e2e/pipeline_e2e_test.dart +dart test -P prod test/e2e/pipeline/ --concurrency=1 ``` +`test/pipeline_e2e_coverage_test.dart` runs with the ordinary unit tests and +needs no credentials. It statically checks that the E2E suite references every +public Pipeline API member, supplies and omits every optional parameter, and +passes both an expression and a plain value to every value position, so a new +Pipeline function needs a live case before the build goes green. See +[`test/e2e/README.md`](test/e2e/README.md) for the layout and helpers. + The E2E suite includes vector nearest-neighbor coverage. Create the vector index once for the test collection group before running the suite in CI: diff --git a/packages/google_cloud_firestore/lib/src/path.dart b/packages/google_cloud_firestore/lib/src/path.dart index 11d493a0..7e7a4105 100644 --- a/packages/google_cloud_firestore/lib/src/path.dart +++ b/packages/google_cloud_firestore/lib/src/path.dart @@ -316,12 +316,21 @@ class FieldPath extends _Path { /// Returns the number of segments of this field path. int get _length => segments.length; + /// Segments that are sent as-is; any other segment is backtick-quoted. + static final _simpleSegment = RegExp(r'^[_a-zA-Z][_a-zA-Z0-9]*$'); + + /// The canonical string form of this path, as the backend expects it. + /// + /// Segments are joined with dots. A segment that is not a simple + /// identifier (letters, digits and underscores, not starting with a digit) + /// is wrapped in backticks, with backslashes and backticks inside it + /// escaped by a backslash: `FieldPath(['a.b', 'c-d'])` is `` `a.b`.`c-d` ``. + /// Matches the Node SDK's `FieldPath.formattedName`. String get _formattedName { - final regex = RegExp(r'^[_a-zA-Z][_a-zA-Z0-9]*$'); return segments .map((e) { - if (regex.hasMatch(e)) return e; - return '`${e.replaceAll(r'\', r'\\').replaceAll('`', r'\')}`'; + if (_simpleSegment.hasMatch(e)) return e; + return '`${e.replaceAll(r'\', r'\\').replaceAll('`', r'\`')}`'; }) .join('.'); } diff --git a/packages/google_cloud_firestore/lib/src/pipeline.dart b/packages/google_cloud_firestore/lib/src/pipeline.dart index 24e71f1c..0fc729a9 100644 --- a/packages/google_cloud_firestore/lib/src/pipeline.dart +++ b/packages/google_cloud_firestore/lib/src/pipeline.dart @@ -15,18 +15,37 @@ part of 'firestore.dart'; /// Creates a field reference expression for Firestore Pipeline operations. -PipelineField field(String fieldPath) => PipelineField._(fieldPath); +/// +/// [fieldPath] is a [String] or a [FieldPath]. A [String] is a dot-separated +/// path, so `field('address.city')` reads the `city` field of the `address` +/// map; use a [FieldPath] for a field whose name contains a dot, as in +/// `field(FieldPath(['a.b']))`. Field names that are not simple identifiers +/// (such as `first-name`, `last name` or `naïve`) need no escaping: they are +/// quoted with backticks when sent, as [PipelineField.path] shows. +/// +/// Throws an [ArgumentError] when [fieldPath] is neither a [String] nor a +/// [FieldPath], or has an empty segment (`''`, `'a..b'`). +PipelineField field(Object fieldPath) => Expression.field(fieldPath); /// Creates a constant expression for Firestore Pipeline operations. -PipelineExpression constant(Object? value) => _PipelineConstant(value); +PipelineExpression constant(Object? value) => Expression.constant(value); /// Creates a variable reference expression for Firestore Pipeline operations. -PipelineExpression variable(String name) => _PipelineVariable(name); +PipelineExpression variable(String name) => Expression.variable(name); /// FlutterFire-style entry points for building Pipeline expressions. abstract final class Expression { + // The top-level field, constant and variable helpers forward to these. The + // forwarding cannot go the other way: in here, a bare `field` names + // Expression.field itself. + /// Creates a field reference expression. - static PipelineField field(String fieldPath) => PipelineField._(fieldPath); + /// + /// [fieldPath] is a [String] (a dot-separated path) or a [FieldPath]; see + /// the top-level [field]. + static PipelineField field(Object fieldPath) { + return PipelineField._(_canonicalFieldPath(fieldPath)); + } /// Creates a constant expression. static PipelineExpression constant(Object? value) => _PipelineConstant(value); @@ -47,12 +66,14 @@ abstract final class Expression { } /// Creates a raw backend function expression. + /// + /// See [PipelineFunctions.raw]. static PipelineExpression raw( String name, Iterable args, { Map options = const {}, }) { - return pipelineFunction(name, args, options: options); + return PipelineFunctions.raw(name, args, options: options); } } @@ -141,8 +162,13 @@ enum PipelineValueType { } /// How the backend should choose indexes when executing a Pipeline. +/// +/// Deprecated: the backend rejects the `index_mode` option this selects +/// ("Unsupported option: index_mode"), and [recommended], letting the backend +/// choose, is already its default. `indexMode` is no longer sent. +@Deprecated(_indexModeDeprecation) enum PipelineIndexMode { - /// Let the backend pick the indexes it recommends. + /// Let the backend pick the indexes it recommends, which it does by default. recommended('recommended'); const PipelineIndexMode(this.value); @@ -151,6 +177,12 @@ enum PipelineIndexMode { final String value; } +/// Why [PipelineIndexMode] and the `indexMode` parameters are deprecated. +const _indexModeDeprecation = + 'The backend rejects the index_mode option ("Unsupported option: ' + 'index_mode"), and recommended is already its default, so indexMode is ' + 'ignored. Omit it.'; + /// Whether the backend should return planning stats alongside the results. enum PipelineExplainMode { /// Execute the Pipeline and return results, without planning stats. @@ -213,52 +245,76 @@ PipelineExpression pipelineFunction( return _PipelineFunctionExpression(name, args.toList(), options); } -/// Creates an equality expression. +/// Creates an equality expression; same as [PipelineFunctions.equal]. PipelineBooleanExpression equal(Object? left, Object? right) { - return _comparison('equal', left, right); + return PipelineFunctions.equal(left, right); } -/// Creates a not-equal expression. +/// Creates a not-equal expression; same as [PipelineFunctions.notEqual]. PipelineBooleanExpression notEqual(Object? left, Object? right) { - return _comparison('not_equal', left, right); + return PipelineFunctions.notEqual(left, right); } -/// Creates a less-than expression. +/// Creates a less-than expression; same as [PipelineFunctions.lessThan]. PipelineBooleanExpression lessThan(Object? left, Object? right) { - return _comparison('less_than', left, right); + return PipelineFunctions.lessThan(left, right); } -/// Creates a less-than-or-equal expression. +/// Creates a less-than-or-equal expression; same as +/// [PipelineFunctions.lessThanOrEqual]. PipelineBooleanExpression lessThanOrEqual(Object? left, Object? right) { - return _comparison('less_than_or_equal', left, right); + return PipelineFunctions.lessThanOrEqual(left, right); } -/// Creates a greater-than expression. +/// Creates a greater-than expression; same as [PipelineFunctions.greaterThan]. PipelineBooleanExpression greaterThan(Object? left, Object? right) { - return _comparison('greater_than', left, right); + return PipelineFunctions.greaterThan(left, right); } -/// Creates a greater-than-or-equal expression. +/// Creates a greater-than-or-equal expression; same as +/// [PipelineFunctions.greaterThanOrEqual]. PipelineBooleanExpression greaterThanOrEqual(Object? left, Object? right) { - return _comparison('greater_than_or_equal', left, right); + return PipelineFunctions.greaterThanOrEqual(left, right); } -PipelineBooleanExpression _comparison( - String name, - Object? left, - Object? right, -) { - return PipelineFunctions._bool(name, [_fieldOrExpression(left), right]); +/// The canonical string the backend expects for the field at [fieldPath]. +/// +/// Mirrors the Node SDK's `field()`, which sends +/// `FieldPath.fromArgument(fieldPath).formattedName`: a [String] is split on +/// dots into segments, a [FieldPath] keeps its segments (dots included), and +/// [FieldPath._formattedName] backtick-quotes every segment that is not a +/// simple identifier. Like Node, and unlike [Query.where], a [String] may hold +/// any character other than a dot. +String _canonicalFieldPath(Object fieldPath) { + final path = switch (fieldPath) { + FieldPath() => fieldPath, + String() => FieldPath(fieldPath.split('.')), + _ => throw ArgumentError.value( + fieldPath, + 'fieldPath', + 'Expected a String or a FieldPath.', + ), + }; + return path._formattedName; } -/// Interprets a [String] in a field position as a field reference. +/// Whether [value] is a field name, as [field] takes one: a [String] (a +/// dot-separated path) or a [FieldPath]. +bool _isFieldName(Object? value) => value is String || value is FieldPath; + +/// Interprets a field name in a field position as a field reference. /// /// Mirrors the Node SDK's `fieldOrExpression`: arguments that name the target /// of a function accept either a field name or an expression, so a bare -/// [String] means [field]. Arguments in a value position keep [String]s as -/// string literals, which is what [_encodePipelineValue] does by default. +/// [String] means [field]. So does a [FieldPath], which [field] takes too but +/// Node only takes inside `field()`. Arguments in a value position keep +/// [String]s as string literals, which is what [_encodePipelineValue] does by +/// default. +/// +/// Every stage and function reads its field names through this, so they all +/// accept the same ones. Object? _fieldOrExpression(Object? value) { - return value is String ? field(value) : value; + return _isFieldName(value) ? field(value!) : value; } /// Applies [_fieldOrExpression] to the first entry of [values]. @@ -306,9 +362,25 @@ Object? _valueToDefaultExpr(Object? value) { return switch (value) { Uint8List() => value, Iterable() => PipelineFunctions.array(value), - Map() => PipelineFunctions.map([ - for (final entry in value.entries) ...[entry.key.toString(), entry.value], - ]), + Map() => PipelineFunctions.map(value), + _ => value, + }; +} + +/// Converts a [Pipeline.rawStage] argument. +/// +/// Mirrors the Node SDK's `rawStage`, which sends a plain object as a literal +/// map value (`_mapValue`) and anything else as is (`constant`). A literal map +/// that is a stage argument may hold expressions, as `select`'s does, but the +/// backend rejects them one level deeper, so each value of the map goes +/// through [_valueToDefaultExpr]: a nested collection becomes the `map(...)` +/// or `array(...)` function that builds it. +Object? _rawStageArg(Object? value) { + return switch (value) { + Map() => { + for (final entry in value.entries) + entry.key.toString(): _valueToDefaultExpr(entry.value), + }, _ => value, }; } @@ -327,19 +399,19 @@ bool _containsExpression(Object? value) { }; } -/// Creates a logical AND expression. +/// Creates a logical AND expression; same as [PipelineFunctions.and]. PipelineBooleanExpression and(Iterable expressions) { - return _PipelineBooleanExpression('and', expressions.toList()); + return PipelineFunctions.and(expressions); } -/// Creates a logical OR expression. +/// Creates a logical OR expression; same as [PipelineFunctions.or]. PipelineBooleanExpression or(Iterable expressions) { - return _PipelineBooleanExpression('or', expressions.toList()); + return PipelineFunctions.or(expressions); } -/// Creates a logical NOT expression. +/// Creates a logical NOT expression; same as [PipelineFunctions.not]. PipelineBooleanExpression not(PipelineBooleanExpression expression) { - return _PipelineBooleanExpression('not', [expression]); + return PipelineFunctions.not(expression); } /// Returns the current document as a Pipeline expression. @@ -356,12 +428,19 @@ PipelineBooleanExpression documentMatches(Object? rquery) { /// Convenience wrappers for the Firestore Pipeline function catalog. /// /// These helpers encode to the backend function names documented in the -/// Firestore Pipeline functions reference. String arguments are encoded as -/// string literals; use [field] when you want to reference a document field. +/// Firestore Pipeline functions reference. A [String] or a [FieldPath] in a +/// field position (the target of most functions, such as `'title'` in +/// `startsWith('title', 'Harry')`) is a field name, read like [field]; in a +/// value position a [String] is a string literal. Use [field] to reference a +/// document field in a value position. /// /// A [List] or [Map] argument may hold expressions, as in `[field('a'), 1]`: /// it is sent as an [array] or [map] function so the backend evaluates them. /// Wrap a collection in [constant] to send it as a literal value instead. +/// +/// The fluent [PipelineExpression] method of the same name, and any top-level +/// or [Expression] helper, forward to the function here, so every form of a +/// function builds the same expression. abstract final class PipelineFunctions { static PipelineExpression _expr(String name, Iterable args) { return pipelineFunction(name, args.map(_valueToDefaultExpr)); @@ -371,6 +450,23 @@ abstract final class PipelineFunctions { return _PipelineBooleanExpression(name, [...args.map(_valueToDefaultExpr)]); } + /// Folds [first], [second] and [others] into left-nested calls of the + /// two-operand function [name], as in `name(name(first, second), others[0])`. + /// + /// With no [others], this is the single call `name(first, second)`. + static PipelineExpression _leftNested( + String name, + Object? first, + Object? second, + Iterable others, + ) { + var result = _expr(name, [_fieldOrExpression(first), second]); + for (final operand in others) { + result = _expr(name, [result, operand]); + } + return result; + } + /// Tests [target] against the values in [searchSpace]. /// /// Like the Node SDK, a list of plain values is sent as a literal array @@ -395,9 +491,13 @@ abstract final class PipelineFunctions { /// /// Unlike the other helpers, [args] are sent as-is: a [List] or [Map] is a /// literal value, which cannot hold expressions. Build those with [array] - /// or [map]. - static PipelineExpression raw(String name, Iterable args) { - return pipelineFunction(name, args); + /// or [map]. [options] are sent as the function's options. + static PipelineExpression raw( + String name, + Iterable args, { + Map options = const {}, + }) { + return pipelineFunction(name, args, options: options); } /// COUNT aggregate function. @@ -489,8 +589,16 @@ abstract final class PipelineFunctions { } /// ADD arithmetic function. - static PipelineExpression add(Object? left, Object? right) { - return _expr('add', [_fieldOrExpression(left), right]); + /// + /// Adds [first], [second] and any [others]. The backend's `add` takes + /// exactly two operands, so further operands are sent as nested + /// two-operand calls: `add(a, b, [c, d])` sends `add(add(add(a, b), c), d)`. + static PipelineExpression add( + Object? first, + Object? second, [ + Iterable others = const [], + ]) { + return _leftNested('add', first, second, others); } /// SUBTRACT arithmetic function. @@ -499,8 +607,17 @@ abstract final class PipelineFunctions { } /// MULTIPLY arithmetic function. - static PipelineExpression multiply(Object? left, Object? right) { - return _expr('multiply', [_fieldOrExpression(left), right]); + /// + /// Multiplies [first], [second] and any [others]. The backend's `multiply` + /// takes exactly two operands, so further operands are sent as nested + /// two-operand calls: `multiply(a, b, [c])` sends + /// `multiply(multiply(a, b), c)`. + static PipelineExpression multiply( + Object? first, + Object? second, [ + Iterable others = const [], + ]) { + return _leftNested('multiply', first, second, others); } /// DIVIDE arithmetic function. @@ -707,30 +824,54 @@ abstract final class PipelineFunctions { } /// ARRAY_SLICE function. + /// + /// Returns [length] elements of [array] starting at index [offset]; when + /// [length] is omitted the slice runs to the end of the array. static PipelineExpression arraySlice( Object? array, - Object? offset, + Object? offset, [ Object? length, - ) { - return _expr('array_slice', [_fieldOrExpression(array), offset, length]); + ]) { + return _expr('array_slice', [ + _fieldOrExpression(array), + offset, + ..._optionalArg(length), + ]); } /// ARRAY_TRANSFORM function. + /// + /// Evaluates [transform] for each element of [array], with the element + /// bound to the variable [elementAlias]. To also bind the element's index, + /// use [arrayTransformWithIndex]. static PipelineExpression arrayTransform( Object? array, - String variableName, - Object? expression, [ - String? indexVariableName, - ]) { - final target = _fieldOrExpression(array); - return indexVariableName == null - ? _expr('array_transform', [target, variableName, expression]) - : _expr('array_transform', [ - target, - variableName, - indexVariableName, - expression, - ]); + String elementAlias, + Object? transform, + ) { + return _expr('array_transform', [ + _fieldOrExpression(array), + elementAlias, + transform, + ]); + } + + /// ARRAY_TRANSFORM function, binding each element's index too. + /// + /// Like [arrayTransform], with the element's zero-based index bound to the + /// variable [indexAlias]. + static PipelineExpression arrayTransformWithIndex( + Object? array, + String elementAlias, + String indexAlias, + Object? transform, + ) { + return _expr('array_transform', [ + _fieldOrExpression(array), + elementAlias, + indexAlias, + transform, + ]); } /// MAXIMUM_N array function. @@ -924,9 +1065,27 @@ abstract final class PipelineFunctions { /// MAP construction function. /// - /// [keyValues] alternates keys and values; values may be expressions. - static PipelineExpression map(Iterable keyValues) { - return _expr('map', keyValues); + /// [keyValues] is either a [Map], as in `map({'title': field('title')})`, + /// or an [Iterable] alternating keys and values, as in + /// `map(['title', field('title')])`. Values may be expressions; a [Map]'s + /// keys are sent as strings. + /// + /// Throws an [ArgumentError] when [keyValues] is neither. + static PipelineExpression map(Object keyValues) { + return _expr('map', switch (keyValues) { + Map() => [ + for (final MapEntry(:key, :value) in keyValues.entries) ...[ + key.toString(), + value, + ], + ], + Iterable() => keyValues, + _ => throw ArgumentError.value( + keyValues, + 'keyValues', + 'Expected a Map, or an Iterable alternating keys and values.', + ), + }); } /// MAP_GET function. @@ -1357,13 +1516,21 @@ abstract final class PipelineFunctions { } /// Creates an ascending Pipeline ordering. +/// +/// [expression] is a field name (a [String] or a [FieldPath], read like +/// [field]) or an expression, like [PipelineExpression.ascending] on that +/// expression. PipelineOrdering ascending(Object expression) { - return PipelineOrdering._('ascending', expression); + return PipelineOrdering._('ascending', _fieldOrExpression(expression)!); } /// Creates a descending Pipeline ordering. +/// +/// [expression] is a field name (a [String] or a [FieldPath], read like +/// [field]) or an expression, like [PipelineExpression.descending] on that +/// expression. PipelineOrdering descending(Object expression) { - return PipelineOrdering._('descending', expression); + return PipelineOrdering._('descending', _fieldOrExpression(expression)!); } /// The starting point for constructing Firestore Pipeline operations. @@ -1374,13 +1541,28 @@ final class PipelineSource { final Firestore _firestore; /// Starts a Pipeline over documents in the collection at [collectionPath]. - Pipeline collection(String collectionPath) { + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [Pipeline.rawStage]'s `options` do. + Pipeline collection( + String collectionPath, { + Map rawOptions = const {}, + }) { _validateResourcePath('collectionPath', collectionPath); - return collectionReference(_firestore.collection(collectionPath)); + return collectionReference( + _firestore.collection(collectionPath), + rawOptions: rawOptions, + ); } /// Starts a Pipeline over every collection with [collectionId]. - Pipeline collectionGroup(String collectionId) { + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [Pipeline.rawStage]'s `options` do. + Pipeline collectionGroup( + String collectionId, { + Map rawOptions = const {}, + }) { if (collectionId.contains('/')) { throw ArgumentError( 'Invalid collectionId "$collectionId". Collection IDs must not contain "/".', @@ -1392,33 +1574,65 @@ final class PipelineSource { return _start('collection_group', [ _PipelineProtoValue(firestore_v1.Value(referenceValue: '')), collectionId, - ]); + ], rawOptions: rawOptions); } /// Starts a Pipeline over every document in the database. - Pipeline database() => _start('database', const []); + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [Pipeline.rawStage]'s `options` do. + Pipeline database({Map rawOptions = const {}}) { + return _start('database', const [], rawOptions: rawOptions); + } /// Starts a Pipeline over the provided collection reference. /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [Pipeline.rawStage]'s `options` do. + /// /// Throws an [ArgumentError] when [collectionReference] targets a different /// database than this Pipeline. Pipeline collectionReference( - CollectionReference collectionReference, - ) { + CollectionReference collectionReference, { + Map rawOptions = const {}, + }) { _validateSameDatabase( _firestore, collectionReference.firestore, 'collectionReference', ); - return _start('collection', [collectionReference]); + return _start('collection', [collectionReference], rawOptions: rawOptions); } - /// Starts a Pipeline over the provided document references. + /// Starts a Pipeline over the provided documents. /// - /// Throws an [ArgumentError] when any of [documents] targets a different - /// database than this Pipeline. - Pipeline documents(Iterable> documents) { - final refs = documents.toList(); + /// Each of [documents] is a [DocumentReference] or a slash-separated + /// document path, such as `'books/book1'`, read like [Firestore.doc] reads + /// it. + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [Pipeline.rawStage]'s `options` do. + /// + /// Throws an [ArgumentError] when [documents] is empty, when an entry is + /// neither a reference nor a path, when a path does not point to a + /// document, or when a reference targets a different database than this + /// Pipeline. + Pipeline documents( + Iterable documents, { + Map rawOptions = const {}, + }) { + final refs = [ + for (final document in documents) + switch (document) { + String() => _firestore.doc(document), + DocumentReference() => document, + _ => throw ArgumentError.value( + document, + 'documents', + 'Expected a DocumentReference or a document path.', + ), + }, + ]; if (refs.isEmpty) { throw ArgumentError.value(documents, 'documents', 'Must not be empty.'); } @@ -1430,14 +1644,16 @@ final class PipelineSource { // name. The full name is only correct for references in a value position. return _start('documents', [ for (final ref in refs) _PipelineProtoValue(_relativeReference(ref.path)), - ]); + ], rawOptions: rawOptions); } /// Converts [query] into an equivalent Pipeline. /// /// [query] must be a [Query] or a [VectorQuery]. Its filters, projections, /// orderings, cursors, limit and offset are all translated into the - /// corresponding Pipeline stages. + /// corresponding Pipeline stages. A [VectorQuery] becomes a find_nearest + /// stage, followed by a where stage on the distance when it has a + /// distance threshold. /// /// Throws an [ArgumentError] when [query] targets a different database than /// this Pipeline. @@ -1458,10 +1674,14 @@ final class PipelineSource { } } - Pipeline _start(String name, List args) { + Pipeline _start( + String name, + List args, { + required Map rawOptions, + }) { return Pipeline._( firestore: _firestore, - stages: [_PipelineStage(name, args)], + stages: [_PipelineStage(name, args, _PipelineOptions(raw: rawOptions))], ); } } @@ -1481,28 +1701,91 @@ final class Pipeline { /// Adds a raw backend Pipeline stage. /// /// Use this for preview stages or options not yet wrapped by this SDK. + /// + /// [args] are converted like the Node SDK's `rawStage` params: each is sent + /// as is (a [List] as a literal array value), except a [Map]. A [Map] is + /// sent as a literal map value, which may hold expressions, but a [List] or + /// [Map] nested in it is sent as the [PipelineFunctions.array] or + /// [PipelineFunctions.map] function that builds it, since the backend + /// rejects expressions nested in a literal value at that depth. Wrap a + /// nested collection in [constant] to send it as a literal value instead. + /// + /// [options] are the stage's options, keyed by the names the backend + /// expects. A key may be a dot-separated path into a map option: + /// `{'outer.inner': 1}` sends `outer: {inner: 1}`, and is merged with any + /// other value set inside `outer`. Keys are applied in order, so a later + /// key overwrites an earlier one it overlaps with. + /// + /// Throws an [ArgumentError] when a key of [options] has an empty segment, + /// such as `''`, `'a.'` or `'a..b'`. Pipeline rawStage( String name, Iterable args, { Map options = const {}, }) { - return _append(_PipelineStage(name, args.toList(), options)); + return _append( + _PipelineStage(name, [ + for (final arg in args) _rawStageArg(arg), + ], _PipelineOptions(raw: options, rawName: 'options')), + ); + } + + /// Adds the stage [name], as one of this SDK's typed stage methods does. + /// + /// Unlike [rawStage], [args] are sent as they are. [options] are the typed + /// options of the stage, under their backend names, and [rawOptions] the + /// caller's raw options, overlaid on them. + Pipeline _stage( + String name, + List args, { + Map options = const {}, + required Map rawOptions, + }) { + return _append( + _PipelineStage( + name, + args, + _PipelineOptions(known: options, raw: rawOptions), + ), + ); } /// Filters inputs using [condition]. - Pipeline where(PipelineBooleanExpression condition) { - return rawStage('where', [condition]); + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. + Pipeline where( + PipelineBooleanExpression condition, { + Map rawOptions = const {}, + }) { + return _stage('where', [condition], rawOptions: rawOptions); } /// Selects or computes fields from the inputs. /// - /// Entries may be [String] field names, [PipelineField] references, - /// [PipelineExpression] instances, or [PipelineAliasedExpression] values. + /// Entries may be field names ([String]s or [FieldPath]s, read like + /// [field]), [PipelineField] references, or [PipelineAliasedExpression] + /// values. /// - /// Throws an [ArgumentError] when two [selections] land on the same field - /// name or alias. - Pipeline select(Iterable selections) { - return rawStage('select', [_projectionMap(selections)]); + /// The selections are sent as a map keyed by the field they land on, which + /// the backend reads as a field path. A field name or [PipelineField] is + /// keyed by its [PipelineField.path], so `'last name'` is keyed by + /// `` `last name` ``. An aliased expression is keyed by its alias as + /// written, as in the Node SDK, so an alias must be a valid field path + /// itself; see [PipelineExpression.as]. + /// + /// Throws an [ArgumentError] when two [selections] land on the same key, as + /// `select(['x-y', field('x-y')])` does. + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. + Pipeline select( + Iterable selections, { + Map rawOptions = const {}, + }) { + return _stage('select', [ + _projectionMap(selections), + ], rawOptions: rawOptions); } /// Adds or overwrites fields on the inputs. @@ -1512,19 +1795,31 @@ final class Pipeline { /// a single map keyed by alias. /// /// Throws an [ArgumentError] when two [fields] share an alias. - Pipeline addFields(Iterable fields) { - return rawStage('add_fields', [ + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. + Pipeline addFields( + Iterable fields, { + Map rawOptions = const {}, + }) { + return _stage('add_fields', [ _projectionMap(fields, argumentName: 'fields'), - ]); + ], rawOptions: rawOptions); } /// Aggregates inputs using aliased aggregate expressions. /// + /// [groups] takes the same entries as [distinct], keyed as in [select]. + /// /// Throws an [ArgumentError] when two [accumulators], or two [groups], land /// on the same field name or alias. + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. Pipeline aggregate( Iterable accumulators, { Iterable groups = const [], + Map rawOptions = const {}, }) { final values = accumulators.toList(); if (values.isEmpty) { @@ -1534,101 +1829,157 @@ final class Pipeline { 'Must not be empty.', ); } - return rawStage('aggregate', [ + return _stage('aggregate', [ _projectionMap(values, argumentName: 'accumulators'), _projectionMap(groups, argumentName: 'groups'), - ]); + ], rawOptions: rawOptions); } /// Returns unique combinations of the provided grouping expressions. /// - /// Entries may be [String] field names, [PipelineField] references, or - /// [PipelineAliasedExpression] values. + /// Entries may be field names ([String]s or [FieldPath]s, read like + /// [field]), [PipelineField] references, or [PipelineAliasedExpression] + /// values, keyed as in [select]. /// /// Throws an [ArgumentError] when two [groups] land on the same field name /// or alias. - Pipeline distinct(Iterable groups) { + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. + Pipeline distinct( + Iterable groups, { + Map rawOptions = const {}, + }) { final values = groups.toList(); if (values.isEmpty) { throw ArgumentError.value(groups, 'groups', 'Must not be empty.'); } - return rawStage('distinct', [ + return _stage('distinct', [ _projectionMap(values, argumentName: 'groups'), - ]); + ], rawOptions: rawOptions); } /// Removes fields from the inputs. - Pipeline removeFields(Iterable fields) { - return rawStage('remove_fields', [ - for (final value in fields) - if (value is String) field(value) else value, - ]); + /// + /// Entries may be field names ([String]s or [FieldPath]s, read like + /// [field]), or [PipelineField] references. + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. + Pipeline removeFields( + Iterable fields, { + Map rawOptions = const {}, + }) { + return _stage('remove_fields', [ + for (final value in fields) _fieldOrExpression(value), + ], rawOptions: rawOptions); } /// Sorts inputs according to [orderings]. - Pipeline sort(Iterable orderings) { + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. + Pipeline sort( + Iterable orderings, { + Map rawOptions = const {}, + }) { final values = orderings.toList(); if (values.isEmpty) { throw ArgumentError.value(orderings, 'orderings', 'Must not be empty.'); } - return rawStage('sort', values); + return _stage('sort', values, rawOptions: rawOptions); } /// Skips the first [offset] inputs. - Pipeline offset(int offset) { + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. + Pipeline offset(int offset, {Map rawOptions = const {}}) { if (offset < 0) { throw ArgumentError.value(offset, 'offset', 'Must be non-negative.'); } - return rawStage('offset', [offset]); + return _stage('offset', [offset], rawOptions: rawOptions); } /// Limits the number of returned inputs to [limit]. - Pipeline limit(int limit) { + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. + Pipeline limit(int limit, {Map rawOptions = const {}}) { if (limit < 0) { throw ArgumentError.value(limit, 'limit', 'Must be non-negative.'); } - return rawStage('limit', [limit]); + return _stage('limit', [limit], rawOptions: rawOptions); } /// Emits a document for each element of the array selected by [selectable]. /// /// Each emitted document has the array element assigned to the selection's - /// alias. [selectable] may be a [String] field name, a [PipelineField], or a - /// [PipelineAliasedExpression] created with [PipelineExpression.as] when the - /// element should land on a different field than the source array. + /// alias. [selectable] may be a field name (a [String] or a [FieldPath]), a + /// [PipelineField], or a [PipelineAliasedExpression] created with + /// [PipelineExpression.as] when the element should land on a different + /// field than the source array. A field name, an alias and [indexField] are + /// all read like [field]. /// /// When [indexField] is given, the element's zero-based index is assigned to /// that field. - Pipeline unnest(Object selectable, {String? indexField}) { - final (expression, alias) = _selectableParts(selectable); - return rawStage( + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do, and takes precedence over [indexField]. + Pipeline unnest( + Object selectable, { + String? indexField, + Map rawOptions = const {}, + }) { + final (expression, target) = _selectableParts(selectable); + return _stage( 'unnest', - [expression, field(alias)], + [expression, target], options: _compactOptions({ 'index_field': indexField == null ? null : field(indexField), }), + rawOptions: rawOptions, ); } /// Replaces each input document with the map produced by [expression]. - Pipeline replaceWith(Object expression) { - return rawStage('replace_with', [ + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. + Pipeline replaceWith( + Object expression, { + Map rawOptions = const {}, + }) { + return _stage('replace_with', [ _fieldOrExpression(expression), 'full_replace', - ]); + ], rawOptions: rawOptions); } /// Performs a union with [pipeline], including duplicates. /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. + /// /// Throws an [ArgumentError] when [pipeline] targets a different database /// than this Pipeline. - Pipeline union(Pipeline pipeline) { + Pipeline union( + Pipeline pipeline, { + Map rawOptions = const {}, + }) { _validateSameDatabase(firestore, pipeline.firestore, 'pipeline'); - return rawStage('union', [pipeline]); + return _stage('union', [pipeline], rawOptions: rawOptions); } /// Samples a fixed number or percentage of documents from the input. - Pipeline sample({int? documents, double? percentage}) { + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do. + Pipeline sample({ + int? documents, + double? percentage, + Map rawOptions = const {}, + }) { if ((documents == null) == (percentage == null)) { throw ArgumentError( 'Exactly one of documents or percentage must be provided.', @@ -1654,43 +2005,118 @@ final class Pipeline { ? (documents, 'documents') : (percentage!, 'percent'); - return rawStage('sample', [rate, mode]); + return _stage('sample', [rate, mode], rawOptions: rawOptions); } /// Performs vector nearest-neighbor search. /// - /// [queryVector] is a [VectorValue], a list of numbers, or an expression. + /// [vectorField] is a field name (a [String] or a [FieldPath], read like + /// [field]) or an expression. [queryVector] is a [VectorValue], a list of + /// numbers, or an expression. [distanceResultField] is the field name, a + /// [String] or a [FieldPath] read like [field], that each result's distance + /// is written to, as for [Query.findNearest]. + /// + /// The backend's `find_nearest` stage takes no distance threshold, so a + /// [distanceThreshold] adds a [where] stage after it, as + /// [PipelineSource.createFrom] does for a [VectorQuery]. It keeps the + /// results whose distance is at most [distanceThreshold] for + /// [DistanceMeasure.euclidean] and [DistanceMeasure.cosine], and at least + /// [distanceThreshold] for [DistanceMeasure.dotProduct], where a larger + /// value means more similar vectors. It compares [distanceResultField] when + /// given, and otherwise computes the distance again. Since it filters the + /// nearest neighbors, [limit] applies first. + /// + /// [rawOptions] sets stage options this SDK does not wrap yet, as + /// [rawStage]'s `options` do, and takes precedence over the typed options. Pipeline findNearest({ required Object vectorField, required Object queryVector, required DistanceMeasure distanceMeasure, int? limit, - String? distanceResultField, + Object? distanceResultField, double? distanceThreshold, + Map rawOptions = const {}, }) { - return rawStage( + return _findNearest( + vectorField: _fieldOrExpression(vectorField)!, + queryVector: queryVector, + distanceMeasure: distanceMeasure, + limit: limit, + distanceField: distanceResultField == null + ? null + : field(distanceResultField), + distanceThreshold: distanceThreshold, + rawOptions: rawOptions, + ); + } + + /// [findNearest], with the distance field already resolved, so that + /// [VectorQuery] conversion can pass a [FieldPath] one. + /// + /// [distanceThreshold] is never sent: the backend rejects a + /// `distance_threshold` option on `find_nearest` ("does not support + /// option(s)"), so it filters the stage's results instead. + Pipeline _findNearest({ + required Object vectorField, + required Object queryVector, + required DistanceMeasure distanceMeasure, + required int? limit, + required PipelineField? distanceField, + required double? distanceThreshold, + Map rawOptions = const {}, + }) { + final vector = _vectorOrExpression(queryVector, 'queryVector'); + final pipeline = _stage( 'find_nearest', - [ - if (vectorField is String) field(vectorField) else vectorField, - _vectorOrExpression(queryVector, 'queryVector'), - distanceMeasure.value.toLowerCase(), - ], + [vectorField, vector, distanceMeasure.value.toLowerCase()], options: _compactOptions({ 'limit': limit, - 'distance_field': distanceResultField == null - ? null - : field(distanceResultField), - 'distance_threshold': distanceThreshold, + 'distance_field': distanceField, }), + rawOptions: rawOptions, + ); + if (distanceThreshold == null) return pipeline; + + // The documents within the threshold are the nearest ones, so filtering + // the `limit` nearest neighbors keeps exactly the documents within the + // threshold, up to `limit`, as a VectorQuery returns. + final distance = + distanceField ?? + switch (distanceMeasure) { + DistanceMeasure.euclidean => PipelineFunctions.euclideanDistance( + vectorField, + vector, + ), + DistanceMeasure.cosine => PipelineFunctions.cosineDistance( + vectorField, + vector, + ), + DistanceMeasure.dotProduct => PipelineFunctions.dotProduct( + vectorField, + vector, + ), + }; + // A larger dot product means more similar vectors. + return pipeline.where( + distanceMeasure == DistanceMeasure.dotProduct + ? distance.greaterThanOrEqual(distanceThreshold) + : distance.lessThanOrEqual(distanceThreshold), ); } /// Adds a search stage. /// /// The search API is still evolving; [options] is passed through to the - /// backend stage as encoded Pipeline values. - Pipeline search(Map options) { - return rawStage('search', const [], options: options); + /// backend stage as encoded Pipeline values, keyed by the names the backend + /// expects. + /// + /// [rawOptions] is overlaid on [options] the way [rawStage] reads its + /// `options`, so its keys may be dot-separated paths. + Pipeline search( + Map options, { + Map rawOptions = const {}, + }) { + return _stage('search', const [], options: options, rawOptions: rawOptions); } /// Executes this Pipeline and returns the results. @@ -1702,10 +2128,20 @@ final class Pipeline { /// Pass [readTime] to read the database as it was at a past timestamp. To /// read inside a transaction, use [Transaction.executePipeline] instead. /// + /// [indexMode] is deprecated and ignored: the backend rejects the + /// `index_mode` option it used to send, and picks the indexes it recommends + /// by default. + /// /// Pass [explain] to ask the backend for planning stats, then read /// [PipelineSnapshot.explainStats]. [rawOptions] sets options this SDK does /// not wrap yet, keyed by the names the backend expects, and takes - /// precedence over the typed options above. + /// precedence over the typed options above. As in [rawStage]'s `options`, a + /// key may be a dot-separated path into a map option, merged with the typed + /// options: `explain` plus `{'explain_options.output_format': 'json'}` send + /// a single `explain_options` map holding both. + /// + /// Throws an [ArgumentError] when a key of [rawOptions] has an empty + /// segment, such as `''`, `'a.'` or `'a..b'`. /// /// ```dart /// final snapshot = await firestore @@ -1722,34 +2158,29 @@ final class Pipeline { /// ``` Future execute({ Timestamp? readTime, - PipelineIndexMode? indexMode, + @Deprecated(_indexModeDeprecation) PipelineIndexMode? indexMode, PipelineExplainOptions? explain, Map rawOptions = const {}, }) async { final result = await _execute( readTime: readTime, - options: _executeOptions( - indexMode: indexMode, - explain: explain, - rawOptions: rawOptions, - ), + options: _executeOptions(explain: explain, rawOptions: rawOptions), ); return result.result; } /// Builds the StructuredPipeline options, with [rawOptions] winning. - static Map _executeOptions({ - required PipelineIndexMode? indexMode, + /// + /// There is no `index_mode`: the backend rejects it ("Unsupported option: + /// index_mode"), so the deprecated `indexMode` parameters are ignored. + static _PipelineOptions _executeOptions({ required PipelineExplainOptions? explain, required Map rawOptions, }) { - return { - ..._compactOptions({ - 'index_mode': indexMode?.value, - 'explain_options': explain?._encoded, - }), - ...rawOptions, - }; + return _PipelineOptions( + known: _compactOptions({'explain_options': explain?._encoded}), + raw: rawOptions, + ); } /// Executes this Pipeline, optionally as part of a transaction. @@ -1761,7 +2192,7 @@ final class Pipeline { String? transactionId, Timestamp? readTime, firestore_v1.TransactionOptions? transactionOptions, - Map options = const {}, + _PipelineOptions options = const _PipelineOptions.none(), }) { final results = []; Timestamp? executionTime; @@ -1789,7 +2220,7 @@ final class Pipeline { database: 'projects/$projectId/databases/${firestore.databaseId}', structuredPipeline: firestore_v1.StructuredPipeline( pipeline: _toProto(), - options: _encodeOptions(options, firestore), + options: options._toProto(firestore), ), transaction: transactionId.let(base64Decode), newTransaction: transactionOptions, @@ -1865,16 +2296,20 @@ extension _QueryToPipeline on Query { final projections = options.projection?.fields ?? const []; if (projections.isNotEmpty) { + // The projection already holds canonical field paths. Node passes them + // back through field(), quoting a path such as `first-name` twice. pipeline = pipeline.select([ - for (final projection in projections) field(projection.fieldPath), + for (final projection in projections) + PipelineField._(projection.fieldPath), ]); } // Inequality fields are skipped here because `_toPipelineBooleanExpression` // has already emitted their existence checks. + final orderedFields = _createImplicitOrderBy(ignoreInequalityFields: true); final existsConditions = [ - for (final fieldOrder in _implicitOrderBy(ignoreInequalityFields: true)) - field(fieldOrder.fieldPath._formattedName).exists(), + for (final fieldOrder in orderedFields) + field(fieldOrder.fieldPath).exists(), ]; pipeline = pipeline.where( existsConditions.length == 1 @@ -1883,12 +2318,12 @@ extension _QueryToPipeline on Query { ); final orderings = [ - for (final fieldOrder in _implicitOrderBy()) + for (final fieldOrder in _createImplicitOrderBy()) PipelineOrdering._( fieldOrder.direction == _Direction.ascending ? 'ascending' : 'descending', - field(fieldOrder.fieldPath._formattedName), + field(fieldOrder.fieldPath), ), ]; @@ -1923,53 +2358,6 @@ extension _QueryToPipeline on Query { return pipeline; } - /// Mirrors the backend's implicit ordering rules for this query. - List<_FieldOrder> _implicitOrderBy({bool ignoreInequalityFields = false}) { - final fieldOrders = _queryOptions.fieldOrders.toList(); - final seen = {for (final fieldOrder in fieldOrders) fieldOrder.fieldPath}; - - // The implicit ordering always follows the last explicit order by. - final lastDirection = fieldOrders.isEmpty - ? _Direction.ascending - : fieldOrders.last.direction; - - if (!ignoreInequalityFields) { - // Inequality fields that are not explicitly ordered are ordered - // lexicographically, with the document key sorted last. - for (final inequalityField in _inequalityFilterFields()) { - // The document key is always appended last, below. - if (seen.contains(inequalityField) || - inequalityField == FieldPath.documentId) { - continue; - } - seen.add(inequalityField); - fieldOrders.add( - _FieldOrder(fieldPath: inequalityField, direction: lastDirection), - ); - } - } - - if (!seen.contains(FieldPath.documentId)) { - fieldOrders.add( - _FieldOrder(fieldPath: FieldPath.documentId, direction: lastDirection), - ); - } - - return fieldOrders; - } - - /// The inequality filter fields of this query, sorted lexicographically. - List _inequalityFilterFields() { - final fields = { - for (final filter in _queryOptions.filters) - for (final subFilter in filter.flattenedFilters) - if (subFilter.isInequalityFilter) subFilter.field, - }; - - return fields.toList() - ..sort((a, b) => a._formattedName.compareTo(b._formattedName)); - } - PipelineBooleanExpression _toPipelineBooleanExpression( _FilterInternal filter, ) { @@ -1989,7 +2377,7 @@ extension _QueryToPipeline on Query { PipelineBooleanExpression _fieldFilterToPipelineBooleanExpression( _FieldFilterInternal filter, ) { - final target = field(filter.field._formattedName); + final target = field(filter.field); final value = _PipelineProtoValue( firestore._serializer.encodeValue(filter.value) ?? firestore_v1.Value(nullValue: protobuf_v1.NullValue.nullValue), @@ -2042,6 +2430,11 @@ List _reversed(List orderings) { /// Cursors compare the ordering expressions lexicographically, so each bound /// contributes either a strict comparison or an equality plus the condition for /// the remaining bounds. +/// +/// A cursor is a position in the query's order, so a descending ordering +/// flips the comparison: `startAt` keeps smaller values. Node's +/// `whereConditionsFromCursor` ignores the direction and keeps the wrong side +/// of a descending bound. PipelineBooleanExpression _cursorCondition( _QueryCursor cursor, List orderings, { @@ -2056,31 +2449,28 @@ PipelineBooleanExpression _cursorCondition( ); } - PipelineBooleanExpression compare(Object? expression, Object? value) { - return before - ? _comparison('less_than', expression, value) - : _comparison('greater_than', expression, value); - } - - PipelineBooleanExpression equals(Object? expression, Object? value) { - return _comparison('equal', expression, value); + PipelineBooleanExpression compare(PipelineOrdering ordering, Object? value) { + final ascending = ordering._name == 'ascending'; + return before == ascending + ? lessThan(ordering._expression, value) + : greaterThan(ordering._expression, value); } var expression = orderings[size - 1]._expression; var value = _PipelineProtoValue(cursor.values[size - 1]); - var condition = compare(expression, value); + var condition = compare(orderings[size - 1], value); // An inclusive bound also matches the cursor value itself. if (before != cursor.before) { - condition = or([condition, equals(expression, value)]); + condition = or([condition, equal(expression, value)]); } for (var i = size - 2; i >= 0; i--) { expression = orderings[i]._expression; value = _PipelineProtoValue(cursor.values[i]); condition = or([ - compare(expression, value), - and([equals(expression, value), condition]), + compare(orderings[i], value), + and([equal(expression, value), condition]), ]); } @@ -2088,18 +2478,27 @@ PipelineBooleanExpression _cursorCondition( } /// Translates the [VectorQuery] surface onto Pipeline stages. +/// +/// Mirrors the Node SDK's `VectorQuery._pipeline()`, except that it keeps the +/// options Node drops: the distance result field becomes find_nearest's +/// `distance_field`, and the distance threshold a filter on the distance. extension _VectorQueryToPipeline on VectorQuery { Pipeline _toPipeline(Firestore firestore) { - final vectorField = _rawVectorField; + // Both fields are a String or a FieldPath; field() reads either. + final vectorField = field(_options.vectorField); + final distanceResultField = _options.distanceResultField; + return _query ._toPipeline(firestore) - .where(field(vectorField).exists()) - .findNearest( - vectorField: field(vectorField), + .where(vectorField.exists()) + ._findNearest( + vectorField: vectorField, queryVector: FieldValue.vector(_rawQueryVector), distanceMeasure: _options.distanceMeasure, limit: _options.limit, - distanceResultField: _rawDistanceResultField, + distanceField: distanceResultField == null + ? null + : field(distanceResultField), distanceThreshold: _options.distanceThreshold, ); } @@ -2176,6 +2575,12 @@ final class PipelineResult { /// /// The returned map is unmodifiable. Projection stages may drop every field, /// in which case this is empty rather than `null`. + /// + /// A reference value decodes to a [DocumentReference] when it names a + /// document. A reference to anything else, such as the database root that + /// `parent()` returns for a top-level document, or a collection, decodes to + /// its resource name as a [String]. This holds at every depth, including + /// inside maps and arrays. The Node SDK throws for such references instead. DocumentData data() => _data; /// Returns the decoded value at [field], or `null` when absent. @@ -2214,36 +2619,41 @@ final class PipelineResult { } } +/// Decodes a Pipeline result value, decoding references as documented on +/// [PipelineResult.data] at every depth. Object? _decodePipelineResultValue( firestore_v1.Value value, Firestore firestore, ) { - final referenceValue = value.referenceValue; - if (referenceValue != null && - referenceValue.isNotEmpty && - !_isDocumentReferenceValue(referenceValue)) { - return referenceValue; - } - return firestore._serializer.decodeValue(value); + return firestore._serializer._decodeValue(value, (referenceValue) { + final documentPath = _documentPathOf(referenceValue); + return documentPath == null ? referenceValue : firestore.doc(documentPath); + }); } final _documentReferenceRegExp = RegExp( r'^projects/[^/]+/databases/[^/]+(?:/documents(?:/(.*))?)?$', ); -bool _isDocumentReferenceValue(String referenceValue) { +/// The database-relative path of [referenceValue] when it names a document, +/// or `null` for any other reference, such as the database root or a +/// collection. +String? _documentPathOf(String referenceValue) { final value = referenceValue.startsWith('/') ? referenceValue.substring(1) : referenceValue; final match = _documentReferenceRegExp.firstMatch(value); if (match == null) { - return false; + return null; } - final path = match.group(1); - if (path == null || path.isEmpty) { - return false; + final segments = (match.group(1) ?? '') + .split('/') + .where((segment) => segment.isNotEmpty) + .toList(); + if (segments.isEmpty || segments.length.isOdd) { + return null; } - return path.split('/').where((segment) => segment.isNotEmpty).length.isEven; + return segments.join('/'); } /// A snapshot returned by executing a Firestore Pipeline operation. @@ -2285,6 +2695,11 @@ sealed class PipelineExpression { firestore_v1.Value _toValue(Firestore firestore); /// Assigns [alias] to this expression for projection-style stages. + /// + /// The backend reads [alias] as a field path, so it must be a valid one: a + /// segment that is not an identifier, such as `total price`, must be + /// backtick-quoted (`` '`total price`' ``), and a dot separates the + /// segments of a nested field. PipelineAliasedExpression as(String alias) { return PipelineAliasedExpression._(this, alias); } @@ -2322,9 +2737,15 @@ sealed class PipelineExpression { return PipelineFunctions.greaterThanOrEqual(this, other); } - /// Creates an addition expression. - PipelineExpression add(Object? other) { - return PipelineFunctions.add(this, other); + /// Adds [second] and any [others] to this expression. + /// + /// Further operands are sent as nested two-operand `add` calls; see + /// [PipelineFunctions.add]. + PipelineExpression add( + Object? second, [ + Iterable others = const [], + ]) { + return PipelineFunctions.add(this, second, others); } /// Creates a subtraction expression. @@ -2332,9 +2753,15 @@ sealed class PipelineExpression { return PipelineFunctions.subtract(this, other); } - /// Creates a multiplication expression. - PipelineExpression multiply(Object? other) { - return PipelineFunctions.multiply(this, other); + /// Multiplies this expression by [second] and any [others]. + /// + /// Further operands are sent as nested two-operand `multiply` calls; see + /// [PipelineFunctions.multiply]. + PipelineExpression multiply( + Object? second, [ + Iterable others = const [], + ]) { + return PipelineFunctions.multiply(this, second, others); } /// Creates a division expression. @@ -2456,7 +2883,9 @@ sealed class PipelineExpression { } /// Checks if this array contains all [values]. - PipelineBooleanExpression arrayContainsAll(Iterable values) { + /// + /// [values] is a list of values or an array expression. + PipelineBooleanExpression arrayContainsAll(Object? values) { return PipelineFunctions.arrayContainsAll(this, values); } @@ -2466,7 +2895,9 @@ sealed class PipelineExpression { } /// Checks if this array contains any [values]. - PipelineBooleanExpression arrayContainsAny(Iterable values) { + /// + /// [values] is a list of values or an array expression. + PipelineBooleanExpression arrayContainsAny(Object? values) { return PipelineFunctions.arrayContainsAny(this, values); } @@ -2532,11 +2963,10 @@ sealed class PipelineExpression { /// Reverses this array expression. PipelineExpression arrayReverse() => PipelineFunctions.arrayReverse(this); - /// Returns a slice of this array expression. + /// Returns [length] elements of this array expression starting at index + /// [offset]; when [length] is omitted the slice runs to the end. PipelineExpression arraySlice(Object? offset, [Object? length]) { - return length == null - ? PipelineFunctions.raw('array_slice', [this, offset]) - : PipelineFunctions.arraySlice(this, offset, length); + return PipelineFunctions.arraySlice(this, offset, length); } /// Returns the sum of numeric elements in this array expression. @@ -2553,11 +2983,11 @@ sealed class PipelineExpression { String indexAlias, Object? transform, ) { - return PipelineFunctions.arrayTransform( + return PipelineFunctions.arrayTransformWithIndex( this, elementAlias, - transform, indexAlias, + transform, ); } @@ -2947,21 +3377,26 @@ sealed class PipelineBooleanExpression extends PipelineExpression { } /// A Pipeline field reference. +/// +/// Create one with [field] or [Expression.field]. @immutable final class PipelineField extends PipelineExpression { + /// Wraps [path], which must already be canonical: build it with + /// [_canonicalFieldPath] or [FieldPath._formattedName]. const PipelineField._(this.path); - /// The field path referenced by this expression. + /// The field path referenced by this expression, in the canonical form sent + /// to the backend. + /// + /// Segments are joined with dots, and a segment that is not a simple + /// identifier is quoted with backticks: `field('address.city').path` is + /// `address.city`, `field('first-name').path` is `` `first-name` `` and + /// `field(FieldPath(['a.b'])).path` is `` `a.b` ``. Like the Node SDK's + /// `Field.fieldName`, it is also the name [Pipeline.select], + /// [Pipeline.distinct] and the `groups` of [Pipeline.aggregate] key this + /// field by. final String path; - /// Creates an ascending ordering for this field. - @override - PipelineOrdering ascending() => PipelineOrdering._('ascending', this); - - /// Creates a descending ordering for this field. - @override - PipelineOrdering descending() => PipelineOrdering._('descending', this); - @override firestore_v1.Value _toValue(Firestore firestore) { return firestore_v1.Value(fieldReferenceValue: path); @@ -3001,6 +3436,20 @@ final class PipelineOrdering { final String _name; final Object _expression; + /// The expression this ordering sorts by. + /// + /// A field name passed to [ascending] or [descending] reads back as a + /// [PipelineField]. + PipelineExpression get expr { + return switch (_expression) { + final PipelineExpression expression => expression, + final value => constant(value), + }; + } + + /// The sort direction: `'ascending'` or `'descending'`. + String get direction => _name; + firestore_v1.Value _toValue(Firestore firestore) { return firestore_v1.Value( mapValue: firestore_v1.MapValue( @@ -3083,17 +3532,85 @@ final class _PipelineBooleanCastExpression extends PipelineBooleanExpression { } final class _PipelineStage { - const _PipelineStage(this.name, this.args, [this.options = const {}]); + const _PipelineStage(this.name, this.args, this.options); final String name; final List args; - final Map options; + final _PipelineOptions options; firestore_v1.Pipeline_Stage _toProto(Firestore firestore) { return firestore_v1.Pipeline_Stage( name: name, args: [for (final arg in args) _encodePipelineValue(arg, firestore)], - options: _encodeOptions(options, firestore), + options: options._toProto(firestore), + ); + } +} + +/// The options of a stage or of a Pipeline execution. +/// +/// Mirrors the Node SDK's `OptionsUtil.getOptionsProto`. [known] holds the +/// options this SDK types, under their backend names. [raw] holds the +/// caller's raw options, which are overlaid on [known] one key at a time, in +/// order. A [raw] key is a dot-separated path: `'explain_options.mode'` sets +/// `mode` inside the `explain_options` map and keeps the map's other entries, +/// creating the map, or replacing a value that is not a map, on the way. A +/// key without a dot replaces the whole option. Segments are not unescaped, +/// so a backtick is part of the name. +@immutable +final class _PipelineOptions { + /// Throws an [ArgumentError], naming the parameter [rawName], when a key of + /// [raw] has an empty segment, which the Node SDK rejects too. + _PipelineOptions({ + this.known = const {}, + Map raw = const {}, + String rawName = 'rawOptions', + }) : raw = _validateRawOptions(raw, rawName); + + const _PipelineOptions.none() : known = const {}, raw = const {}; + + final Map known; + final Map raw; + + static Map _validateRawOptions( + Map raw, + String name, + ) { + for (final key in raw.keys) { + if (key.split('.').any((segment) => segment.isEmpty)) { + throw ArgumentError.value( + key, + name, + 'Option keys must be dot-separated paths without empty segments.', + ); + } + } + return raw; + } + + Map _toProto(Firestore firestore) { + final result = _encodeOptions(known, firestore); + for (final MapEntry(:key, :value) in raw.entries) { + _setPath(result, key.split('.'), _encodePipelineValue(value, firestore)); + } + return result; + } + + /// Sets [value] at [path] inside [fields], descending into map values. + static void _setPath( + Map fields, + List path, + firestore_v1.Value value, + ) { + final [segment, ...rest] = path; + if (rest.isEmpty) { + fields[segment] = value; + return; + } + final nested = {...?fields[segment]?.mapValue?.fields}; + _setPath(nested, rest, value); + fields[segment] = firestore_v1.Value( + mapValue: firestore_v1.MapValue(fields: nested), ); } } @@ -3222,8 +3739,16 @@ Map _compactOptions(Map options) { /// Keys each selection's expression by the field name or alias it lands on. /// /// Throws an [ArgumentError] on a repeated key instead of silently keeping the -/// last entry. A [String] or [PipelineField] lands on its own path, so it +/// last entry. A field name or [PipelineField] lands on its own path, so it /// collides with an alias of the same name, matching the Node SDK. +/// +/// The backend reads each key as a field path, and rejects one that is not +/// an identifier unless it is quoted ("Invalid property path \"last +/// name\""). So a field name ([String] or [FieldPath]) is keyed by the +/// canonical [PipelineField.path] of [field], as a [PipelineField] is: +/// `select(['first-name', field('last name')])` keys `` `first-name` `` and +/// `` `last name` ``. The Node SDK's `selectablesToObject` keys a string by +/// the string itself. An alias is kept as written, as in Node. Map _projectionMap( Iterable selections, { String argumentName = 'selections', @@ -3239,32 +3764,40 @@ Map _projectionMap( return result; } -/// Splits a selectable into the expression it computes and the alias it lands +/// Splits a selectable into the expression it computes and the field it lands /// on. /// -/// A [String] or [PipelineField] is its own alias, matching the Node SDK where -/// `Field._alias` is the field name. -(Object expression, String alias) _selectableParts(Object selectable) { - return switch (selectable) { - String() => (field(selectable), selectable), - PipelineField() => (selectable, selectable.path), - PipelineAliasedExpression() => (selectable.expression, selectable.name), +/// A field name ([String] or [FieldPath]) or [PipelineField] lands on its own +/// path, matching the Node SDK where `Field._alias` is the field name. An +/// alias is a field path, as in Node's `field(alias)`. +/// +/// Node rebuilds the field from `Field._alias`, its already-quoted name, so it +/// quotes a field such as `last name` twice. The field is reused as is here. +(Object expression, PipelineField target) _selectableParts(Object selectable) { + return switch (_fieldOrExpression(selectable)) { + final PipelineField target => (target, target), + final PipelineAliasedExpression aliased => ( + aliased.expression, + field(aliased.name), + ), _ => throw ArgumentError.value( selectable, 'selectable', - 'Expected a String, PipelineField, or PipelineAliasedExpression. ' - 'Computed expressions must be aliased with as().', + 'Expected a String, FieldPath, PipelineField, or ' + 'PipelineAliasedExpression. Computed expressions must be aliased ' + 'with as().', ), }; } +/// The key and the expression of a selection, a field name being read like +/// [field]. MapEntry _projectionEntry(Object selection) { - return switch (selection) { - String() => MapEntry(selection, field(selection)), - PipelineField() => MapEntry(selection.path, selection), - PipelineAliasedExpression() => MapEntry( - selection.name, - selection.expression, + return switch (_fieldOrExpression(selection)) { + final PipelineField target => MapEntry(target.path, target), + final PipelineAliasedExpression aliased => MapEntry( + aliased.name, + aliased.expression, ), PipelineExpression() => throw ArgumentError.value( selection, @@ -3274,7 +3807,8 @@ MapEntry _projectionEntry(Object selection) { _ => throw ArgumentError.value( selection, 'selections', - 'Expected a String, PipelineField, or PipelineAliasedExpression.', + 'Expected a String, FieldPath, PipelineField, or ' + 'PipelineAliasedExpression.', ), }; } diff --git a/packages/google_cloud_firestore/lib/src/reference/composite_filter_internal.dart b/packages/google_cloud_firestore/lib/src/reference/composite_filter_internal.dart index bd9d3c45..14ae7b73 100644 --- a/packages/google_cloud_firestore/lib/src/reference/composite_filter_internal.dart +++ b/packages/google_cloud_firestore/lib/src/reference/composite_filter_internal.dart @@ -31,13 +31,6 @@ class _CompositeFilterInternal extends _FilterInternal { return allFilters..addAll(subFilter.flattenedFilters); }); - @override - FieldPath? get firstInequalityField { - return flattenedFilters - .firstWhereOrNull((filter) => filter.isInequalityFilter) - ?.field; - } - @override firestore_v1.StructuredQuery_Filter toProto() { if (filters.length == 1) return filters.single.toProto(); diff --git a/packages/google_cloud_firestore/lib/src/reference/field_filter_internal.dart b/packages/google_cloud_firestore/lib/src/reference/field_filter_internal.dart index 7032996f..f35829a9 100644 --- a/packages/google_cloud_firestore/lib/src/reference/field_filter_internal.dart +++ b/packages/google_cloud_firestore/lib/src/reference/field_filter_internal.dart @@ -33,14 +33,21 @@ class _FieldFilterInternal extends _FilterInternal { @override List<_FieldFilterInternal> get filters => [this]; - @override - FieldPath? get firstInequalityField => isInequalityFilter ? field : null; - + /// Whether the backend implicitly orders by this filter's field: `<`, `<=`, + /// `>`, `>=`, `!=` and `not-in`, as in the Node SDK. bool get isInequalityFilter { - return op == WhereFilter.lessThan || - op == WhereFilter.lessThanOrEqual || - op == WhereFilter.greaterThan || - op == WhereFilter.greaterThanOrEqual; + return switch (op) { + WhereFilter.lessThan || + WhereFilter.lessThanOrEqual || + WhereFilter.greaterThan || + WhereFilter.greaterThanOrEqual || + WhereFilter.notEqual || + WhereFilter.notIn => true, + WhereFilter.equal || + WhereFilter.arrayContains || + WhereFilter.isIn || + WhereFilter.arrayContainsAny => false, + }; } @override diff --git a/packages/google_cloud_firestore/lib/src/reference/filter_internal.dart b/packages/google_cloud_firestore/lib/src/reference/filter_internal.dart index 851280a7..acfe9e01 100644 --- a/packages/google_cloud_firestore/lib/src/reference/filter_internal.dart +++ b/packages/google_cloud_firestore/lib/src/reference/filter_internal.dart @@ -22,9 +22,6 @@ sealed class _FilterInternal { /// Returns a list of all filters that are contained within this filter List<_FilterInternal> get filters; - /// Returns the field of the first filter that's an inequality, or null if none. - FieldPath? get firstInequalityField; - /// Returns the proto representation of this filter firestore_v1.StructuredQuery_Filter toProto(); diff --git a/packages/google_cloud_firestore/lib/src/reference/query.dart b/packages/google_cloud_firestore/lib/src/reference/query.dart index 8edbc143..cae15c3b 100644 --- a/packages/google_cloud_firestore/lib/src/reference/query.dart +++ b/packages/google_cloud_firestore/lib/src/reference/query.dart @@ -132,7 +132,11 @@ interface class Query { ); } - final fieldOrders = _createImplicitOrderBy(snapshot); + // A DocumentSnapshot cursor takes its values from the snapshot for every + // field the backend orders by, including the implicit orderings. + final fieldOrders = snapshot == null + ? _queryOptions.fieldOrders + : _createImplicitOrderBy(); final cursor = _createCursor( fieldOrders, fieldValues: fieldValues, @@ -142,37 +146,35 @@ interface class Query { return (cursor, fieldOrders); } - /// Computes the backend ordering semantics for DocumentSnapshot cursors. - List<_FieldOrder> _createImplicitOrderBy( - DocumentSnapshot? snapshot, - ) { - // Add an implicit orderBy if the only cursor value is a DocumentSnapshot - // or a DocumentReference. - if (snapshot == null) return _queryOptions.fieldOrders; - + /// The orderings the backend applies to this query: the explicit orderBy + /// clauses, then every other inequality field, then the document key. + /// + /// Mirrors the Node SDK's `createImplicitOrderBy`. The implicit orderings + /// take the direction of the last explicit orderBy, or ascending without + /// one. [ignoreInequalityFields] leaves out the inequality fields. + List<_FieldOrder> _createImplicitOrderBy({ + bool ignoreInequalityFields = false, + }) { final fieldOrders = _queryOptions.fieldOrders.toList(); - - // If no explicit ordering is specified, use the first inequality to - // define an implicit order. - if (fieldOrders.isEmpty) { - for (final filter in _queryOptions.filters) { - final fieldReference = filter.firstInequalityField; - if (fieldReference != null) { - fieldOrders.add(_FieldOrder(fieldPath: fieldReference)); - break; + final seen = {for (final fieldOrder in fieldOrders) fieldOrder.fieldPath}; + + final lastDirection = fieldOrders.isEmpty + ? _Direction.ascending + : fieldOrders.last.direction; + + if (!ignoreInequalityFields) { + for (final inequalityField in _inequalityFilterFields()) { + // The document key is always ordered last, below. + if (inequalityField == FieldPath.documentId) continue; + if (seen.add(inequalityField)) { + fieldOrders.add( + _FieldOrder(fieldPath: inequalityField, direction: lastDirection), + ); } } } - final hasDocumentId = fieldOrders.any( - (fieldOrder) => fieldOrder.fieldPath == FieldPath.documentId, - ); - if (!hasDocumentId) { - // Add implicit sorting by name, using the last specified direction. - final lastDirection = fieldOrders.isEmpty - ? _Direction.ascending - : fieldOrders.last.direction; - + if (!seen.contains(FieldPath.documentId)) { fieldOrders.add( _FieldOrder(fieldPath: FieldPath.documentId, direction: lastDirection), ); @@ -181,6 +183,17 @@ interface class Query { return fieldOrders; } + /// The fields of this query's inequality filters (`<`, `<=`, `>`, `>=`, + /// `!=` and `not-in`), in field path order. + List _inequalityFilterFields() { + final fields = { + for (final filter in _queryOptions.filters) + for (final subFilter in filter.flattenedFilters) + if (subFilter.isInequalityFilter) subFilter.field, + }; + return fields.toList()..sort(_compareFieldPaths); + } + /// Creates and returns a new [Query] that starts at the provided /// set of field values relative to the order of the query. The order of the /// provided values must match the order of the order by clauses of the query. @@ -1226,3 +1239,16 @@ interface class Query { return VectorQuery._(query: this, options: options); } } + +/// Orders field paths segment by segment, comparing segments by their UTF-8 +/// encoding, as the Node SDK's `FieldPath.compareTo` does. +/// +/// This differs from comparing formatted paths: `a.c` sorts before `` `a-b` ``. +int _compareFieldPaths(FieldPath left, FieldPath right) { + final length = math.min(left.segments.length, right.segments.length); + for (var i = 0; i < length; i++) { + final comparison = _compareUtf8Strings(left.segments[i], right.segments[i]); + if (comparison != 0) return comparison; + } + return left.segments.length.compareTo(right.segments.length); +} diff --git a/packages/google_cloud_firestore/lib/src/serializer.dart b/packages/google_cloud_firestore/lib/src/serializer.dart index c4ea3f19..96f77b1d 100644 --- a/packages/google_cloud_firestore/lib/src/serializer.dart +++ b/packages/google_cloud_firestore/lib/src/serializer.dart @@ -131,7 +131,25 @@ class Serializer { } /// Decodes a single Firestore 'Value' Protobuf. - Object? decodeValue(Object? proto) { + /// + /// A reference value must name a document and decodes to a + /// [DocumentReference]; any other reference throws an [ArgumentError]. + Object? decodeValue(Object? proto) => _decodeValue(proto, _decodeReference); + + /// Decodes a document reference value, as [decodeValue] does. + DocumentReference _decodeReference(String referenceValue) { + final resourcePath = _QualifiedResourcePath.fromSlashSeparatedString( + referenceValue, + ); + return firestore.doc(resourcePath.relativeName); + } + + /// Decodes [proto] like [decodeValue], but hands every reference value, at + /// any depth, to [decodeReference]. + Object? _decodeValue( + Object? proto, + Object? Function(String referenceValue) decodeReference, + ) { if (proto is! firestore_v1.Value) { throw ArgumentError.value( proto, @@ -153,13 +171,12 @@ class Serializer { case firestore_v1.Value(:final timestampValue?): return Timestamp._fromProto(timestampValue); case firestore_v1.Value(:final referenceValue?): - final resourcePath = _QualifiedResourcePath.fromSlashSeparatedString( - referenceValue, - ); - return firestore.doc(resourcePath.relativeName); + return decodeReference(referenceValue); case firestore_v1.Value(:final arrayValue?): final values = arrayValue.values; - return [for (final value in values) decodeValue(value)]; + return [ + for (final value in values) _decodeValue(value, decodeReference), + ]; case firestore_v1.Value(nullValue: != null): return null; case firestore_v1.Value(:final mapValue?): @@ -176,7 +193,7 @@ class Serializer { } return { for (final entry in fields.entries) - entry.key: decodeValue(entry.value), + entry.key: _decodeValue(entry.value, decodeReference), }; case firestore_v1.Value(:final geoPointValue?): return GeoPoint._fromProto(geoPointValue); diff --git a/packages/google_cloud_firestore/lib/src/transaction.dart b/packages/google_cloud_firestore/lib/src/transaction.dart index c2b9821c..e94c216f 100644 --- a/packages/google_cloud_firestore/lib/src/transaction.dart +++ b/packages/google_cloud_firestore/lib/src/transaction.dart @@ -145,8 +145,9 @@ class Transaction { /// Executes a Pipeline and returns the results as part of this transaction. /// - /// The Pipeline is executed at the transaction's snapshot. [indexMode], - /// [explain] and [rawOptions] behave as they do on [Pipeline.execute]. + /// The Pipeline is executed at the transaction's snapshot. [explain] and + /// [rawOptions] behave as they do on [Pipeline.execute]. [indexMode] is + /// deprecated and ignored, as it is there. /// /// ```dart /// firestore.runTransaction((transaction) async { @@ -164,7 +165,7 @@ class Transaction { /// ``` Future executePipeline( Pipeline pipeline, { - PipelineIndexMode? indexMode, + @Deprecated(_indexModeDeprecation) PipelineIndexMode? indexMode, PipelineExplainOptions? explain, Map rawOptions = const {}, }) async { @@ -182,7 +183,6 @@ class Transaction { ); final options = Pipeline._executeOptions( - indexMode: indexMode, explain: explain, rawOptions: rawOptions, ); diff --git a/packages/google_cloud_firestore/pubspec.yaml b/packages/google_cloud_firestore/pubspec.yaml index 02c4a9b5..444ea828 100644 --- a/packages/google_cloud_firestore/pubspec.yaml +++ b/packages/google_cloud_firestore/pubspec.yaml @@ -25,6 +25,10 @@ dependencies: retry: ^3.1.2 dev_dependencies: + # Used by the analyzer-based Pipeline tests (API parity, single + # implementation, E2E coverage). Spans the releases that run on the minimum + # SDK (up to 13.0.0 on Dart 3.9) and the newer ones (14.x needs Dart 3.11). + analyzer: '>=10.0.0 <15.0.0' build_runner: ^2.4.7 mocktail: ^1.0.2 test: ^1.26.3 diff --git a/packages/google_cloud_firestore/test/e2e/README.md b/packages/google_cloud_firestore/test/e2e/README.md index cfc1f80f..ea30f30c 100644 --- a/packages/google_cloud_firestore/test/e2e/README.md +++ b/packages/google_cloud_firestore/test/e2e/README.md @@ -13,16 +13,67 @@ suite somewhere it cannot pass. export FIRESTORE_PIPELINE_E2E_PROJECT_ID="" export FIRESTORE_PIPELINE_E2E_DATABASE_ID="firestore-pipeline-test" export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json" -dart test -P prod test/e2e/pipeline_e2e_test.dart +dart test -P prod test/e2e/pipeline/ --concurrency=1 ``` The credential needs permission to create, read, query, and delete documents in that database — `roles/datastore.user` covers it. -The suite writes temporary documents, executes Pipeline queries against them, -and deletes the documents in teardown. It covers source stages, projection, -sorting, aggregates, result metadata, vector search, and the supported -expression helpers. +## Layout + +The suite lives in `test/e2e/pipeline/`, one file per area: + +| File | Covers | +| --- | --- | +| `harness.dart` | Shared setup, seed data and case helpers (not a test file). | +| `sources_test.dart` | `Firestore.pipeline` and the `PipelineSource` stages. | +| `stages_test.dart` | `Pipeline` stages, orderings and aliases. | +| `results_test.dart` | `PipelineSnapshot` / `PipelineResult` accessors and value decoding. | +| `execute_test.dart` | `execute` options, explain stats, `Transaction.executePipeline`. | +| `aggregates_test.dart` | Aggregate functions and the `aggregate` stage. | +| `functions__test.dart` | One file per function family: `comparison_logical`, `arithmetic`, `string`, `array`, `map_reference`, `timestamp_vector`, `type_debug`. | + +Each file wraps its tests in a single `pipelineE2E(...)` call, which writes the +documents described by `buildSeed` in `harness.dart` once per file — under a +run ID unique to that file — and deletes them in `tearDownAll`. `buildSeed`'s +doc comment lists every seeded field and value; compute expectations from it. + +Prefer the table-driven helpers, which declare one test per case so a single +backend rejection never hides another: + +```dart +pipelineE2E('Pipeline arithmetic functions', (ctx) { + ctx.functionCases('round', [ + // Evaluated on book 1 as select([expression.as('v')]). + FunctionCase('static, omitted decimals', PipelineFunctions.round('half'), isNumber(3)), + FunctionCase('fluent, with decimals', field('precise').round(2), isNumber(4.12)), + ]); +}); +``` + +`ctx.aggregateCases` evaluates `aggregate([...])` over the run's three +documents, and `ctx.filterCases` checks which books a `where` condition keeps. +`isInt`, `isDouble` and `isNumber` match numeric results by wire type. + +Test files import only `package:google_cloud_firestore/google_cloud_firestore.dart`, +`package:test/test.dart` and the harness (plus `dart:` libraries), so a public +API missing from the barrel fails to compile here. + +## Coverage guard + +`test/pipeline_e2e_coverage_test.dart` is an ordinary, credential-free unit +test. It resolves these files with `package:analyzer` and fails unless every +public Pipeline API member — static and fluent forms counted separately — is +referenced here, every optional parameter is both supplied and omitted, every +`Object`-typed value position receives both an expression and a plain value, +and a few listed parameters receive specific shapes (collections holding +expressions, numeric vector lists, dotted and `FieldPath` result paths). Its +report groups gaps by family, naming the file that owns each one. The rules, +the family tables and the few exemptions are documented at the top of the +guard. + +Adding a public Pipeline function therefore means adding a live case for it +here; the guard fails until you do. Vector nearest-neighbor coverage requires this vector index: @@ -89,7 +140,7 @@ IDs, so the two can run concurrently without interfering. a misconfigured job would report a green build for tests that never ran. - `concurrency` is scoped per ref with `cancel-in-progress`, so two runs never write to the database on the same ref at once. A cancelled run does skip its - `tearDown`, which leaves that run's `run__book_*` documents behind. + `tearDownAll`, which leaves that run's `run__book_*` documents behind. They are inert — every assertion filters on `runId` — but `pipeline_e2e_books` is worth sweeping occasionally. diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/aggregates_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/aggregates_test.dart new file mode 100644 index 00000000..f3f11911 --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/aggregates_test.dart @@ -0,0 +1,265 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of the aggregate functions and the `aggregate` stage. +@Tags(['prod']) +library; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +import 'harness.dart'; + +void main() { + pipelineE2E('Pipeline aggregates', (ctx) { + test('executes aggregate pipeline stages', () async { + final aggregateSnapshot = await ctx.firestore + .pipeline() + .collection(ctx.collectionPath) + .where(ctx.runFilter(Expression.field('active').equal(true))) + .aggregate([ + Expression.field('price').sum().as('totalPrice'), + Expression.field('rating').average().as('averageRating'), + Expression.field('title').count().as('bookCount'), + ]) + .execute(); + + expect(aggregateSnapshot.results, hasLength(1)); + expect(aggregateSnapshot.results.single.get('totalPrice'), 30); + expect(aggregateSnapshot.results.single.get('averageRating'), 4.5); + expect(aggregateSnapshot.results.single.get('bookCount'), 2); + }); + + // Aggregated over the run's three books; see buildSeed in harness.dart. + ctx.aggregateCases('count', [ + // count() with no argument counts every input, like countAll(). + AggregateCase('static, no argument', PipelineFunctions.count(), 3), + AggregateCase('countAll', PipelineFunctions.countAll(), 3), + // Only book 1 has `sparse`, and no book has `missing`. + AggregateCase('static, field name', PipelineFunctions.count('sparse'), 1), + AggregateCase( + 'static, expression', + PipelineFunctions.count(Expression.field('missing')), + 0, + ), + AggregateCase('fluent', Expression.field('title').count(), 3), + ]); + + ctx.aggregateCases('countIf', [ + AggregateCase( + 'static, expression', + PipelineFunctions.countIf(Expression.field('active')), + 2, + ), + // A constant true condition holds for every input. + AggregateCase('static, constant', PipelineFunctions.countIf(true), 3), + // price > 15: books 2 and 3. + AggregateCase( + 'fluent', + Expression.field('price').greaterThan(15).countIf(), + 2, + ), + ]); + + ctx.aggregateCases('countDistinct', [ + // active: true, true, false. + AggregateCase( + 'static, field name', + PipelineFunctions.countDistinct('active'), + 2, + ), + // half is 2.5 on every book. + AggregateCase( + 'static, expression', + PipelineFunctions.countDistinct(Expression.field('half')), + 1, + ), + AggregateCase( + 'fluent', + Expression.field('metadata').mapGetLiteral('lang').countDistinct(), + 1, + ), + ]); + + ctx.aggregateCases('sum', [ + // rating: 5 + 4 + 2. + AggregateCase('static, field name', PipelineFunctions.sum('rating'), 11), + // score: -12.7 + 3.2 + 8.9. + AggregateCase( + 'static, expression', + PipelineFunctions.sum(Expression.field('score')), + isNumber(-0.6), + ), + AggregateCase('fluent', Expression.field('price').sum(), 60), + ]); + + ctx.aggregateCases('average', [ + // price: (10 + 20 + 30) / 3. + AggregateCase( + 'static, field name', + PipelineFunctions.average('price'), + isDouble(20), + ), + // ratio: (0.5 + 1.25 + 2.0) / 3. + AggregateCase( + 'static, expression', + PipelineFunctions.average(Expression.field('ratio')), + isDouble(1.25), + ), + // rating: (5 + 4 + 2) / 3. + AggregateCase( + 'fluent', + Expression.field('rating').average(), + isDouble(11 / 3), + ), + ]); + + ctx.aggregateCases('minimum', [ + AggregateCase( + 'static, field name', + PipelineFunctions.minimum('score'), + isDouble(-12.7), + ), + AggregateCase( + 'static, expression on timestamps', + PipelineFunctions.minimum(Expression.field('createdAt')), + Timestamp(seconds: 1700000000, nanoseconds: 0), + ), + AggregateCase('fluent', Expression.field('price').minimum(), 10), + ]); + + ctx.aggregateCases('maximum', [ + AggregateCase( + 'static, field name', + PipelineFunctions.maximum('rating'), + 5, + ), + AggregateCase( + 'static, expression on strings', + PipelineFunctions.maximum(Expression.field('title')), + 'Inactive Draft', + ), + AggregateCase('fluent', Expression.field('price').maximum(), 30), + ]); + + ctx.aggregateCases('arrayAggDistinct', [ + // The order of distinct values is unspecified. + AggregateCase( + 'static, field name', + PipelineFunctions.arrayAggDistinct('active'), + unorderedEquals([true, false]), + ), + AggregateCase( + 'static, expression', + PipelineFunctions.arrayAggDistinct(Expression.field('half')), + [2.5], + ), + AggregateCase( + 'fluent', + Expression.field('metadata').mapGetLiteral('lang').arrayAggDistinct(), + ['dart'], + ), + ]); + + test('first, last and arrayAgg follow the input order', () async { + final snapshot = + await ctx.runPipeline().sort([ascending('price')]).aggregate([ + PipelineFunctions.first('title').as('firstTitle'), + PipelineFunctions.first(Expression.field('price')).as('firstPrice'), + Expression.field('rating').first().as('firstRating'), + PipelineFunctions.last('title').as('lastTitle'), + PipelineFunctions.last(Expression.field('price')).as('lastPrice'), + Expression.field('rating').last().as('lastRating'), + PipelineFunctions.arrayAgg('title').as('titles'), + PipelineFunctions.arrayAgg( + Expression.field('rating'), + ).as('ratings'), + Expression.field('price').arrayAgg().as('prices'), + ]).execute(); + + expect(snapshot.results.single.data(), { + 'firstTitle': 'Dart Pipelines', + 'firstPrice': 10, + 'firstRating': 5, + 'lastTitle': 'Inactive Draft', + 'lastPrice': 30, + 'lastRating': 2, + 'titles': ['Dart Pipelines', 'Firestore Admin', 'Inactive Draft'], + 'ratings': [5, 4, 2], + 'prices': [10, 20, 30], + }); + }); + + test('aggregates per group', () async { + final snapshot = await ctx + .runPipeline() + .aggregate( + [ + PipelineFunctions.countAll().as('books'), + PipelineFunctions.sum('rating').as('totalRating'), + PipelineFunctions.average('price').as('averagePrice'), + ], + groups: [ + 'active', + Expression.field('metadata').mapGetLiteral('lang').as('lang'), + ], + ) + .sort([ascending('active')]) + .execute(); + + // Book 3 is inactive; books 1 and 2 are active. + expect( + [for (final result in snapshot.results) result.data()], + [ + { + 'active': false, + 'lang': 'dart', + 'books': 1, + 'totalRating': 2, + 'averagePrice': isDouble(30), + }, + { + 'active': true, + 'lang': 'dart', + 'books': 2, + 'totalRating': 9, + 'averagePrice': isDouble(15), + }, + ], + ); + }); + + test('aggregates per group named by a FieldPath', () async { + // A FieldPath names a group as a String does. Only book 3 is archived. + final snapshot = await ctx + .runPipeline() + .aggregate( + [PipelineFunctions.countAll().as('books')], + groups: [ + FieldPath(const ['archived']), + ], + ) + .sort([ascending('archived')]) + .execute(); + + expect( + [for (final result in snapshot.results) result.data()], + [ + {'archived': false, 'books': 2}, + {'archived': true, 'books': 1}, + ], + ); + }); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/execute_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/execute_test.dart new file mode 100644 index 00000000..354d2d80 --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/execute_test.dart @@ -0,0 +1,220 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of `Pipeline.execute` options, explain stats and +/// `Transaction.executePipeline`. +@Tags(['prod']) +library; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +import 'harness.dart'; + +/// The titles of the three seeded books, in book order. +const _allTitles = ['Dart Pipelines', 'Firestore Admin', 'Inactive Draft']; + +/// The titles in [snapshot], in order. +List _titles(PipelineSnapshot snapshot) { + return [for (final result in snapshot.results) result.get('title')]; +} + +/// [timestamp] in whole microseconds, for ordering comparisons. +int _micros(Timestamp timestamp) { + return timestamp.seconds * 1000000 + timestamp.nanoseconds ~/ 1000; +} + +/// [timestamp] truncated to whole microseconds, the precision the backend +/// accepts for a read time. +Timestamp _truncateToMicros(Timestamp timestamp, {int secondsEarlier = 0}) { + return Timestamp( + seconds: timestamp.seconds - secondsEarlier, + nanoseconds: timestamp.nanoseconds - timestamp.nanoseconds % 1000, + ); +} + +/// Expects [stats] to hold a human-readable text plan, as the backend +/// returns for the `text` format (and by default). +void _expectTextStats(ExplainStats? stats) { + expect(stats, isNotNull); + expect(stats!.typeName, 'google.protobuf.StringValue'); + expect(stats.text, isNotEmpty); + // Text, not JSON. + expect(stats.text, isNot(startsWith('{'))); + expect(stats.raw, isNotNull); +} + +void main() { + pipelineE2E('Pipeline execution', (ctx) { + /// This run's books, in book order. + Pipeline sortedBooks() => ctx.runPipeline().sort([ascending('price')]); + + test('a pipeline keeps the Firestore instance it was built from', () { + expect(sortedBooks().firestore, same(ctx.firestore)); + }); + + test('explain analyze in text format returns stats and results', () async { + const options = PipelineExplainOptions( + mode: PipelineExplainMode.analyze, + outputFormat: PipelineExplainOutputFormat.text, + ); + expect(options.mode, PipelineExplainMode.analyze); + expect(options.outputFormat, PipelineExplainOutputFormat.text); + + final snapshot = await sortedBooks().execute(explain: options); + + // `analyze` runs the pipeline and returns planning stats alongside the + // results, which is what proves the option names reached the backend. + _expectTextStats(snapshot.explainStats); + expect(_titles(snapshot), _allTitles); + }); + + test('explain analyze defaults to the text format', () async { + const options = PipelineExplainOptions(mode: PipelineExplainMode.analyze); + expect(options.outputFormat, isNull); + + final snapshot = await sortedBooks().execute(explain: options); + + _expectTextStats(snapshot.explainStats); + expect(_titles(snapshot), _allTitles); + }); + + test('explain execute returns results without stats', () async { + final snapshot = await sortedBooks().execute( + explain: const PipelineExplainOptions( + mode: PipelineExplainMode.execute, + outputFormat: PipelineExplainOutputFormat.text, + ), + ); + + expect(snapshot.explainStats, isNull); + expect(_titles(snapshot), _allTitles); + }); + + test('explain without a mode defaults to execute', () async { + for (final options in const [ + PipelineExplainOptions(), + PipelineExplainOptions(outputFormat: PipelineExplainOutputFormat.text), + ]) { + expect(options.mode, isNull); + + final snapshot = await sortedBooks().execute(explain: options); + + expect(snapshot.explainStats, isNull); + expect(_titles(snapshot), _allTitles); + } + }); + + test('executes with the recommended index mode', () async { + // indexMode is deprecated and not sent: the backend rejects the + // index_mode option ("Unsupported option: index_mode"). + final snapshot = await sortedBooks().execute( + indexMode: PipelineIndexMode.recommended, + ); + + expect(_titles(snapshot), _allTitles); + }); + + test('rawOptions reach the backend', () async { + final snapshot = await sortedBooks().execute( + rawOptions: const { + 'explain_options': {'mode': 'analyze'}, + }, + ); + + _expectTextStats(snapshot.explainStats); + expect(_titles(snapshot), _allTitles); + }); + + test('rawOptions take precedence over typed options', () async { + final snapshot = await sortedBooks().execute( + explain: const PipelineExplainOptions( + mode: PipelineExplainMode.execute, + ), + rawOptions: const { + 'explain_options': {'mode': 'analyze'}, + }, + ); + + _expectTextStats(snapshot.explainStats); + expect(_titles(snapshot), _allTitles); + }); + + test('readTime reads the database as it was at that time', () async { + final current = await sortedBooks().execute(); + expect(_titles(current), _allTitles); + + // Before the first book was written, none of this run's books existed. + final firstWrite = current.results + .map((result) => result.createTime!) + .reduce((a, b) => _micros(a) <= _micros(b) ? a : b); + final beforeSeed = _truncateToMicros(firstWrite, secondsEarlier: 1); + final past = await sortedBooks().execute(readTime: beforeSeed); + expect(past.results, isEmpty); + + // By the time the first pipeline ran, all of them did. + final seeded = _truncateToMicros(current.executionTime!); + final atSeed = await sortedBooks().execute(readTime: seeded); + expect(_titles(atSeed), _allTitles); + + // A read-only transaction at a read time reads at that time too. + final inTransaction = await ctx.firestore.runTransaction( + (transaction) => transaction.executePipeline(sortedBooks()), + transactionOptions: ReadOnlyTransactionOptions(readTime: beforeSeed), + ); + expect(inTransaction.results, isEmpty); + }); + + test('executes a pipeline inside a transaction', () async { + final titles = await ctx.firestore.runTransaction((transaction) async { + final snapshot = await transaction.executePipeline( + ctx.firestore + .pipeline() + .collection(ctx.collectionPath) + .where(ctx.runFilter(Expression.field('active').equal(true))) + .sort([Expression.field('price').ascending()]) + .select([Expression.field('title')]), + ); + return _titles(snapshot); + }, transactionOptions: ReadOnlyTransactionOptions()); + + expect(titles, ['Dart Pipelines', 'Firestore Admin']); + }); + + test('a transaction forwards execute options', () async { + final pipeline = sortedBooks(); + + final snapshot = await pipeline.firestore.runTransaction( + (transaction) => transaction.executePipeline( + pipeline, + // Deprecated and not sent, as on Pipeline.execute. + indexMode: PipelineIndexMode.recommended, + // rawOptions win over the typed explain options, so this runs in + // the default `execute` mode and returns no stats. + explain: const PipelineExplainOptions( + mode: PipelineExplainMode.analyze, + outputFormat: PipelineExplainOutputFormat.text, + ), + rawOptions: const { + 'explain_options': {'mode': 'execute'}, + }, + ), + transactionOptions: ReadOnlyTransactionOptions(), + ); + + expect(snapshot.explainStats, isNull); + expect(_titles(snapshot), _allTitles); + }); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/functions_arithmetic_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/functions_arithmetic_test.dart new file mode 100644 index 00000000..ace0d6c1 --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/functions_arithmetic_test.dart @@ -0,0 +1,624 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of the arithmetic Pipeline functions. +/// +/// Result types follow the backend's arithmetic reference: `add`, `subtract`, +/// `multiply`, `divide`, `mod`, `abs`, `ceil`, `floor`, `round` and `trunc` +/// keep the type of their (widest) operand, so two int64 operands give an +/// int64 and any float64 operand gives a float64; `pow`, `sqrt`, `exp`, `ln`, +/// `log`, `log10` and `rand` always return a float64. Integer division +/// truncates toward zero and `mod` takes the sign of the dividend. Arithmetic +/// on a `NULL` operand is `NULL`. +/// +/// `logicalMaximum` / `logicalMinimum` (the backend's scalar `maximum` / +/// `minimum`) return the largest / smallest operand unchanged, skipping +/// `NULL` and absent operands. +@Tags(['prod']) +library; + +import 'dart:math' as math; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +import 'harness.dart'; + +void main() { + pipelineE2E('Pipeline arithmetic functions', (ctx) { + // Book 1: price 10, rating 5, discount 2, quantity 7, zero 0 (ints); + // score -12.7, ratio 0.5, half 2.5, negativeHalf -2.5, precise 4.123456 + // (doubles); nullable null. + + ctx.functionCases('add', [ + FunctionCase( + 'fluent, int literal: int + int is int', + Expression.field('price').add(2), + isInt(12), + ), + FunctionCase( + 'fluent, field expression', + Expression.field('price').add(Expression.field('rating')), + isInt(15), + ), + FunctionCase( + 'int + double widens to double', + Expression.field('price').add(Expression.field('ratio')), + isDouble(10.5), + ), + FunctionCase( + 'int64 beyond 2^53 keeps full precision', + Expression.field('largeInt').add(1), + isInt(9007199254740994), + ), + FunctionCase( + 'a NULL operand gives NULL', + Expression.field('price').add(Expression.field('nullable')), + isNull, + ), + FunctionCase( + 'static, field name and literal', + PipelineFunctions.add('price', 5), + isInt(15), + ), + FunctionCase( + 'static, expressions', + PipelineFunctions.add( + Expression.field('price'), + Expression.field('discount'), + ), + isInt(12), + ), + // Further operands are sent as nested two-operand add calls, since the + // backend's add takes exactly two: ((10 + 1) + 2) + 3. + FunctionCase( + 'static, further literal operands', + PipelineFunctions.add('price', 1, [2, 3]), + isInt(16), + ), + // 10 + 5 + 2. + FunctionCase( + 'static, further expression operands', + PipelineFunctions.add( + Expression.field('price'), + Expression.field('rating'), + [Expression.field('discount')], + ), + isInt(17), + ), + // 10 + 1 + 2. + FunctionCase( + 'fluent, further literal operands', + Expression.field('price').add(1, [2]), + isInt(13), + ), + // 10 + 2 + 5 + 7. + FunctionCase( + 'fluent, further expression operands', + Expression.field('price').add(Expression.field('discount'), [ + Expression.field('rating'), + Expression.field('quantity'), + ]), + isInt(24), + ), + ]); + + ctx.functionCases('subtract', [ + FunctionCase( + 'fluent, int literal', + Expression.field('price').subtract(3), + isInt(7), + ), + FunctionCase( + 'fluent, field expression', + Expression.field('price').subtract(Expression.field('quantity')), + isInt(3), + ), + FunctionCase( + 'static, field name and literal: result may be negative', + PipelineFunctions.subtract('price', 15), + isInt(-5), + ), + FunctionCase( + 'static, expressions: double - double', + PipelineFunctions.subtract( + Expression.field('ratio'), + Expression.field('half'), + ), + isDouble(-2), + ), + ]); + + ctx.functionCases('multiply', [ + FunctionCase( + 'fluent, int literal', + Expression.field('price').multiply(2), + isInt(20), + ), + FunctionCase( + 'fluent, field expression: int * double is double', + Expression.field('price').multiply(Expression.field('ratio')), + isDouble(5), + ), + FunctionCase( + 'static, field name and literal', + PipelineFunctions.multiply('rating', 3), + isInt(15), + ), + FunctionCase( + 'static, expressions', + PipelineFunctions.multiply( + Expression.field('price'), + Expression.field('quantity'), + ), + isInt(70), + ), + // Further operands are sent as nested two-operand multiply calls, since + // the backend's multiply takes exactly two: (5 * 2) * 3. + FunctionCase( + 'static, further literal operands', + PipelineFunctions.multiply('rating', 2, [3]), + isInt(30), + ), + // 10 * 5 * 2. + FunctionCase( + 'static, further expression operands', + PipelineFunctions.multiply( + Expression.field('price'), + Expression.field('rating'), + [Expression.field('discount')], + ), + isInt(100), + ), + // 10 * 2 * 3. + FunctionCase( + 'fluent, further literal operands', + Expression.field('price').multiply(2, [3]), + isInt(60), + ), + // 5 * 2 * 6. + FunctionCase( + 'fluent, further expression operands', + Expression.field( + 'rating', + ).multiply(Expression.field('discount'), [Expression.field('flags')]), + isInt(60), + ), + ]); + + ctx.functionCases('divide', [ + FunctionCase( + 'fluent, int literal', + Expression.field('price').divide(2), + isInt(5), + ), + FunctionCase( + 'fluent, double literal: widens to double', + Expression.field('price').divide(4.0), + isDouble(2.5), + ), + FunctionCase( + 'fluent, field expression: int / double', + Expression.field('price').divide(Expression.field('ratio')), + isDouble(20), + ), + FunctionCase( + 'static, field name and literal: integer division truncates', + PipelineFunctions.divide('price', 3), + isInt(3), + ), + FunctionCase( + 'static, expressions: integer division truncates toward zero', + PipelineFunctions.divide( + Expression.constant(-7), + Expression.field('discount'), + ), + isInt(-3), + ), + ]); + + ctx.functionCases('mod', [ + FunctionCase( + 'fluent, int literal', + Expression.field('price').mod(4), + isInt(2), + ), + FunctionCase( + 'fluent, field expression', + Expression.field('price').mod(Expression.field('quantity')), + isInt(3), + ), + FunctionCase( + 'fluent, double dividend', + Expression.field('half').mod(2), + isDouble(0.5), + ), + FunctionCase( + 'static, field name and literal', + PipelineFunctions.mod('price', 3), + isInt(1), + ), + FunctionCase( + 'static, expressions: takes the sign of the dividend', + PipelineFunctions.mod(Expression.constant(-10), Expression.constant(3)), + isInt(-1), + ), + ]); + + ctx.functionCases('abs', [ + FunctionCase( + 'fluent, double', + Expression.field('score').abs(), + isDouble(12.7), + ), + FunctionCase( + 'static, field name', + PipelineFunctions.abs('negativeHalf'), + isDouble(2.5), + ), + FunctionCase( + 'static, expression: int stays int', + PipelineFunctions.abs(Expression.constant(-10)), + isInt(10), + ), + ]); + + ctx.functionCases('ceil', [ + FunctionCase('fluent', Expression.constant(12.2).ceil(), isDouble(13)), + FunctionCase( + 'fluent, int stays int', + Expression.field('price').ceil(), + isInt(10), + ), + FunctionCase( + 'static, field name: negative half', + PipelineFunctions.ceil('negativeHalf'), + isDouble(-2), + ), + FunctionCase( + 'static, expression', + PipelineFunctions.ceil(Expression.field('score')), + isDouble(-12), + ), + ]); + + ctx.functionCases('floor', [ + FunctionCase('fluent', Expression.constant(12.8).floor(), isDouble(12)), + FunctionCase( + 'static, field name: negative half', + PipelineFunctions.floor('negativeHalf'), + isDouble(-3), + ), + FunctionCase( + 'static, expression', + PipelineFunctions.floor(Expression.field('ratio')), + isDouble(0), + ), + ]); + + ctx.functionCases('round', [ + FunctionCase('fluent', Expression.constant(12.6).round(), isDouble(13)), + FunctionCase( + 'fluent: positive half rounds away from zero', + Expression.field('half').round(), + isDouble(3), + ), + FunctionCase( + 'fluent: negative half rounds away from zero', + Expression.field('negativeHalf').round(), + isDouble(-3), + ), + FunctionCase( + 'fluent, literal decimal places', + Expression.constant(12.68).round(1), + isDouble(12.7), + ), + FunctionCase( + 'fluent, expression decimal places', + Expression.field('precise').round(Expression.field('discount')), + isDouble(4.12), + ), + FunctionCase( + 'static, field name, no decimal places', + PipelineFunctions.round('half'), + isDouble(3), + ), + FunctionCase( + 'static, field name, literal decimal places', + PipelineFunctions.round('precise', 4), + isDouble(4.1235), + ), + FunctionCase( + 'static, expression, expression decimal places', + PipelineFunctions.round( + Expression.field('precise'), + Expression.constant(1), + ), + isDouble(4.1), + ), + FunctionCase( + 'static, expression, negative decimal places: int stays int', + PipelineFunctions.round(Expression.constant(15), -1), + isInt(20), + ), + ]); + + ctx.functionCases('trunc', [ + FunctionCase('fluent', Expression.constant(12.8).trunc(), isDouble(12)), + FunctionCase( + 'fluent: truncates toward zero', + Expression.field('negativeHalf').trunc(), + isDouble(-2), + ), + FunctionCase( + 'fluent, literal decimal places', + Expression.constant(12.89).trunc(1), + isDouble(12.8), + ), + FunctionCase( + 'fluent, expression decimal places', + Expression.field('precise').trunc(Expression.field('discount')), + isDouble(4.12), + ), + FunctionCase( + 'static, field name, no decimal places', + PipelineFunctions.trunc('score'), + isDouble(-12), + ), + FunctionCase( + 'static, field name, literal decimal places', + PipelineFunctions.trunc('precise', 4), + isDouble(4.1234), + ), + FunctionCase( + 'static, expression, expression decimal places', + PipelineFunctions.trunc( + Expression.field('precise'), + Expression.constant(1), + ), + isDouble(4.1), + ), + FunctionCase( + 'static, expression, negative decimal places: int stays int', + PipelineFunctions.trunc(Expression.constant(15), -1), + isInt(10), + ), + ]); + + ctx.functionCases('pow', [ + FunctionCase( + 'static, literals: int operands give a double', + PipelineFunctions.pow(2, 3), + isDouble(8), + ), + FunctionCase( + 'static, field name base', + PipelineFunctions.pow('ratio', 2), + isDouble(0.25), + ), + FunctionCase( + 'static, expressions: negative exponent', + PipelineFunctions.pow( + Expression.field('discount'), + Expression.constant(-1), + ), + isDouble(0.5), + ), + FunctionCase( + 'fluent, literal exponent', + Expression.field('rating').pow(2), + isDouble(25), + ), + FunctionCase( + 'fluent, expression exponent', + Expression.field('price').pow(Expression.field('discount')), + isDouble(100), + ), + ]); + + ctx.functionCases('sqrt', [ + FunctionCase( + 'fluent: int operand gives a double', + Expression.constant(9).sqrt(), + isDouble(3), + ), + FunctionCase( + 'static, field name', + PipelineFunctions.sqrt('discount'), + isDouble(math.sqrt2), + ), + FunctionCase( + 'static, expression', + PipelineFunctions.sqrt(Expression.field('ratio')), + isDouble(math.sqrt1_2), + ), + ]); + + ctx.functionCases('exp', [ + FunctionCase( + 'static, literal', + PipelineFunctions.exp(1), + isDouble(math.e), + ), + FunctionCase( + 'static, expression', + PipelineFunctions.exp(Expression.field('discount')), + isDouble(math.exp(2)), + ), + FunctionCase('fluent', Expression.field('zero').exp(), isDouble(1)), + ]); + + ctx.functionCases('ln', [ + FunctionCase( + 'static, literal', + PipelineFunctions.ln(math.e), + isDouble(1), + ), + FunctionCase( + 'static, expression', + PipelineFunctions.ln(Expression.constant(1)), + isDouble(0), + ), + FunctionCase( + 'fluent', + Expression.field('price').ln(), + isDouble(math.log(10)), + ), + ]); + + ctx.functionCases('log', [ + FunctionCase( + 'static, literals', + PipelineFunctions.log(8, 2), + isDouble(3), + ), + FunctionCase( + 'static, expressions', + PipelineFunctions.log( + Expression.constant(100), + Expression.field('price'), + ), + isDouble(2), + ), + FunctionCase( + 'static, field name, no base: natural logarithm', + PipelineFunctions.log('price'), + isDouble(math.log(10)), + ), + FunctionCase( + 'fluent, literal base', + Expression.field('price').log(10), + isDouble(1), + ), + FunctionCase( + 'fluent, expression base', + Expression.constant(1024).log(Expression.field('discount')), + isDouble(10), + ), + FunctionCase( + 'fluent, no base: natural logarithm', + Expression.field('price').log(), + isDouble(math.log(10)), + ), + ]); + + ctx.functionCases('log10', [ + FunctionCase( + 'static, literal', + PipelineFunctions.log10(100), + isDouble(2), + ), + FunctionCase( + 'static, expression', + PipelineFunctions.log10(Expression.constant(1000)), + isDouble(3), + ), + FunctionCase('fluent', Expression.field('price').log10(), isDouble(1)), + ]); + + ctx.functionCases('rand', [ + FunctionCase( + 'a double in [0, 1)', + PipelineFunctions.rand(), + allOf(isA(), inClosedOpenRange(0, 1)), + ), + ]); + + ctx.functionCases('logicalMaximum', [ + FunctionCase( + 'static, field name and literal', + PipelineFunctions.logicalMaximum('price', 15), + isInt(15), + ), + FunctionCase( + 'static, expressions and others', + PipelineFunctions.logicalMaximum( + Expression.field('rating'), + Expression.field('discount'), + [Expression.field('price'), 3], + ), + isInt(10), + ), + FunctionCase( + 'static: NULL and absent operands are skipped', + PipelineFunctions.logicalMaximum( + 'missing', + Expression.field('nullable'), + [Expression.field('rating')], + ), + isInt(5), + ), + FunctionCase( + 'fluent, literal: returns the operand unchanged', + Expression.field('score').logicalMaximum(-20), + isDouble(-12.7), + ), + FunctionCase( + 'fluent, expression and others', + Expression.field('price').logicalMaximum(Expression.field('rating'), [ + 30, + Expression.field('quantity'), + ]), + isInt(30), + ), + ]); + + ctx.functionCases('logicalMinimum', [ + FunctionCase( + 'static, field name and literal', + PipelineFunctions.logicalMinimum('price', 15), + isInt(10), + ), + FunctionCase( + 'static, expressions and others', + PipelineFunctions.logicalMinimum( + Expression.field('rating'), + Expression.field('price'), + [Expression.field('quantity'), 3], + ), + isInt(3), + ), + FunctionCase( + 'static: NULL and absent operands are skipped', + PipelineFunctions.logicalMinimum( + 'nullable', + Expression.field('missing'), + [Expression.field('discount')], + ), + isInt(2), + ), + FunctionCase( + 'fluent, literal: returns the operand unchanged', + Expression.field('score').logicalMinimum(0), + isDouble(-12.7), + ), + FunctionCase( + 'fluent, expression and others: int and double operands compare ' + 'numerically', + Expression.field('price').logicalMinimum(Expression.field('ratio'), [ + 20, + Expression.field('rating'), + ]), + isDouble(0.5), + ), + FunctionCase( + 'fluent: only NULL and absent operands give NULL', + Expression.field( + 'nullable', + ).logicalMinimum(Expression.field('missing')), + isNull, + ), + ]); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/functions_array_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/functions_array_test.dart new file mode 100644 index 00000000..8dbcfd06 --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/functions_array_test.dart @@ -0,0 +1,797 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of the array Pipeline functions. +@Tags(['prod']) +library; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +import 'harness.dart'; + +void main() { + pipelineE2E('Pipeline array functions', (ctx) { + ctx.functionCases('array construction', [ + FunctionCase('array', Expression.array([1, 2, 3]), [1, 2, 3]), + FunctionCase( + 'array with an expression element', + Expression.array([Expression.field('prefix'), 'x']), + ['Dart', 'x'], + ), + FunctionCase( + 'static array of plain values', + PipelineFunctions.array(['a', 1, true, null]), + ['a', 1, true, null], + ), + FunctionCase( + 'static array with expression elements', + PipelineFunctions.array([ + Expression.field('title'), + Expression.field('rating'), + 0, + ]), + ['Dart Pipelines', 5, 0], + ), + ]); + + ctx.functionCases('arrayConcat', [ + FunctionCase( + 'arrayConcat', + Expression.field('tags').arrayConcat(['admin']), + ['dart', 'firebase', 'admin'], + ), + FunctionCase( + 'arrayConcat with an array expression', + Expression.field('tags').arrayConcat(Expression.field('words')), + ['dart', 'firebase', 'dart', 'firebase'], + ), + FunctionCase( + 'arrayConcatMultiple', + Expression.field('tags').arrayConcatMultiple([ + ['admin'], + ['sdk'], + ]), + ['dart', 'firebase', 'admin', 'sdk'], + ), + FunctionCase( + 'static arrayConcat', + PipelineFunctions.arrayConcat([ + 'tags', + ['admin'], + Expression.field('emptyArray'), + [null], + ]), + ['dart', 'firebase', 'admin', null], + ), + FunctionCase( + 'static arrayConcat of expressions', + PipelineFunctions.arrayConcat([ + Expression.field('numbers'), + Expression.field('scores'), + ]), + [3, 1, 2, 3, 1.5, 2.5, -0.5], + ), + ]); + + ctx.functionCases('arrayContains', [ + FunctionCase( + 'arrayContainsValue', + Expression.field('tags').arrayContains('dart'), + true, + ), + FunctionCase( + 'arrayContainsElement', + Expression.field('tags').arrayContains(Expression.constant('firebase')), + true, + ), + FunctionCase( + 'static arrayContains', + PipelineFunctions.arrayContains('tags', 'firebase'), + true, + ), + FunctionCase( + 'static arrayContains with expressions', + PipelineFunctions.arrayContains( + Expression.field('numbers'), + Expression.field('discount'), + ), + true, + ), + FunctionCase( + 'static arrayContains of a value not in the array', + PipelineFunctions.arrayContains('numbers', Expression.field('rating')), + false, + ), + FunctionCase( + 'arrayContainsAll', + Expression.field('tags').arrayContainsAll(['dart', 'firebase']), + true, + ), + FunctionCase( + 'arrayContainsAll with an array expression', + Expression.field( + 'words', + ).arrayContainsAll(Expression.array(['firebase', 'dart'])), + true, + ), + FunctionCase( + 'static arrayContainsAll with expressions', + PipelineFunctions.arrayContainsAll( + Expression.field('tags'), + Expression.field('words'), + ), + true, + ), + FunctionCase( + 'static arrayContainsAll with a missing value', + PipelineFunctions.arrayContainsAll('numbers', [1, 4]), + false, + ), + FunctionCase( + 'arrayContainsAllFrom', + Expression.field( + 'tags', + ).arrayContainsAllFrom(Expression.array(['dart'])), + true, + ), + FunctionCase( + 'arrayContainsAllFrom with a list', + Expression.field('tags').arrayContainsAllFrom(['dart', 'missing']), + false, + ), + FunctionCase( + 'arrayContainsAny', + Expression.field('tags').arrayContainsAny(['missing', 'dart']), + true, + ), + FunctionCase( + 'arrayContainsAny with an array expression', + Expression.field( + 'tags', + ).arrayContainsAny(Expression.array(['sdk', 'admin'])), + false, + ), + FunctionCase( + 'static arrayContainsAny', + PipelineFunctions.arrayContainsAny('tags', ['missing', 'firebase']), + true, + ), + FunctionCase( + 'static arrayContainsAny with expressions', + PipelineFunctions.arrayContainsAny( + Expression.field('numbers'), + Expression.array([9, 4]), + ), + false, + ), + ]); + + ctx.functionCases('arrayFilter and arrayTransform', [ + FunctionCase( + 'arrayFilter', + Expression.field( + 'tags', + ).arrayFilter('tag', Expression.variable('tag').notEqual('firebase')), + ['dart'], + ), + FunctionCase( + 'static arrayFilter', + PipelineFunctions.arrayFilter( + 'numbers', + 'n', + variable('n').greaterThan(1), + ), + [3, 2, 3], + ), + FunctionCase( + 'static arrayFilter with a constant predicate', + PipelineFunctions.arrayFilter(Expression.field('tags'), 'tag', false), + [], + ), + FunctionCase( + 'arrayTransform', + Expression.field( + 'numbers', + ).arrayTransform('n', Expression.variable('n').add(1)), + [4, 2, 3, 4], + ), + // A list body holding the variable is built with array(...), so each + // element maps to a pair. + FunctionCase( + 'arrayTransform with a list body', + Expression.field('tags').arrayTransform('t', [variable('t'), 1]), + [ + ['dart', 1], + ['firebase', 1], + ], + ), + FunctionCase( + 'static arrayTransform', + PipelineFunctions.arrayTransform( + 'numbers', + 'n', + variable('n').multiply(2), + ), + [6, 2, 4, 6], + ), + FunctionCase( + 'static arrayTransform with a constant body', + PipelineFunctions.arrayTransform(Expression.field('tags'), 't', 'x'), + ['x', 'x'], + ), + FunctionCase( + 'arrayTransformWithIndex', + Expression.field('numbers').arrayTransformWithIndex( + 'n', + 'i', + Expression.variable('n').add(Expression.variable('i')), + ), + [3, 2, 4, 6], + ), + FunctionCase( + 'arrayTransformWithIndex with a constant body', + Expression.field('tags').arrayTransformWithIndex('t', 'i', 7), + [7, 7], + ), + FunctionCase( + 'static arrayTransformWithIndex', + PipelineFunctions.arrayTransformWithIndex( + 'tags', + 't', + 'i', + variable('i'), + ), + [0, 1], + ), + FunctionCase( + 'static arrayTransformWithIndex with a list body', + PipelineFunctions.arrayTransformWithIndex( + Expression.field('scores'), + 's', + 'i', + [variable('i'), variable('s')], + ), + [ + [0, 1.5], + [1, 2.5], + [2, -0.5], + ], + ), + ]); + + ctx.functionCases('element access', [ + FunctionCase( + 'arrayGet', + Expression.field('tags').arrayGet(1), + 'firebase', + ), + // A negative index counts from the end. + FunctionCase( + 'arrayGet with a negative index', + Expression.field('scores').arrayGet(Expression.constant(-1)), + -0.5, + ), + FunctionCase( + 'static arrayGet', + PipelineFunctions.arrayGet(Expression.field('numbers'), 1), + 1, + ), + FunctionCase( + 'static arrayGet with an index expression', + PipelineFunctions.arrayGet('numbers', Expression.field('discount')), + 2, + ), + FunctionCase('arrayFirst', Expression.field('numbers').arrayFirst(), 3), + FunctionCase( + 'static arrayFirst', + PipelineFunctions.arrayFirst('tags'), + 'dart', + ), + FunctionCase( + 'static arrayFirst of an expression', + PipelineFunctions.arrayFirst(Expression.field('scores')), + 1.5, + ), + FunctionCase('arrayFirstN', Expression.field('numbers').arrayFirstN(2), [ + 3, + 1, + ]), + FunctionCase( + 'arrayFirstN with an expression count', + Expression.field('scores').arrayFirstN(Expression.constant(2)), + [1.5, 2.5], + ), + FunctionCase( + 'static arrayFirstN', + PipelineFunctions.arrayFirstN('tags', 1), + ['dart'], + ), + FunctionCase( + 'static arrayFirstN with expressions', + PipelineFunctions.arrayFirstN( + Expression.field('numbers'), + Expression.field('discount'), + ), + [3, 1], + ), + FunctionCase('arrayLast', Expression.field('numbers').arrayLast(), 3), + FunctionCase( + 'static arrayLast', + PipelineFunctions.arrayLast('tags'), + 'firebase', + ), + FunctionCase( + 'static arrayLast of an expression', + PipelineFunctions.arrayLast(Expression.field('scores')), + -0.5, + ), + FunctionCase('arrayLastN', Expression.field('numbers').arrayLastN(2), [ + 2, + 3, + ]), + FunctionCase( + 'arrayLastN with an expression count', + Expression.field('tags').arrayLastN(Expression.constant(1)), + ['firebase'], + ), + FunctionCase( + 'static arrayLastN', + PipelineFunctions.arrayLastN('scores', 2), + [2.5, -0.5], + ), + FunctionCase( + 'static arrayLastN with expressions', + PipelineFunctions.arrayLastN( + Expression.field('numbers'), + Expression.field('discount'), + ), + [2, 3], + ), + FunctionCase('arraySlice', Expression.field('numbers').arraySlice(1, 2), [ + 1, + 2, + ]), + FunctionCase( + 'arraySlice without a length runs to the end', + Expression.field('scores').arraySlice(1), + [2.5, -0.5], + ), + FunctionCase( + 'arraySlice with expressions', + Expression.field( + 'numbers', + ).arraySlice(Expression.constant(1), Expression.field('discount')), + [1, 2], + ), + // A negative offset counts from the end: -2 is index 2 of [3, 1, 2, 3]. + FunctionCase( + 'arraySlice with a negative offset', + Expression.field('numbers').arraySlice(-2, 1), + [2], + ), + FunctionCase( + 'static arraySlice', + PipelineFunctions.arraySlice('tags', 0, 1), + ['dart'], + ), + FunctionCase( + 'static arraySlice without a length runs to the end', + PipelineFunctions.arraySlice('numbers', 1), + [1, 2, 3], + ), + FunctionCase( + 'static arraySlice with expressions', + PipelineFunctions.arraySlice( + Expression.field('numbers'), + Expression.field('discount'), + Expression.constant(1), + ), + [2], + ), + ]); + + ctx.functionCases('index search', [ + FunctionCase( + 'arrayIndexOf', + Expression.field('numbers').arrayIndexOf(1), + 1, + ), + FunctionCase( + 'arrayIndexOf an expression', + Expression.field('tags').arrayIndexOf(Expression.constant('firebase')), + isInt(1), + ), + FunctionCase( + 'static arrayIndexOf', + PipelineFunctions.arrayIndexOf('numbers', 3), + isInt(0), + ), + FunctionCase( + 'static arrayIndexOf with expressions', + PipelineFunctions.arrayIndexOf( + Expression.field('numbers'), + Expression.field('discount'), + ), + isInt(2), + ), + FunctionCase( + 'static arrayIndexOf of a value not in the array', + PipelineFunctions.arrayIndexOf('tags', 'missing'), + isInt(-1), + ), + FunctionCase( + 'static arrayIndexOf of null', + PipelineFunctions.arrayIndexOf('withNulls', null), + isInt(1), + ), + FunctionCase( + 'arrayIndexOfAll', + Expression.field('numbers').arrayIndexOfAll(3), + [0, 3], + ), + FunctionCase( + 'arrayIndexOfAll an expression', + Expression.field( + 'numbers', + ).arrayIndexOfAll(Expression.field('discount')), + [2], + ), + FunctionCase( + 'arrayIndexOfAll of a value not in the array', + Expression.field('tags').arrayIndexOfAll('missing'), + [], + ), + FunctionCase( + 'static arrayIndexOfAll', + PipelineFunctions.arrayIndexOfAll('booleans', true), + [0, 2], + ), + FunctionCase( + 'static arrayIndexOfAll with expressions', + PipelineFunctions.arrayIndexOfAll( + Expression.field('booleans'), + Expression.field('archived'), + ), + [1], + ), + FunctionCase( + 'arrayLastIndexOf', + Expression.field('numbers').arrayLastIndexOf(3), + 3, + ), + FunctionCase( + 'arrayLastIndexOf an expression', + Expression.field( + 'booleans', + ).arrayLastIndexOf(Expression.field('active')), + isInt(2), + ), + FunctionCase( + 'static arrayLastIndexOf', + PipelineFunctions.arrayLastIndexOf('booleans', false), + isInt(1), + ), + FunctionCase( + 'static arrayLastIndexOf with expressions', + PipelineFunctions.arrayLastIndexOf( + Expression.field('numbers'), + Expression.constant(3), + ), + isInt(3), + ), + FunctionCase( + 'static arrayLastIndexOf of a value not in the array', + PipelineFunctions.arrayLastIndexOf('tags', 'missing'), + isInt(-1), + ), + ]); + + ctx.functionCases('arrayLength and arrayReverse', [ + FunctionCase('arrayLength', Expression.field('numbers').arrayLength(), 4), + FunctionCase( + 'static arrayLength', + PipelineFunctions.arrayLength('tags'), + isInt(2), + ), + FunctionCase( + 'static arrayLength of an expression', + PipelineFunctions.arrayLength(Expression.field('emptyArray')), + isInt(0), + ), + FunctionCase('arrayReverse', Expression.field('numbers').arrayReverse(), [ + 3, + 2, + 1, + 3, + ]), + FunctionCase( + 'static arrayReverse', + PipelineFunctions.arrayReverse('scores'), + [-0.5, 2.5, 1.5], + ), + FunctionCase( + 'static arrayReverse of an expression', + PipelineFunctions.arrayReverse(Expression.field('tags')), + ['firebase', 'dart'], + ), + ]); + + // maximumN returns the largest elements in descending order and minimumN + // the smallest in ascending order, as in the Node SDK system tests. + ctx.functionCases('maximum, minimum and sum', [ + FunctionCase( + 'arrayMaximum', + Expression.field('numbers').arrayMaximum(), + 3, + ), + FunctionCase( + 'arrayMaximumN', + Expression.field('numbers').arrayMaximumN(2), + [3, 3], + ), + FunctionCase( + 'arrayMaximumN with an expression count', + Expression.field('scores').arrayMaximumN(Expression.constant(1)), + [2.5], + ), + FunctionCase( + 'arrayMinimum', + Expression.field('numbers').arrayMinimum(), + 1, + ), + FunctionCase( + 'arrayMinimumN', + Expression.field('numbers').arrayMinimumN(2), + [1, 2], + ), + FunctionCase( + 'arrayMinimumN with an expression count', + Expression.field('tags').arrayMinimumN(Expression.field('discount')), + ['dart', 'firebase'], + ), + FunctionCase('arraySum', Expression.field('numbers').arraySum(), 9), + FunctionCase( + 'arraySum of mixed ints and doubles', + Expression.field('mixedNumbers').arraySum(), + isDouble(0.5), + ), + // The static helpers must emit the same backend names as the fluent + // forms (`maximum`, not `array_maximum`). + FunctionCase( + 'static arrayMaximum', + PipelineFunctions.arrayMaximum('numbers'), + 3, + ), + FunctionCase( + 'static arrayMaximum of an expression', + PipelineFunctions.arrayMaximum(Expression.field('scores')), + isDouble(2.5), + ), + FunctionCase( + 'static arrayMaximumN', + PipelineFunctions.arrayMaximumN('numbers', 2), + [3, 3], + ), + FunctionCase( + 'static arrayMaximumN with expressions', + PipelineFunctions.arrayMaximumN( + Expression.field('tags'), + Expression.constant(1), + ), + ['firebase'], + ), + FunctionCase( + 'static arrayMinimum', + PipelineFunctions.arrayMinimum('numbers'), + 1, + ), + FunctionCase( + 'static arrayMinimum of an expression', + PipelineFunctions.arrayMinimum(Expression.field('mixedNumbers')), + isInt(-3), + ), + FunctionCase( + 'static arrayMinimumN', + PipelineFunctions.arrayMinimumN('numbers', 2), + [1, 2], + ), + FunctionCase( + 'static arrayMinimumN with expressions', + PipelineFunctions.arrayMinimumN( + Expression.field('scores'), + Expression.constant(2), + ), + [-0.5, 1.5], + ), + FunctionCase('static arraySum', PipelineFunctions.arraySum('numbers'), 9), + FunctionCase( + 'static arraySum of an expression', + PipelineFunctions.arraySum(Expression.field('scores')), + isDouble(3.5), + ), + FunctionCase('static maximumN', PipelineFunctions.maximumN('scores', 2), [ + 2.5, + 1.5, + ]), + FunctionCase( + 'static maximumN with expressions', + PipelineFunctions.maximumN( + Expression.field('mixedNumbers'), + Expression.field('discount'), + ), + [2.5, 1], + ), + FunctionCase('static minimumN', PipelineFunctions.minimumN('scores', 2), [ + -0.5, + 1.5, + ]), + FunctionCase( + 'static minimumN with expressions', + PipelineFunctions.minimumN( + Expression.field('mixedNumbers'), + Expression.field('discount'), + ), + [-3, 0], + ), + ]); + + ctx.functionCases('join', [ + FunctionCase( + 'joinLiteral', + Expression.field('words').joinLiteral('-'), + 'dart-firebase', + ), + FunctionCase( + 'join', + Expression.field('words').join('/'), + 'dart/firebase', + ), + FunctionCase( + 'join with an expression delimiter', + Expression.field('tags').join(Expression.field('delimiter')), + 'dart;firebase', + ), + FunctionCase( + 'static join', + PipelineFunctions.join('words', ', '), + 'dart, firebase', + ), + FunctionCase( + 'static join with expressions', + PipelineFunctions.join( + Expression.array(['a', 'b', 'c']), + Expression.field('delimiter'), + ), + 'a;b;c', + ), + ]); + + // Collections that hold expressions must be built with array(...) / + // map(...); the backend rejects them inside a literal array or map value. + ctx.functionCases('collections holding expressions', [ + FunctionCase( + 'arrayContainsAll', + PipelineFunctions.arrayContainsAll('tags', [ + Expression.field('metadata').mapGetLiteral('lang'), + 'firebase', + ]), + true, + ), + FunctionCase( + 'fluent arrayContainsAll', + Expression.field( + 'numbers', + ).arrayContainsAll([Expression.field('discount'), 3]), + true, + ), + FunctionCase( + 'fluent arrayContainsAll with a value not in the array', + Expression.field( + 'numbers', + ).arrayContainsAll([Expression.field('rating'), 3]), + false, + ), + FunctionCase( + 'arrayContainsAny', + Expression.field('tags').arrayContainsAny([ + Expression.field('metadata').mapGetLiteral('lang'), + 'missing', + ]), + true, + ), + FunctionCase( + 'static arrayContainsAny', + PipelineFunctions.arrayContainsAny('numbers', [ + Expression.field('rating'), + Expression.field('discount'), + ]), + true, + ), + FunctionCase( + 'arrayConcat', + Expression.field('tags').arrayConcat([Expression.field('title')]), + ['dart', 'firebase', 'Dart Pipelines'], + ), + FunctionCase( + 'static arrayConcat', + PipelineFunctions.arrayConcat([ + 'tags', + [Expression.field('prefix'), 'sdk'], + ]), + ['dart', 'firebase', 'Dart', 'sdk'], + ), + FunctionCase( + 'arrayConcatMultiple', + Expression.field('numbers').arrayConcatMultiple([ + Expression.field('emptyArray'), + [Expression.field('rating'), 0], + ]), + [3, 1, 2, 3, 5, 0], + ), + FunctionCase( + 'nested array', + Expression.array([ + 1, + [Expression.field('price')], + ]), + [ + 1, + [10], + ], + ), + FunctionCase( + 'static nested array and map', + PipelineFunctions.array([ + [Expression.field('rating'), 0], + {'price': Expression.field('price')}, + ]), + [ + [5, 0], + {'price': 10}, + ], + ), + ]); + + // As in the Node SDK system tests: the first or last element of an empty + // array is absent, while the maximum of an empty array, any of these on + // null, and an index search in an absent field are null. + test('empty, null and absent arrays', () async { + final snapshot = await ctx.book1Pipeline().select([ + PipelineFunctions.arrayFirst('emptyArray').as('firstOfEmpty'), + Expression.field('emptyArray').arrayLast().as('lastOfEmpty'), + PipelineFunctions.arrayFirst('nullable').as('firstOfNull'), + PipelineFunctions.arrayLast('missing').as('lastOfAbsent'), + PipelineFunctions.arrayFirstN('emptyArray', 1).as('firstNOfEmpty'), + PipelineFunctions.arrayLastN('nullable', 1).as('lastNOfNull'), + PipelineFunctions.arrayMaximum('emptyArray').as('maximumOfEmpty'), + PipelineFunctions.arrayMinimum('nullable').as('minimumOfNull'), + PipelineFunctions.arrayIndexOf('missing', 'x').as('indexOfAbsent'), + PipelineFunctions.arrayIndexOfAll('missing', 'x').as('indexesOfAbsent'), + ]).execute(); + + expect(snapshot.results, hasLength(1)); + expect(snapshot.results.single.data(), { + 'firstOfNull': null, + 'lastOfAbsent': null, + 'firstNOfEmpty': [], + 'lastNOfNull': null, + 'maximumOfEmpty': null, + 'minimumOfNull': null, + 'indexOfAbsent': null, + 'indexesOfAbsent': null, + }); + }); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/functions_comparison_logical_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/functions_comparison_logical_test.dart new file mode 100644 index 00000000..5e870948 --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/functions_comparison_logical_test.dart @@ -0,0 +1,671 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of the comparison and logical Pipeline functions. +@Tags(['prod']) +library; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +// The top-level Pipeline `greaterThan` / `lessThan` share their names with +// package:matcher's matchers. +import 'package:test/test.dart' hide greaterThan, lessThan; + +import 'harness.dart'; + +void main() { + pipelineE2E('Pipeline comparison and logical functions', (ctx) { + // Book 1: price 10, rating 5, discount 2, quantity 7, zero 0, + // score -12.7, ratio 0.5, half 2.5, title 'Dart Pipelines'. + ctx.functionCases('comparison functions', [ + FunctionCase('equal', Expression.field('price').equal(10), true), + FunctionCase('notEqual', Expression.field('price').notEqual(11), true), + FunctionCase( + 'greaterThan', + Expression.field('price').greaterThan(9), + true, + ), + FunctionCase( + 'greaterThanOrEqual', + Expression.field('price').greaterThanOrEqual(10), + true, + ), + FunctionCase('lessThan', Expression.field('price').lessThan(11), true), + FunctionCase( + 'lessThanOrEqual', + Expression.field('price').lessThanOrEqual(10), + true, + ), + FunctionCase( + 'lessThan is false at the boundary', + Expression.field('price').lessThan(10), + false, + ), + FunctionCase( + 'equal against an expression', + Expression.field('price').equal(Expression.field('rating').multiply(2)), + true, + ), + FunctionCase( + 'notEqual against an expression', + Expression.field('price').notEqual(Expression.field('discount')), + true, + ), + FunctionCase( + 'greaterThan against an expression', + Expression.field('price').greaterThan(Expression.field('quantity')), + true, + ), + FunctionCase( + 'greaterThanOrEqual against an expression', + Expression.field( + 'discount', + ).greaterThanOrEqual(Expression.field('rating')), + false, + ), + FunctionCase( + 'lessThan against an expression', + Expression.field('rating').lessThan(Expression.field('price')), + true, + ), + FunctionCase( + 'lessThanOrEqual against an expression', + Expression.field('price').lessThanOrEqual(Expression.field('discount')), + false, + ), + ]); + + // In the static and top-level forms the left operand is a field position + // (a String names a field) while the right operand is a value position (a + // String is a string literal). + ctx.functionCases('comparison functions (PipelineFunctions)', [ + FunctionCase( + 'equal with a field name and a literal', + PipelineFunctions.equal('rating', 5), + true, + ), + FunctionCase( + 'equal with two expressions', + PipelineFunctions.equal( + Expression.field('price'), + Expression.field('rating').multiply(2), + ), + true, + ), + FunctionCase( + 'notEqual with a field name and a literal', + PipelineFunctions.notEqual('title', 'Firestore Admin'), + true, + ), + FunctionCase( + 'notEqual with two expressions', + PipelineFunctions.notEqual( + Expression.field('quantity'), + Expression.field('stats').mapGetLiteral('likes'), + ), + false, + ), + FunctionCase( + 'lessThan with a field name and a literal', + PipelineFunctions.lessThan('score', 0), + true, + ), + FunctionCase( + 'lessThan with a literal left operand', + PipelineFunctions.lessThan(5, Expression.field('price')), + true, + ), + FunctionCase( + 'lessThan with two expressions', + PipelineFunctions.lessThan( + Expression.field('price'), + Expression.field('discount'), + ), + false, + ), + FunctionCase( + 'lessThanOrEqual with a field name and a literal', + PipelineFunctions.lessThanOrEqual('zero', 0), + true, + ), + FunctionCase( + 'lessThanOrEqual with two expressions', + PipelineFunctions.lessThanOrEqual( + Expression.field('quantity'), + Expression.field('discount'), + ), + false, + ), + FunctionCase( + 'greaterThan with a field name and a string literal', + PipelineFunctions.greaterThan('title', 'Dart'), + true, + ), + FunctionCase( + 'greaterThan with two expressions', + PipelineFunctions.greaterThan( + Expression.field('zero'), + Expression.field('quantity'), + ), + false, + ), + FunctionCase( + 'greaterThanOrEqual with a field name and a literal', + PipelineFunctions.greaterThanOrEqual('rating', 5), + true, + ), + FunctionCase( + 'greaterThanOrEqual with two expressions', + PipelineFunctions.greaterThanOrEqual( + Expression.field('ratio'), + Expression.field('half'), + ), + false, + ), + ]); + + ctx.functionCases('comparison functions (top-level)', [ + FunctionCase( + 'equal with a field name and a literal', + equal('price', 10), + true, + ), + FunctionCase( + 'equal treats a right-hand String as a literal', + equal('title', 'title'), + false, + ), + FunctionCase( + 'equal with two expressions', + equal(Expression.field('rating'), Expression.field('price')), + false, + ), + FunctionCase( + 'notEqual with a field name and a literal', + notEqual('price', 11), + true, + ), + FunctionCase( + 'notEqual with two expressions', + notEqual(Expression.field('price'), Expression.constant(10)), + false, + ), + FunctionCase( + 'lessThan with a field name and an expression', + lessThan('rating', Expression.field('price')), + true, + ), + FunctionCase( + 'lessThan with an expression and a literal', + lessThan(Expression.field('price'), 10), + false, + ), + FunctionCase( + 'lessThanOrEqual with a field name and a literal', + lessThanOrEqual('price', 10), + true, + ), + FunctionCase( + 'lessThanOrEqual with two expressions', + lessThanOrEqual(Expression.field('price'), Expression.field('rating')), + false, + ), + FunctionCase( + 'greaterThan with a field name and an expression', + greaterThan('price', Expression.field('rating')), + true, + ), + FunctionCase( + 'greaterThan with an expression and a literal', + greaterThan(Expression.field('rating'), 5), + false, + ), + FunctionCase( + 'greaterThanOrEqual with a field name and a literal', + greaterThanOrEqual('rating', 5), + true, + ), + FunctionCase( + 'greaterThanOrEqual with two expressions', + greaterThanOrEqual( + Expression.field('discount'), + Expression.field('rating'), + ), + false, + ), + ]); + + // cmp returns -1, 0 or 1 as the left operand sorts before, equal to or + // after the right one. + ctx.functionCases('cmp', [ + FunctionCase( + 'cmp', + PipelineFunctions.cmp(Expression.field('price'), 10), + 0, + ), + FunctionCase( + 'cmp with a field name and an expression', + PipelineFunctions.cmp('rating', Expression.field('price')), + isInt(-1), + ), + FunctionCase( + 'cmp with two expressions', + PipelineFunctions.cmp( + Expression.field('price'), + Expression.field('discount'), + ), + isInt(1), + ), + ]); + + // Book 1: active true, archived false. + ctx.functionCases('logical functions', [ + FunctionCase( + 'and', + PipelineFunctions.and([true, Expression.field('active')]), + true, + ), + FunctionCase( + 'or', + PipelineFunctions.or([false, Expression.field('active')]), + true, + ), + FunctionCase('xor', PipelineFunctions.xor([true, false]), true), + FunctionCase( + 'xor with expressions', + PipelineFunctions.xor([ + Expression.field('active'), + Expression.field('archived'), + ]), + true, + ), + FunctionCase( + 'xor with two true operands', + PipelineFunctions.xor([ + Expression.field('price').equal(10), + Expression.field('rating').equal(5), + Expression.field('archived'), + ]), + false, + ), + FunctionCase('nor', PipelineFunctions.nor([false, false]), true), + FunctionCase( + 'nor with expressions', + PipelineFunctions.nor([ + Expression.field('archived'), + Expression.field('price').greaterThan(10), + ]), + true, + ), + FunctionCase( + 'nor with a true operand', + PipelineFunctions.nor([false, Expression.field('active')]), + false, + ), + FunctionCase('not', PipelineFunctions.not(false), true), + FunctionCase( + 'not with an expression', + PipelineFunctions.not(Expression.field('active')), + false, + ), + FunctionCase( + 'fluent not', + Expression.field('archived').equal(true).not(), + true, + ), + FunctionCase( + 'top-level and', + and([equal('price', 10), greaterThan('rating', 4)]), + true, + ), + FunctionCase( + 'top-level and with a false operand', + and([equal('price', 10), equal('archived', true)]), + false, + ), + FunctionCase( + 'top-level or', + or([equal('price', 20), equal('rating', 5)]), + true, + ), + FunctionCase( + 'top-level or with no true operand', + or([equal('price', 20), lessThan('rating', 3)]), + false, + ), + FunctionCase('top-level not', not(equal('price', 10)), false), + ]); + + ctx.functionCases('conditional', [ + FunctionCase( + 'conditional', + PipelineFunctions.conditional(Expression.field('active'), 'yes', 'no'), + 'yes', + ), + FunctionCase( + 'conditional with a literal condition and expression branches', + PipelineFunctions.conditional( + false, + Expression.field('price'), + Expression.field('rating'), + ), + isInt(5), + ), + FunctionCase( + 'fluent conditional with literal branches', + Expression.field( + 'rating', + ).greaterThanOrEqual(5).conditional('great', 'good'), + 'great', + ), + FunctionCase( + 'fluent conditional with expression branches', + Expression.field('price') + .greaterThan(15) + .conditional( + Expression.field('price'), + Expression.field('discount'), + ), + isInt(2), + ), + ]); + + // Book 1: nullable null, no field named `missing`. coalesce returns its + // first argument that is neither absent nor null. + ctx.functionCases('ifNull and coalesce', [ + FunctionCase( + 'ifNull', + PipelineFunctions.ifNull(Expression.field('nullable'), 'fallback'), + 'fallback', + ), + FunctionCase( + 'ifNull with a field name and an expression', + PipelineFunctions.ifNull('nullable', Expression.field('price')), + isInt(10), + ), + FunctionCase( + 'ifNull keeps a non-null value', + PipelineFunctions.ifNull('title', 'fallback'), + 'Dart Pipelines', + ), + FunctionCase( + 'fluent ifNull with a literal', + Expression.field('nullable').ifNull(42), + isInt(42), + ), + FunctionCase( + 'fluent ifNull with an expression', + Expression.field('nullable').ifNull(Expression.field('title')), + 'Dart Pipelines', + ), + FunctionCase( + 'fluent ifNull keeps a non-null value', + Expression.field('price').ifNull(0), + isInt(10), + ), + FunctionCase( + 'coalesce with a field name and a literal', + PipelineFunctions.coalesce('nullable', 'fallback'), + 'fallback', + ), + FunctionCase( + 'coalesce keeps a present value', + PipelineFunctions.coalesce('price', 0), + isInt(10), + ), + FunctionCase( + 'coalesce skips absent and null values', + PipelineFunctions.coalesce( + Expression.field('missing'), + Expression.field('nullable'), + [Expression.field('title'), 'unused'], + ), + 'Dart Pipelines', + ), + FunctionCase( + 'coalesce falls through to a literal', + PipelineFunctions.coalesce( + Expression.field('missing'), + Expression.field('nullable'), + ['last'], + ), + 'last', + ), + FunctionCase( + 'fluent coalesce with a literal', + Expression.field('nullable').coalesce('fallback'), + 'fallback', + ), + FunctionCase( + 'fluent coalesce with expressions', + Expression.field('missing').coalesce(Expression.field('nullable'), [ + Expression.field('rating'), + 0, + ]), + isInt(5), + ), + ]); + + // switchOn takes alternating conditions and results, and an optional + // trailing default; it returns the result of the first true condition. + ctx.functionCases('switchOn', [ + FunctionCase( + 'switchOn picks the first true condition', + PipelineFunctions.switchOn([ + Expression.field('price').greaterThan(20), + 'expensive', + Expression.field('price').greaterThan(5), + 'mid', + 'cheap', + ]), + 'mid', + ), + FunctionCase( + 'switchOn falls back to the default', + PipelineFunctions.switchOn([ + Expression.field('archived'), + 'archived', + Expression.field('price').lessThan(5), + 'cheap', + Expression.field('title'), + ]), + 'Dart Pipelines', + ), + FunctionCase( + 'switchOn with literal conditions', + PipelineFunctions.switchOn([ + false, + 'no', + true, + Expression.field('rating'), + ]), + isInt(5), + ), + ]); + + ctx.functionCases('equalAny and notEqualAny', [ + // The first argument is a field position: a bare String means a field + // reference, not a string literal. Passing 'dart' here asked about a + // non-existent field named `dart`, which made equalAny false and made + // notEqualAny true for the wrong reason. + FunctionCase( + 'equalAny', + PipelineFunctions.equalAny( + 'title', + Expression.array(['Dart Pipelines', 'Unrelated Title']), + ), + true, + ), + FunctionCase( + 'notEqualAny', + PipelineFunctions.notEqualAny( + 'title', + Expression.array(['Unrelated Title', 'Another Title']), + ), + true, + ), + FunctionCase( + 'equalAny with an expression and a list', + PipelineFunctions.equalAny(Expression.field('price'), [10, 20]), + true, + ), + FunctionCase( + 'notEqualAny with an expression and a list', + PipelineFunctions.notEqualAny(Expression.field('price'), [10, 20]), + false, + ), + FunctionCase( + 'fluent equalAny with an array field', + Expression.constant('firebase').equalAny(Expression.field('tags')), + true, + ), + FunctionCase( + 'fluent notEqualAny with a list', + Expression.field( + 'title', + ).notEqualAny(['Firestore Admin', 'Inactive Draft']), + true, + ), + FunctionCase( + 'fluent notEqualAny with an array field', + Expression.constant('graphql').notEqualAny(Expression.field('tags')), + true, + ), + ]); + + // Collections that hold expressions must be built with array(...) / + // map(...); the backend rejects them inside a literal array or map value. + ctx.functionCases('collections holding expressions', [ + FunctionCase( + 'equalAny', + PipelineFunctions.equalAny('rating', [Expression.field('score'), 5]), + true, + ), + FunctionCase( + 'equalAnyMatchesExpression', + Expression.field( + 'price', + ).equalAny([Expression.field('rating').multiply(2), 3]), + true, + ), + FunctionCase( + 'notEqualAny', + PipelineFunctions.notEqualAny('rating', [Expression.field('price'), 4]), + true, + ), + FunctionCase( + 'fluent notEqualAny', + Expression.field('rating').notEqualAny([ + Expression.field('discount'), + Expression.field('price').subtract(5), + ]), + false, + ), + FunctionCase( + 'equal', + Expression.field('tags').equal([ + Expression.field('metadata').mapGetLiteral('lang'), + 'firebase', + ]), + true, + ), + FunctionCase( + 'top-level equal', + equal('tags', [ + Expression.field('metadata').mapGetLiteral('lang'), + 'firebase', + ]), + true, + ), + // Book 1's stats are {views: 100, likes: 7}. + FunctionCase( + 'PipelineFunctions.equal with a map', + PipelineFunctions.equal('stats', { + 'views': Expression.field('price').multiply(10), + 'likes': Expression.field('quantity'), + }), + true, + ), + FunctionCase( + 'conditional', + PipelineFunctions.conditional( + Expression.field('price').greaterThan(5), + [Expression.field('price'), Expression.field('rating')], + Expression.field('tags'), + ), + [10, 5], + ), + FunctionCase( + 'fluent conditional', + Expression.field('price').equal(10).conditional({ + 'doubled': Expression.field('price').multiply(2), + }, 'n/a'), + {'doubled': 20}, + ), + ]); + + // Books 1-3: price 10/20/30, rating 5/4/2, discount 2/3/4, + // active true/true/false, archived false/false/true. + ctx.filterCases('filters', [ + // Regression: the list was sent as a literal array value, which the + // backend rejected with "Value type is not supported: + // FIELD_REFERENCE_VALUE". Book 1 matches the literal 2, book 2 its own + // `flags` (3). + FilterCase( + 'filters with a search space holding expressions', + PipelineFunctions.equalAny('discount', [Expression.field('flags'), 2]), + ['Dart Pipelines', 'Firestore Admin'], + ), + FilterCase( + 'compares two fields per document', + Expression.field('rating').lessThan(Expression.field('discount')), + ['Inactive Draft'], + ), + FilterCase( + 'and', + and([greaterThan('price', 10), lessThan('rating', 5)]), + ['Firestore Admin', 'Inactive Draft'], + ), + FilterCase('or', or([equal('price', 10), equal('active', false)]), [ + 'Dart Pipelines', + 'Inactive Draft', + ]), + FilterCase('not', not(equal('active', true)), ['Inactive Draft']), + FilterCase( + 'xor', + PipelineFunctions.xor([ + Expression.field('active'), + greaterThan('price', 15), + ]), + ['Dart Pipelines', 'Inactive Draft'], + ), + FilterCase( + 'nor', + PipelineFunctions.nor([ + Expression.field('archived'), + greaterThan('price', 15), + ]), + ['Dart Pipelines'], + ), + // The second entry is rating * 5. Book 1: 10 is in [10, 25]; book 2: 20 + // is in [10, 20]; book 3: 30 is not in [10, 10]. + FilterCase( + 'notEqualAny with a per-document search space', + Expression.field( + 'price', + ).notEqualAny([10, Expression.field('rating').multiply(5)]), + ['Inactive Draft'], + ), + ]); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/functions_map_reference_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/functions_map_reference_test.dart new file mode 100644 index 00000000..159a286e --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/functions_map_reference_test.dart @@ -0,0 +1,473 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of the map and reference Pipeline functions. +@Tags(['prod']) +library; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +import 'harness.dart'; + +/// Matches a [DocumentReference] to the same path as [expected]. +Matcher isReferenceTo(DocumentReference expected) { + return isA>().having( + (reference) => reference.path, + 'path', + expected.path, + ); +} + +void main() { + pipelineE2E('Pipeline map and reference functions', (ctx) { + // Book 1's map fields, from the seed table. + const metadata = {'lang': 'dart', 'category': 'sdk'}; + const stats = {'views': 100, 'likes': 7}; + + ctx.functionCases('map construction and access', [ + FunctionCase('map', PipelineFunctions.map(['a', 1, 'b', 2]), { + 'a': 1, + 'b': 2, + }), + FunctionCase( + 'map with expression values', + PipelineFunctions.map([ + 'title', + Expression.field('title'), + 'rating', + Expression.field('rating'), + ]), + {'title': 'Dart Pipelines', 'rating': 5}, + ), + // A Map is sent like the alternating key/value form. + FunctionCase( + 'map from a Map', + PipelineFunctions.map({ + 'title': Expression.field('title'), + 'answer': 42, + }), + {'title': 'Dart Pipelines', 'answer': 42}, + ), + FunctionCase( + 'map from a Map holding a nested Map', + PipelineFunctions.map({ + 'nested': {'price': Expression.field('price')}, + }), + { + 'nested': {'price': 10}, + }, + ), + FunctionCase( + 'mapGetLiteral', + Expression.field('metadata').mapGetLiteral('category'), + 'sdk', + ), + FunctionCase( + 'mapGet', + Expression.field('metadata').mapGet('lang'), + 'dart', + ), + FunctionCase( + 'mapGet with an expression key', + Expression.field('metadata').mapGet(Expression.constant('category')), + 'sdk', + ), + // The key is a literal map key, not a field path. + FunctionCase( + 'mapGet with a dotted key', + Expression.field('nested').mapGet('dotted.key'), + 'dot', + ), + FunctionCase( + 'static mapGet on a field name', + PipelineFunctions.mapGet('stats', 'views'), + isInt(100), + ), + FunctionCase( + 'static mapGet on an expression', + PipelineFunctions.mapGet( + PipelineFunctions.mapGet('nested', 'level1'), + Expression.constant('level2'), + ), + {'value': 42}, + ), + FunctionCase( + 'getField', + Expression.field('stats').getField('likes'), + isInt(7), + ), + FunctionCase( + 'getField with an expression key', + Expression.field('metadata').getField(Expression.constant('lang')), + 'dart', + ), + FunctionCase( + 'static getField on a field name', + PipelineFunctions.getField('metadata', 'category'), + 'sdk', + ), + FunctionCase( + 'static getField on the current document', + PipelineFunctions.getField( + PipelineFunctions.currentDocument(), + Expression.constant('title'), + ), + 'Dart Pipelines', + ), + FunctionCase( + 'currentDocument', + currentDocument(), + allOf( + containsPair('title', 'Dart Pipelines'), + containsPair('price', 10), + containsPair('metadata', metadata), + ), + ), + ]); + + ctx.functionCases('map updates', [ + FunctionCase( + 'mapSet', + Expression.field('metadata').mapSet('edition', 'enterprise'), + {...metadata, 'edition': 'enterprise'}, + ), + FunctionCase( + 'mapSet overwrites a key and sets more pairs', + Expression.field( + 'metadata', + ).mapSet('lang', 'go', ['edition', 'enterprise']), + {'lang': 'go', 'category': 'sdk', 'edition': 'enterprise'}, + ), + FunctionCase( + 'mapSet with expression keys and values', + Expression.field('stats').mapSet( + Expression.constant('shares'), + Expression.field('rating'), + [Expression.constant('likes'), Expression.field('discount')], + ), + {'views': 100, 'likes': 2, 'shares': 5}, + ), + // Setting a key to an absent value removes it. + FunctionCase( + 'mapSet to an absent value', + Expression.field( + 'metadata', + ).mapSet('lang', Expression.field('missing')), + {'category': 'sdk'}, + ), + FunctionCase( + 'static mapSet on a field name', + PipelineFunctions.mapSet('metadata', ['edition', 'enterprise']), + {...metadata, 'edition': 'enterprise'}, + ), + FunctionCase( + 'static mapSet on an expression', + PipelineFunctions.mapSet(Expression.field('stats'), [ + 'views', + Expression.field('price'), + ]), + {'views': 10, 'likes': 7}, + ), + FunctionCase( + 'mapRemove', + Expression.field('metadata').mapRemove('lang'), + {'category': 'sdk'}, + ), + FunctionCase( + 'mapRemove with an expression key', + Expression.field('metadata').mapRemove(Expression.constant('lang')), + {'category': 'sdk'}, + ), + FunctionCase( + 'chained mapRemove', + Expression.field('metadata').mapRemove('lang').mapRemove('category'), + {}, + ), + FunctionCase( + 'mapRemove of an absent key', + Expression.field('metadata').mapRemove('missing'), + metadata, + ), + FunctionCase( + 'static mapRemove on a field name', + PipelineFunctions.mapRemove('stats', 'likes'), + {'views': 100}, + ), + FunctionCase( + 'static mapRemove on an expression', + PipelineFunctions.mapRemove( + PipelineFunctions.map(['a', 1, 'b', 2]), + Expression.constant('a'), + ), + {'b': 2}, + ), + FunctionCase( + 'mapMerge', + Expression.field('metadata').mapMerge([ + {'edition': 'enterprise'}, + ]), + {...metadata, 'edition': 'enterprise'}, + ), + FunctionCase( + 'mapMerge with a map expression', + Expression.field('metadata').mapMerge([Expression.field('stats')]), + {...metadata, ...stats}, + ), + FunctionCase( + 'mapMerge with several maps, later keys winning', + Expression.field('metadata').mapMerge([ + {'lang': 'go'}, + Expression.field('stats'), + ]), + {'lang': 'go', 'category': 'sdk', ...stats}, + ), + FunctionCase( + 'static mapMerge on a field name', + PipelineFunctions.mapMerge([ + 'metadata', + {'edition': 'enterprise'}, + ]), + {...metadata, 'edition': 'enterprise'}, + ), + FunctionCase( + 'static mapMerge on expressions', + PipelineFunctions.mapMerge([ + Expression.field('stats'), + Expression.field('metadata'), + ]), + {...stats, ...metadata}, + ), + ]); + + // The backend does not guarantee the order of keys, values or entries. + ctx.functionCases('map decomposition', [ + FunctionCase( + 'mapKeys', + Expression.field('metadata').mapKeys(), + unorderedEquals(['lang', 'category']), + ), + FunctionCase( + 'static mapKeys on a field name', + PipelineFunctions.mapKeys('stats'), + unorderedEquals(['views', 'likes']), + ), + FunctionCase( + 'static mapKeys on an expression', + PipelineFunctions.mapKeys(Expression.field('emptyMap')), + [], + ), + FunctionCase( + 'mapValues', + Expression.field('metadata').mapValues(), + unorderedEquals(['dart', 'sdk']), + ), + FunctionCase( + 'static mapValues on a field name', + PipelineFunctions.mapValues('stats'), + unorderedEquals([100, 7]), + ), + // `nested.level1` is {level2: {value: 42}}, so its single value is the + // `level2` map. + FunctionCase( + 'static mapValues on an expression', + PipelineFunctions.mapValues(Expression.field('nested.level1')), + [ + {'value': 42}, + ], + ), + FunctionCase( + 'mapEntries', + Expression.field('metadata').mapEntries(), + unorderedEquals([ + {'k': 'lang', 'v': 'dart'}, + {'k': 'category', 'v': 'sdk'}, + ]), + ), + FunctionCase( + 'static mapEntries on a field name', + PipelineFunctions.mapEntries('stats'), + unorderedEquals([ + {'k': 'views', 'v': 100}, + {'k': 'likes', 'v': 7}, + ]), + ), + FunctionCase( + 'static mapEntries on an expression', + PipelineFunctions.mapEntries(Expression.field('emptyMap')), + [], + ), + ]); + + // Book 1's `pathRef` is book 2, a top-level document; `chapter` is a + // document in a subcollection of book 1, so its parent is a document. + final chapter = ctx.firestore.doc('${ctx.book1Ref.path}/chapters/c1'); + + ctx.functionCases('reference functions', [ + FunctionCase( + 'collectionId', + Expression.field('pathRef').collectionId(), + pipelineE2ECollection, + ), + FunctionCase( + 'collectionId of a subcollection document', + Expression.constant(chapter).collectionId(), + 'chapters', + ), + FunctionCase( + 'static collectionId on a field name', + PipelineFunctions.collectionId('pathRef'), + pipelineE2ECollection, + ), + FunctionCase( + 'static collectionId on an expression', + PipelineFunctions.collectionId(Expression.field('__name__')), + pipelineE2ECollection, + ), + FunctionCase( + 'documentId', + Expression.field('pathRef').documentId(), + ctx.book2Ref.id, + ), + FunctionCase( + 'static documentId on an expression', + PipelineFunctions.documentId(Expression.field('__name__')), + ctx.book1Ref.id, + ), + FunctionCase( + 'static documentId on a DocumentReference', + PipelineFunctions.documentId(chapter), + 'c1', + ), + FunctionCase( + 'parent', + Expression.constant(chapter).parent(), + isReferenceTo(ctx.book1Ref), + ), + FunctionCase( + 'static parent on a DocumentReference', + PipelineFunctions.parent(chapter), + isReferenceTo(ctx.book1Ref), + ), + FunctionCase( + 'static parent on an expression', + PipelineFunctions.parent(Expression.constant(chapter)), + isReferenceTo(ctx.book1Ref), + ), + // The parent of a top-level document is the database root, a reference + // that has no parent itself. + FunctionCase( + 'parent of a top-level document', + PipelineFunctions.parent( + Expression.field('pathRef'), + ).isType(PipelineValueType.reference), + true, + ), + FunctionCase( + 'parent of the root', + Expression.field('pathRef').parent().parent(), + isNull, + ), + // A reference is a list of (collection ID, document ID) segments, which + // referenceSlice slices like an array. + FunctionCase( + 'referenceSlice', + Expression.constant(chapter).referenceSlice(0, 1), + isReferenceTo(ctx.book1Ref), + ), + FunctionCase( + 'referenceSlice with expression bounds', + Expression.field( + 'pathRef', + ).referenceSlice(Expression.constant(0), Expression.constant(1)), + isReferenceTo(ctx.book2Ref), + ), + FunctionCase( + 'static referenceSlice on a field name', + PipelineFunctions.referenceSlice('pathRef', 0, 1), + isReferenceTo(ctx.book2Ref), + ), + FunctionCase( + 'static referenceSlice on an expression', + PipelineFunctions.referenceSlice( + Expression.constant(chapter), + Expression.constant(1), + Expression.constant(1), + ), + isReferenceTo(ctx.firestore.doc('chapters/c1')), + ), + ]); + + // Collections that hold expressions must be built with array(...) / + // map(...); the backend rejects them inside a literal array or map value. + ctx.functionCases('collections holding expressions', [ + FunctionCase( + 'mapMerge', + Expression.field('metadata').mapMerge([ + {'title': Expression.field('title')}, + ]), + {...metadata, 'title': 'Dart Pipelines'}, + ), + FunctionCase( + 'static mapMerge', + PipelineFunctions.mapMerge([ + Expression.field('metadata'), + {'price': Expression.field('price')}, + ]), + {...metadata, 'price': 10}, + ), + FunctionCase( + 'mapSet with a map value', + Expression.field( + 'metadata', + ).mapSet('extra', {'price': Expression.field('price')}), + { + ...metadata, + 'extra': {'price': 10}, + }, + ), + FunctionCase( + 'mapSet with an array value', + Expression.field( + 'metadata', + ).mapSet('extra', [Expression.field('price'), 1]), + { + ...metadata, + 'extra': [10, 1], + }, + ), + FunctionCase( + 'static mapSet', + PipelineFunctions.mapSet('stats', [ + 'extra', + {'rating': Expression.field('rating')}, + ]), + { + ...stats, + 'extra': {'rating': 5}, + }, + ), + FunctionCase( + 'nestedMap', + PipelineFunctions.map([ + 'nested', + {'price': Expression.field('price')}, + ]), + { + 'nested': {'price': 10}, + }, + ), + ]); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/functions_string_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/functions_string_test.dart new file mode 100644 index 00000000..9a1dc89b --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/functions_string_test.dart @@ -0,0 +1,693 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of the string Pipeline functions and the generic `length`, +/// `reverse` and `concat` functions. +/// +/// Each function is exercised in its static form with a field-name `String` +/// and with an expression, and in its fluent form; value arguments are passed +/// both as plain values and as expressions (`constant(...)` or a field). +@Tags(['prod']) +library; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +import 'harness.dart'; + +void main() { + pipelineE2E('Pipeline string functions', (ctx) { + // `unicode` is 'café ☕ 日本': 9 code points, 16 UTF-8 bytes. + ctx.functionCases('lengths', [ + FunctionCase( + 'byteLength', + Expression.field('title').byteLength(), + isInt(14), + ), + FunctionCase( + 'byteLength static counts UTF-8 bytes', + PipelineFunctions.byteLength('unicode'), + isInt(16), + ), + FunctionCase( + 'byteLength static of a bytes value', + PipelineFunctions.byteLength(Expression.field('bytes')), + isInt(2), + ), + FunctionCase( + 'charLength', + Expression.field('empty').charLength(), + isInt(0), + ), + FunctionCase( + 'charLength static counts code points', + PipelineFunctions.charLength('unicode'), + isInt(9), + ), + FunctionCase( + 'charLength static with an expression', + PipelineFunctions.charLength(Expression.field('greeting')), + isInt(13), + ), + FunctionCase( + 'length of a string', + Expression.field('title').length(), + isInt(14), + ), + FunctionCase( + 'length of a vector', + Expression.field('embedding').length(), + isInt(3), + ), + FunctionCase( + 'length static of an array', + PipelineFunctions.length('tags'), + isInt(2), + ), + FunctionCase( + 'length static of a map', + PipelineFunctions.length(Expression.field('metadata')), + isInt(2), + ), + ]); + + ctx.functionCases('reverse', [ + FunctionCase( + 'reverse', + Expression.field('greeting').reverse(), + '!dlroW ,olleH', + ), + FunctionCase( + 'reverse static', + PipelineFunctions.reverse('prefix'), + 'traD', + ), + FunctionCase( + 'reverse static with an expression', + PipelineFunctions.reverse(Expression.field('title')), + 'senilepiP traD', + ), + FunctionCase( + 'stringReverse', + Expression.field('semicolons').stringReverse(), + 'c;b;a', + ), + FunctionCase( + 'stringReverse static', + PipelineFunctions.stringReverse('title'), + 'senilepiP traD', + ), + FunctionCase( + 'stringReverse static with an expression', + PipelineFunctions.stringReverse(Expression.constant('Dart')), + 'traD', + ), + ]); + + ctx.functionCases('concatenation', [ + FunctionCase( + 'concat', + Expression.field('title').concat([' v2']), + 'Dart Pipelines v2', + ), + FunctionCase( + 'concat with an expression', + Expression.field('prefix').concat([', ', Expression.field('greeting')]), + 'Dart, Hello, World!', + ), + // A field-name first argument; the other Strings are literals. + FunctionCase( + 'concat static with more values', + PipelineFunctions.concat('prefix', ' & ', [ + Expression.field('title'), + '!', + ]), + 'Dart & Dart Pipelines!', + ), + FunctionCase( + 'concat static of two arrays', + PipelineFunctions.concat( + Expression.field('tags'), + Expression.field('words'), + ), + ['dart', 'firebase', 'dart', 'firebase'], + ), + FunctionCase( + 'stringConcat', + Expression.field( + 'prefix', + ).stringConcat([' + ', Expression.field('title')]), + 'Dart + Dart Pipelines', + ), + // The first value names a field; the others are literals or expressions. + FunctionCase( + 'stringConcat static', + PipelineFunctions.stringConcat([ + 'prefix', + ' ', + Expression.field('greeting'), + ]), + 'Dart Hello, World!', + ), + ]); + + ctx.functionCases('prefix, suffix and pattern predicates', [ + FunctionCase( + 'startsWith', + Expression.field('title').startsWith('Dart'), + true, + ), + FunctionCase( + 'startsWith is case-sensitive', + Expression.field('title').startsWith('dart'), + false, + ), + FunctionCase( + 'startsWith with an expression', + Expression.field('sentence').startsWith(Expression.field('prefix')), + true, + ), + FunctionCase( + 'startsWith static', + PipelineFunctions.startsWith('greeting', 'Hello'), + true, + ), + FunctionCase( + 'endsWith', + Expression.field('title').endsWith('lines'), + true, + ), + FunctionCase( + 'endsWith with an expression', + Expression.field('sentence').endsWith(Expression.constant('fast')), + true, + ), + FunctionCase( + 'endsWith static', + PipelineFunctions.endsWith('email', '.com'), + true, + ), + FunctionCase('like', Expression.field('title').like('Dart%'), true), + FunctionCase( + 'like is case-sensitive', + Expression.field('title').like('dart%'), + false, + ), + FunctionCase( + 'like static', + PipelineFunctions.like('email', '%@example.com'), + true, + ), + FunctionCase( + 'like static with an expression', + PipelineFunctions.like( + Expression.field('title'), + Expression.constant('%Pipe%'), + ), + true, + ), + FunctionCase( + 'stringContains', + Expression.field('title').stringContains('Pipeline'), + true, + ), + FunctionCase( + 'stringContains with an expression', + Expression.field( + 'email', + ).stringContains(Expression.constant('@example.')), + true, + ), + FunctionCase( + 'stringContains static', + PipelineFunctions.stringContains('sentence', 'fun'), + true, + ), + ]); + + // Patterns use RE2 syntax. + ctx.functionCases('regular expressions', [ + FunctionCase( + 'regexContains', + Expression.field('title').regexContains('Pipe'), + true, + ), + FunctionCase( + 'regexContains is case-sensitive', + Expression.field('title').regexContains('pipe'), + false, + ), + FunctionCase( + 'regexContains static with an expression', + PipelineFunctions.regexContains( + Expression.field('title'), + Expression.constant('(?i)pipe'), + ), + true, + ), + FunctionCase( + 'regexMatch', + Expression.field('title').regexMatch(r'^Dart.*'), + true, + ), + FunctionCase( + 'regexMatch static', + PipelineFunctions.regexMatch('csv', '[a-z,]+'), + true, + ), + // Unlike regexContains, the pattern must match the whole string. + FunctionCase( + 'regexMatch static needs a full match', + PipelineFunctions.regexMatch( + Expression.field('title'), + Expression.constant('Dart'), + ), + false, + ), + FunctionCase( + 'regexFind', + Expression.field('title').regexFind(r'P\w+'), + 'Pipelines', + ), + FunctionCase( + 'regexFind with an expression', + Expression.field('greeting').regexFind(Expression.constant(r'\w+')), + 'Hello', + ), + FunctionCase( + 'regexFind static', + PipelineFunctions.regexFind('email', '@.+'), + '@example.com', + ), + FunctionCase( + 'regexFind static returns the first match', + PipelineFunctions.regexFind( + Expression.field('sentence'), + Expression.constant(r'f\w+'), + ), + 'fun', + ), + FunctionCase( + 'regexFindAll', + Expression.field('title').regexFindAll('[aei]'), + ['a', 'i', 'e', 'i', 'e'], + ), + FunctionCase( + 'regexFindAll with an expression', + Expression.field('semicolons').regexFindAll(Expression.constant(r'\w')), + ['a', 'b', 'c'], + ), + FunctionCase( + 'regexFindAll static', + PipelineFunctions.regexFindAll('csv', '[a-z]+'), + ['dart', 'firebase', 'admin'], + ), + FunctionCase( + 'regexFindAll static with an expression', + PipelineFunctions.regexFindAll( + Expression.field('sentence'), + Expression.constant('Dart'), + ), + ['Dart', 'Dart'], + ), + ]); + + // Indexes are 0-based. + ctx.functionCases('stringIndexOf', [ + FunctionCase( + 'stringIndexOf', + Expression.field('title').stringIndexOf('Pipeline'), + isInt(5), + ), + FunctionCase( + 'stringIndexOf with an expression', + Expression.field( + 'semicolons', + ).stringIndexOf(Expression.field('delimiter')), + isInt(1), + ), + FunctionCase( + 'stringIndexOf static', + PipelineFunctions.stringIndexOf('sentence', 'fun'), + isInt(8), + ), + FunctionCase( + 'stringIndexOf static with an expression', + PipelineFunctions.stringIndexOf( + Expression.field('greeting'), + Expression.constant('World'), + ), + isInt(7), + ), + ]); + + ctx.functionCases('case conversion', [ + FunctionCase( + 'toUpperCase', + Expression.field('title').toUpperCase(), + 'DART PIPELINES', + ), + FunctionCase( + 'toLowerCase', + Expression.field('title').toLowerCase(), + 'dart pipelines', + ), + FunctionCase( + 'toUpper static', + PipelineFunctions.toUpper('prefix'), + 'DART', + ), + FunctionCase( + 'toUpper static with an expression', + PipelineFunctions.toUpper(Expression.field('greeting')), + 'HELLO, WORLD!', + ), + FunctionCase( + 'toLower static', + PipelineFunctions.toLower('title'), + 'dart pipelines', + ), + FunctionCase( + 'toLower static with an expression', + PipelineFunctions.toLower(Expression.field('greeting')), + 'hello, world!', + ), + ]); + + // The position is a 0-based index and the optional second argument a + // length, not an end index. `rating` is 5 on book 1. + ctx.functionCases('substring', [ + FunctionCase( + 'substringLiteral', + Expression.field('title').substringLiteral(0, 4), + 'Dart', + ), + FunctionCase( + 'substringLiteral to the end', + Expression.field('email').substringLiteral(9), + 'example.com', + ), + FunctionCase( + 'substring with a length', + Expression.field('title').substring(5, 3), + 'Pip', + ), + FunctionCase( + 'substring to the end', + Expression.field('title').substring(5), + 'Pipelines', + ), + FunctionCase( + 'substring with expressions', + Expression.field( + 'greeting', + ).substring(Expression.constant(7), Expression.field('rating')), + 'World', + ), + FunctionCase( + 'substring static', + PipelineFunctions.substring('email', 0, 3), + 'dev', + ), + FunctionCase( + 'substring static to the end with an expression', + PipelineFunctions.substring( + Expression.field('title'), + Expression.field('rating'), + ), + 'Pipelines', + ), + FunctionCase( + 'substring static with an expression length', + PipelineFunctions.substring('sentence', 8, Expression.constant(3)), + 'fun', + ), + // Positions and lengths count code points for a string. + FunctionCase( + 'substring static counts code points', + PipelineFunctions.substring('unicode', 5, 1), + '☕', + ), + ]); + + // `discount` is 2 and `rating` 5 on book 1. + ctx.functionCases('stringRepeat', [ + FunctionCase( + 'stringRepeat', + Expression.constant('ha').stringRepeat(3), + 'hahaha', + ), + FunctionCase( + 'stringRepeat with an expression', + Expression.constant('-').stringRepeat(Expression.field('rating')), + '-----', + ), + FunctionCase( + 'stringRepeat static', + PipelineFunctions.stringRepeat('prefix', 2), + 'DartDart', + ), + FunctionCase( + 'stringRepeat static with expressions', + PipelineFunctions.stringRepeat( + Expression.field('delimiter'), + Expression.field('discount'), + ), + ';;', + ), + ]); + + // The search value is a literal substring, not a pattern. + ctx.functionCases('replacement', [ + FunctionCase( + 'stringReplaceAllLiteral', + Expression.field('title').stringReplaceAllLiteral('i', 'I'), + 'Dart PIpelInes', + ), + FunctionCase( + 'stringReplaceAll', + Expression.field('sentence').stringReplaceAll('Dart', 'Go'), + 'Go is fun and Go is fast', + ), + FunctionCase( + 'stringReplaceAll with expressions', + Expression.field('csv').stringReplaceAll( + Expression.constant(','), + Expression.field('delimiter'), + ), + 'dart;firebase;admin', + ), + FunctionCase( + 'stringReplaceAll static replaces a literal dot', + PipelineFunctions.stringReplaceAll('email', '.', '_'), + 'dev_team@example_com', + ), + FunctionCase( + 'stringReplaceAll static with expressions', + PipelineFunctions.stringReplaceAll( + Expression.field('semicolons'), + Expression.field('delimiter'), + Expression.constant(', '), + ), + 'a, b, c', + ), + FunctionCase( + 'stringReplaceOneLiteral', + Expression.field('title').stringReplaceOneLiteral('i', 'I'), + 'Dart PIpelines', + ), + FunctionCase( + 'stringReplaceOne', + Expression.field('email').stringReplaceOne('.', '_'), + 'dev_team@example.com', + ), + FunctionCase( + 'stringReplaceOne with expressions', + Expression.field('csv').stringReplaceOne( + Expression.constant(','), + Expression.constant(' & '), + ), + 'dart & firebase,admin', + ), + FunctionCase( + 'stringReplaceOne static', + PipelineFunctions.stringReplaceOne('sentence', 'Dart', 'Go'), + 'Go is fun and Dart is fast', + ), + FunctionCase( + 'stringReplaceOne static with expressions', + PipelineFunctions.stringReplaceOne( + Expression.field('semicolons'), + Expression.field('delimiter'), + Expression.constant('+'), + ), + 'a+b;c', + ), + ]); + + // Without a value, whitespace is trimmed; a value is a set of characters, + // not a substring. + ctx.functionCases('trimming', [ + FunctionCase('trim', Expression.field('spaced').trim(), 'Dart'), + FunctionCase( + 'trim a value', + Expression.field('padded').trim('_'), + 'Dart', + ), + FunctionCase( + 'trim an expression', + Expression.field('semicolons').trim(Expression.constant('ac')), + ';b;', + ), + FunctionCase('trim static', PipelineFunctions.trim('spaced'), 'Dart'), + FunctionCase( + 'trim static a value', + PipelineFunctions.trim(Expression.field('padded'), '_'), + 'Dart', + ), + FunctionCase( + 'trim static an expression', + PipelineFunctions.trim('greeting', Expression.constant('H!')), + 'ello, World', + ), + FunctionCase('ltrim', Expression.field('spaced').ltrim(), 'Dart '), + FunctionCase( + 'ltrim a value', + Expression.field('padded').ltrim('_'), + 'Dart__', + ), + FunctionCase( + 'ltrim an expression', + Expression.field('semicolons').ltrim(Expression.constant('a;')), + 'b;c', + ), + FunctionCase('ltrim static', PipelineFunctions.ltrim('spaced'), 'Dart '), + FunctionCase( + 'ltrim static a value', + PipelineFunctions.ltrim(Expression.field('padded'), '_'), + 'Dart__', + ), + FunctionCase( + 'ltrim static an expression', + PipelineFunctions.ltrim('greeting', Expression.constant('Hel')), + 'o, World!', + ), + FunctionCase('rtrim', Expression.field('spaced').rtrim(), ' Dart'), + FunctionCase( + 'rtrim a value', + Expression.field('padded').rtrim('_'), + '__Dart', + ), + FunctionCase( + 'rtrim an expression', + Expression.field('semicolons').rtrim(Expression.constant(';c')), + 'a;b', + ), + FunctionCase('rtrim static', PipelineFunctions.rtrim('spaced'), ' Dart'), + FunctionCase( + 'rtrim static a value', + PipelineFunctions.rtrim(Expression.field('padded'), '_'), + '__Dart', + ), + FunctionCase( + 'rtrim static an expression', + PipelineFunctions.rtrim('greeting', Expression.constant('!dl')), + 'Hello, Wor', + ), + ]); + + ctx.functionCases('split', [ + FunctionCase('splitLiteral', Expression.field('csv').splitLiteral(','), [ + 'dart', + 'firebase', + 'admin', + ]), + FunctionCase('split', Expression.field('email').split('@'), [ + 'dev.team', + 'example.com', + ]), + FunctionCase( + 'split with an expression', + Expression.field('sentence').split(Expression.constant(' and ')), + ['Dart is fun', 'Dart is fast'], + ), + // The field-name first argument is a field; the delimiter a literal. + FunctionCase('split static', PipelineFunctions.split('csv', ','), [ + 'dart', + 'firebase', + 'admin', + ]), + FunctionCase( + 'split static with expressions', + PipelineFunctions.split( + Expression.field('semicolons'), + Expression.field('delimiter'), + ), + ['a', 'b', 'c'], + ), + ]); + + // Comparing two fields of each book, so every book gives its own answer. + ctx.filterCases('string predicates across books', [ + FilterCase( + 'startsWith static with expressions', + PipelineFunctions.startsWith( + Expression.field('title'), + Expression.field('prefix'), + ), + ['Dart Pipelines', 'Firestore Admin'], + ), + FilterCase( + 'endsWith static with expressions', + PipelineFunctions.endsWith( + Expression.field('title'), + Expression.field('prefix'), + ), + ['Inactive Draft'], + ), + FilterCase( + 'stringContains static with expressions', + PipelineFunctions.stringContains( + Expression.field('greeting'), + Expression.field('prefix'), + ), + ['Inactive Draft'], + ), + FilterCase( + 'like with an expression', + Expression.field('greeting').like(Expression.constant('Hello%')), + ['Dart Pipelines', 'Firestore Admin'], + ), + FilterCase( + 'regexContains static', + PipelineFunctions.regexContains('email', r'@example\.(com|org)$'), + ['Dart Pipelines', 'Firestore Admin'], + ), + // Each book's `prefix` is used as its pattern. + FilterCase( + 'regexContains with an expression', + Expression.field('greeting').regexContains(Expression.field('prefix')), + ['Inactive Draft'], + ), + // `dev.team` holds a dot, so book 1's email cannot fully match. + FilterCase( + 'regexMatch with an expression', + Expression.field( + 'email', + ).regexMatch(Expression.constant(r'[a-z]+@example\.(org|net)')), + ['Firestore Admin', 'Inactive Draft'], + ), + ]); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/functions_timestamp_vector_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/functions_timestamp_vector_test.dart new file mode 100644 index 00000000..62404523 --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/functions_timestamp_vector_test.dart @@ -0,0 +1,624 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of the timestamp and vector Pipeline functions. +/// +/// Book 1's `createdAt` is 2023-11-14T22:13:20Z (`1700000000`), and its +/// `updatedAt` is exactly one day and 123456 microseconds later +/// (`Timestamp(1700086400, 123456000)`). In the time zones used below that +/// instant is: +/// +/// | Zone | Local time | UTC offset | +/// | --- | --- | --- | +/// | `UTC` | 2023-11-14 22:13:20 | +0 | +/// | `Asia/Tokyo` | 2023-11-15 07:13:20 | +9 | +/// | `MST` | 2023-11-14 15:13:20 | -7 | +/// | `America/Los_Angeles` | 2023-11-14 14:13:20 (PST) | -8 | +/// +/// Book 1's `embedding` is the vector `[1, 0, 0]`. +@Tags(['prod']) +library; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +import 'harness.dart'; + +Timestamp _ts(int seconds, [int nanoseconds = 0]) { + return Timestamp(seconds: seconds, nanoseconds: nanoseconds); +} + +/// Book 1's `createdAt`. +final _createdAt = DateTime.utc(2023, 11, 14, 22, 13, 20); + +/// 2023-11-14T00:00:00Z, book 1's `createdAt` truncated to the UTC day. +const _createdAtUtcDay = 1699920000; + +void main() { + pipelineE2E('Pipeline timestamp and vector functions', (ctx) { + ctx.functionCases('currentTimestamp', [ + FunctionCase( + 'is close to the client clock', + PipelineFunctions.currentTimestamp(), + isA().having( + (now) => now.toDate().difference(DateTime.now()).inMinutes.abs(), + 'minutes from the client clock', + inInclusiveRange(0, 9), + ), + ), + FunctionCase( + 'composes as an operand', + PipelineFunctions.timestampDiff( + PipelineFunctions.currentTimestamp(), + 'createdAt', + 'day', + ), + allOf( + isA(), + closeTo(DateTime.now().difference(_createdAt).inDays, 1), + ), + ), + ]); + + ctx.functionCases('timestamp to Unix conversions', [ + // The conversions truncate: updatedAt carries 123456000 nanoseconds. + FunctionCase( + 'static timestampToUnixSeconds with a field name', + PipelineFunctions.timestampToUnixSeconds('updatedAt'), + isInt(1700086400), + ), + FunctionCase( + 'static timestampToUnixSeconds with an expression', + PipelineFunctions.timestampToUnixSeconds( + Expression.constant(_ts(1741380235, 123456789)), + ), + isInt(1741380235), + ), + FunctionCase( + 'fluent timestampToUnixSeconds', + Expression.field('createdAt').timestampToUnixSeconds(), + isInt(1700000000), + ), + FunctionCase( + 'static timestampToUnixMillis with a field name', + PipelineFunctions.timestampToUnixMillis('updatedAt'), + isInt(1700086400123), + ), + FunctionCase( + 'static timestampToUnixMillis with an expression', + PipelineFunctions.timestampToUnixMillis( + Expression.constant(_ts(1741380235, 123456789)), + ), + isInt(1741380235123), + ), + FunctionCase( + 'fluent timestampToUnixMillis', + Expression.field('createdAt').timestampToUnixMillis(), + isInt(1700000000000), + ), + FunctionCase( + 'static timestampToUnixMicros with a field name', + PipelineFunctions.timestampToUnixMicros('updatedAt'), + isInt(1700086400123456), + ), + FunctionCase( + 'static timestampToUnixMicros with an expression', + PipelineFunctions.timestampToUnixMicros( + Expression.constant(_ts(1741380235, 123456789)), + ), + isInt(1741380235123456), + ), + FunctionCase( + 'fluent timestampToUnixMicros', + Expression.field('createdAt').timestampToUnixMicros(), + isInt(1700000000000000), + ), + ]); + + ctx.functionCases('Unix to timestamp conversions', [ + FunctionCase( + 'static unixSecondsToTimestamp with a number', + PipelineFunctions.unixSecondsToTimestamp(1700000000), + _ts(1700000000), + ), + FunctionCase( + 'static unixSecondsToTimestamp with an expression', + PipelineFunctions.unixSecondsToTimestamp( + Expression.field('unixSeconds'), + ), + _ts(1700000000), + ), + FunctionCase( + 'fluent unixSecondsToTimestamp', + Expression.field('unixSeconds').unixSecondsToTimestamp(), + _ts(1700000000), + ), + FunctionCase( + 'static unixMillisToTimestamp with a number', + PipelineFunctions.unixMillisToTimestamp(1700086400123), + _ts(1700086400, 123000000), + ), + // A String is a field name, not a constant. + FunctionCase( + 'static unixMillisToTimestamp with a field name', + PipelineFunctions.unixMillisToTimestamp('unixMillis'), + _ts(1700000000), + ), + FunctionCase( + 'static unixMillisToTimestamp with an expression', + PipelineFunctions.unixMillisToTimestamp(Expression.field('unixMillis')), + _ts(1700000000), + ), + FunctionCase( + 'fluent unixMillisToTimestamp', + Expression.constant(1700086400123).unixMillisToTimestamp(), + _ts(1700086400, 123000000), + ), + FunctionCase( + 'static unixMicrosToTimestamp with a number', + PipelineFunctions.unixMicrosToTimestamp(1700086400123456), + _ts(1700086400, 123456000), + ), + FunctionCase( + 'static unixMicrosToTimestamp with an expression', + PipelineFunctions.unixMicrosToTimestamp(Expression.field('unixMicros')), + _ts(1700000000), + ), + FunctionCase( + 'fluent unixMicrosToTimestamp', + Expression.field('unixMicros').unixMicrosToTimestamp(), + _ts(1700000000), + ), + FunctionCase( + 'micros round trip keeps the sub-second part', + Expression.field( + 'updatedAt', + ).timestampToUnixMicros().unixMicrosToTimestamp(), + _ts(1700086400, 123456000), + ), + ]); + + ctx.functionCases('timestampAdd', [ + FunctionCase( + 'static with plain values', + PipelineFunctions.timestampAdd('createdAt', 'day', 1), + _ts(1700086400), + ), + // rating is 5 on book 1. + FunctionCase( + 'static with expressions', + PipelineFunctions.timestampAdd( + Expression.field('createdAt'), + Expression.constant('hour'), + Expression.field('rating'), + ), + _ts(1700018000), + ), + FunctionCase( + 'fluent with plain values', + Expression.field('createdAt').timestampAdd('second', 60), + _ts(1700000060), + ), + FunctionCase( + 'fluent with expressions', + Expression.field( + 'createdAt', + ).timestampAdd(Expression.constant('minute'), Expression.constant(30)), + _ts(1700001800), + ), + FunctionCase( + 'microsecond keeps nanosecond precision', + Expression.field('updatedAt').timestampAdd('microsecond', 10), + _ts(1700086400, 123466000), + ), + FunctionCase( + 'millisecond carries into seconds', + Expression.field('updatedAt').timestampAdd('millisecond', 900), + _ts(1700086401, 23456000), + ), + ]); + + ctx.functionCases('timestampSubtract', [ + FunctionCase( + 'static with plain values', + PipelineFunctions.timestampSubtract('updatedAt', 'day', 1), + _ts(1700000000, 123456000), + ), + // price is 10 on book 1. + FunctionCase( + 'static with expressions', + PipelineFunctions.timestampSubtract( + Expression.field('updatedAt'), + Expression.constant('millisecond'), + Expression.field('price'), + ), + _ts(1700086400, 113456000), + ), + FunctionCase( + 'fluent with plain values', + Expression.field('createdAt').timestampSubtract('second', 60), + _ts(1699999940), + ), + FunctionCase( + 'fluent with expressions borrows from seconds', + Expression.field('createdAt').timestampSubtract( + Expression.constant('microsecond'), + Expression.constant(1), + ), + _ts(1699999999, 999999000), + ), + ]); + + // updatedAt - createdAt is exactly 86400.123456 seconds, so every unit + // below gives the same whole number however the backend rounds. + ctx.functionCases('timestampDiff', [ + FunctionCase( + 'static with field names', + PipelineFunctions.timestampDiff( + 'updatedAt', + 'createdAt', + 'microsecond', + ), + isInt(86400123456), + ), + FunctionCase( + 'static with field names, end before start', + PipelineFunctions.timestampDiff( + 'createdAt', + 'updatedAt', + 'microsecond', + ), + isInt(-86400123456), + ), + FunctionCase( + 'static in hours', + PipelineFunctions.timestampDiff('updatedAt', 'createdAt', 'hour'), + isInt(24), + ), + FunctionCase( + 'static with expressions', + PipelineFunctions.timestampDiff( + Expression.field('updatedAt'), + Expression.field('createdAt'), + Expression.constant('day'), + ), + isInt(1), + ), + FunctionCase( + 'static with a computed start', + PipelineFunctions.timestampDiff( + Expression.field('createdAt'), + PipelineFunctions.unixSecondsToTimestamp(1699999940), + 'second', + ), + isInt(60), + ), + FunctionCase( + 'fluent with plain values', + Expression.field('updatedAt').timestampDiff('createdAt', 'millisecond'), + isInt(86400123), + ), + FunctionCase( + 'fluent with expressions', + Expression.field('updatedAt').timestampDiff( + Expression.field('createdAt'), + Expression.constant('minute'), + ), + isInt(1440), + ), + ]); + + ctx.functionCases('timestampTruncate', [ + FunctionCase( + 'static with plain values, timezone omitted', + PipelineFunctions.timestampTruncate('createdAt', 'day'), + _ts(_createdAtUtcDay), + ), + FunctionCase( + 'static to the month', + PipelineFunctions.timestampTruncate('createdAt', 'month'), + _ts(1698796800), + ), + FunctionCase( + 'static to the year', + PipelineFunctions.timestampTruncate('createdAt', 'year'), + _ts(1672531200), + ), + // Local midnight on 2023-11-15 in Tokyo is 2023-11-14T15:00:00Z. + FunctionCase( + 'static with expressions and a plain timezone', + PipelineFunctions.timestampTruncate( + Expression.field('createdAt'), + Expression.constant('day'), + 'Asia/Tokyo', + ), + _ts(_createdAtUtcDay + 15 * 3600), + ), + // Local midnight on 2023-11-14 in MST is 2023-11-14T07:00:00Z. + FunctionCase( + 'static with a timezone expression', + PipelineFunctions.timestampTruncate( + 'createdAt', + 'day', + Expression.constant('MST'), + ), + _ts(_createdAtUtcDay + 7 * 3600), + ), + FunctionCase( + 'fluent with a plain timezone', + Expression.field('createdAt').timestampTruncate('day', 'UTC'), + _ts(_createdAtUtcDay), + ), + FunctionCase( + 'fluent to the second drops the sub-second part', + Expression.field('updatedAt').timestampTruncate('second'), + _ts(1700086400), + ), + FunctionCase( + 'fluent to the millisecond', + Expression.field('updatedAt').timestampTruncate('millisecond'), + _ts(1700086400, 123000000), + ), + FunctionCase( + 'fluent with a granularity expression, timezone omitted', + Expression.field( + 'createdAt', + ).timestampTruncate(Expression.constant('hour')), + _ts(1699999200), + ), + // Local midnight on 2023-11-14 in Los Angeles (PST) is 08:00Z. + FunctionCase( + 'fluent with expressions', + Expression.field('createdAt').timestampTruncate( + Expression.constant('day'), + Expression.constant('America/Los_Angeles'), + ), + _ts(_createdAtUtcDay + 8 * 3600), + ), + ]); + + ctx.functionCases('timestampExtract', [ + FunctionCase( + 'static with plain values, timezone omitted', + PipelineFunctions.timestampExtract('createdAt', 'year'), + isInt(2023), + ), + FunctionCase( + 'static month', + PipelineFunctions.timestampExtract('createdAt', 'month'), + isInt(11), + ), + FunctionCase( + 'static day defaults to UTC', + PipelineFunctions.timestampExtract('createdAt', 'day'), + isInt(14), + ), + FunctionCase( + 'static with expressions and a plain timezone', + PipelineFunctions.timestampExtract( + Expression.field('createdAt'), + Expression.constant('day'), + 'Asia/Tokyo', + ), + isInt(15), + ), + FunctionCase( + 'static with a timezone expression', + PipelineFunctions.timestampExtract( + 'createdAt', + 'hour', + Expression.constant('MST'), + ), + isInt(15), + ), + FunctionCase( + 'fluent with a plain timezone', + Expression.field('createdAt').timestampExtract('year', 'UTC'), + isInt(2023), + ), + FunctionCase( + 'fluent with timezone omitted', + Expression.field('createdAt').timestampExtract('hour'), + isInt(22), + ), + FunctionCase( + 'fluent minute', + Expression.field('createdAt').timestampExtract('minute'), + isInt(13), + ), + FunctionCase( + 'fluent with expressions', + Expression.field('createdAt').timestampExtract( + Expression.constant('hour'), + Expression.constant('America/Los_Angeles'), + ), + isInt(14), + ), + ]); + + // Vector positions take a VectorValue, a plain list of numbers (which must + // reach the backend as a vector, not an array) or an expression. The + // [0.1, 0.1] / [0.5, 0.8] values are the Node SDK system test's. + ctx.functionCases('cosineDistance', [ + FunctionCase( + 'static with a field name and a list', + PipelineFunctions.cosineDistance('embedding', [0, 1, 0]), + isDouble(1), + ), + FunctionCase( + 'static with expressions', + PipelineFunctions.cosineDistance( + Expression.field('embedding'), + Expression.vector([1, 1, 0]), + ), + isDouble(0.29289321881345254), + ), + FunctionCase( + 'static with a field expression on the right', + PipelineFunctions.cosineDistance( + Expression.vector([1, 1, 0]), + Expression.field('embedding'), + ), + isDouble(0.29289321881345254), + ), + FunctionCase( + 'static with a VectorValue', + PipelineFunctions.cosineDistance( + Expression.constant(FieldValue.vector([0.1, 0.1])), + FieldValue.vector([0.5, 0.8]), + ), + isDouble(0.02560880430538015), + ), + FunctionCase( + 'fluent with a vector expression', + Expression.field( + 'embedding', + ).cosineDistance(Expression.vector([1, 0, 0])), + isDouble(0), + ), + FunctionCase( + 'fluent with a list', + Expression.field('embedding').cosineDistance(const [1.0, 1.0, 0.0]), + isDouble(0.29289321881345254), + ), + FunctionCase( + 'fluent with a VectorValue', + Expression.constant( + FieldValue.vector([0.1, 0.1]), + ).cosineDistance(FieldValue.vector([0.5, 0.8])), + isDouble(0.02560880430538015), + ), + ]); + + ctx.functionCases('dotProduct', [ + FunctionCase( + 'static with a field name and a list', + PipelineFunctions.dotProduct('embedding', const [2.5, 4, 5]), + isDouble(2.5), + ), + FunctionCase( + 'static with expressions', + PipelineFunctions.dotProduct( + Expression.field('embedding'), + Expression.vector([2.5, 4, 5]), + ), + isDouble(2.5), + ), + FunctionCase( + 'static with a VectorValue', + PipelineFunctions.dotProduct( + Expression.constant(FieldValue.vector([0.1, 0.1])), + FieldValue.vector([0.5, 0.8]), + ), + isDouble(0.13), + ), + FunctionCase( + 'fluent with a vector expression', + Expression.field('embedding').dotProduct(Expression.vector([1, 0, 0])), + isDouble(1), + ), + FunctionCase( + 'fluent with a field expression', + Expression.vector([ + 2.5, + 4, + 5, + ]).dotProduct(Expression.field('embedding')), + isDouble(2.5), + ), + FunctionCase( + 'fluent with a list', + Expression.field('embedding').dotProduct([-1.5, 4, 5]), + isDouble(-1.5), + ), + FunctionCase( + 'fluent with a VectorValue', + Expression.constant( + FieldValue.vector([0.1, 0.1]), + ).dotProduct(FieldValue.vector([0.5, 0.8])), + isDouble(0.13), + ), + ]); + + ctx.functionCases('euclideanDistance', [ + FunctionCase( + 'static with a field name and a list', + PipelineFunctions.euclideanDistance('embedding', [4, 4, 0]), + isDouble(5), + ), + FunctionCase( + 'static with expressions', + PipelineFunctions.euclideanDistance( + Expression.field('embedding'), + Expression.vector([0, 1, 0]), + ), + isDouble(1.4142135623730951), + ), + FunctionCase( + 'static with a VectorValue', + PipelineFunctions.euclideanDistance( + Expression.constant(FieldValue.vector([0.1, 0.1])), + FieldValue.vector([0.5, 0.8]), + ), + isDouble(0.806225774829855), + ), + FunctionCase( + 'fluent with a vector expression', + Expression.field( + 'embedding', + ).euclideanDistance(Expression.vector([1, 0, 0])), + isDouble(0), + ), + FunctionCase( + 'fluent with a field expression', + Expression.vector([ + 4, + 4, + 0, + ]).euclideanDistance(Expression.field('embedding')), + isDouble(5), + ), + FunctionCase( + 'fluent with a list', + Expression.field('embedding').euclideanDistance(const [1.0, 2.0, 2.0]), + isDouble(2.8284271247461903), + ), + FunctionCase( + 'fluent with a VectorValue', + Expression.field( + 'embedding', + ).euclideanDistance(FieldValue.vector([1, 3, 4])), + isDouble(5), + ), + ]); + + ctx.functionCases('vectorLength', [ + FunctionCase( + 'static with a field name', + PipelineFunctions.vectorLength('embedding'), + isInt(3), + ), + FunctionCase( + 'static with an expression', + PipelineFunctions.vectorLength(Expression.vector([1, 2, 3, 4])), + isInt(4), + ), + FunctionCase( + 'fluent', + Expression.field('embedding').vectorLength(), + isInt(3), + ), + ]); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/functions_type_debug_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/functions_type_debug_test.dart new file mode 100644 index 00000000..15e31bfc --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/functions_type_debug_test.dart @@ -0,0 +1,321 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of the type, debugging and search Pipeline functions, every +/// `PipelineValueType`, and the expression primitives (`field`, `constant`, +/// raw functions). +@Tags(['prod']) +library; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +import 'harness.dart'; + +/// A book 1 field holding a value of [type], or `null` when the Admin SDK +/// cannot write one, and a field holding a value of another type. +/// +/// Exhaustive on purpose: a new [PipelineValueType] fails to compile here +/// until it gets a case. +(String?, String) _isTypeFields(PipelineValueType type) { + return switch (type) { + PipelineValueType.nullValue => ('nullable', 'title'), + PipelineValueType.boolean => ('active', 'price'), + PipelineValueType.number => ('score', 'title'), + PipelineValueType.int32 => (null, 'price'), + PipelineValueType.int64 => ('price', 'score'), + PipelineValueType.double => ('score', 'price'), + PipelineValueType.decimal128 => (null, 'score'), + PipelineValueType.timestamp => ('createdAt', 'unixSeconds'), + PipelineValueType.string => ('title', 'bytes'), + PipelineValueType.bytes => ('bytes', 'title'), + PipelineValueType.reference => ('pathRef', 'title'), + PipelineValueType.geoPoint => ('location', 'metadata'), + PipelineValueType.array => ('tags', 'metadata'), + PipelineValueType.map => ('metadata', 'tags'), + PipelineValueType.vector => ('embedding', 'numbers'), + PipelineValueType.maxKey => (null, 'title'), + PipelineValueType.minKey => (null, 'title'), + PipelineValueType.objectId => (null, 'title'), + PipelineValueType.regex => (null, 'title'), + }; +} + +/// The backend type name of each book 1 field, as `type` returns it. +const _typeNames = { + 'nullable': 'null', + 'active': 'boolean', + 'price': 'int64', + 'score': 'float64', + 'createdAt': 'timestamp', + 'title': 'string', + 'bytes': 'bytes', + 'pathRef': 'reference', + 'location': 'geo_point', + 'tags': 'array', + 'metadata': 'map', + 'embedding': 'vector', +}; + +void main() { + pipelineE2E('Pipeline type and debugging functions', (ctx) { + // Integer division by zero is an error. + final divisionByZero = Expression.constant(1).divide(0); + + ctx.functionCases('expression primitives', [ + FunctionCase('field', field('title'), 'Dart Pipelines'), + FunctionCase( + 'field with a nested path', + field('nested.level1.level2.value'), + isInt(42), + ), + // A FieldPath segment is one field name, even when it holds a dot. + FunctionCase( + 'field with a FieldPath', + field(FieldPath(const ['nested', 'dotted.key'])), + 'dot', + ), + FunctionCase( + 'field with a FieldPath to a name that is not an identifier', + field(FieldPath(const ['first-name'])), + 'Ada', + ), + FunctionCase( + 'field with a name that is not an identifier', + field('last name'), + 'Lovelace', + ), + FunctionCase( + 'Expression.field with a FieldPath', + Expression.field(FieldPath(const ['metadata', 'lang'])), + 'dart', + ), + FunctionCase('constant', constant('literal'), 'literal'), + ]); + + test('PipelineField.path names the selected field', () async { + final title = field('title'); + final snapshot = await ctx.book1Pipeline().select([title]).execute(); + expect(title.path, 'title'); + expect(snapshot.results.single.get(title.path), 'Dart Pipelines'); + }); + + ctx.functionCases('raw functions', [ + FunctionCase( + 'pipelineFunction', + pipelineFunction('add', [field('price'), 1]), + isInt(11), + ), + FunctionCase( + 'Expression.raw', + Expression.raw('multiply', [field('rating'), 3]), + isInt(15), + ), + FunctionCase( + 'PipelineFunctions.raw', + PipelineFunctions.raw('string_concat', [field('prefix'), '!']), + 'Dart!', + ), + FunctionCase( + 'asBoolean', + pipelineFunction('equal', [field('rating'), 5]).asBoolean().not(), + false, + ), + ]); + + ctx.filterCases('raw boolean functions', [ + FilterCase( + 'PipelineFunctions.raw as a condition', + PipelineFunctions.raw('greater_than', [field('price'), 15]).asBoolean(), + ['Firestore Admin', 'Inactive Draft'], + ), + ]); + + ctx.functionCases('debugging functions', [ + FunctionCase('exists', field('title').exists(), true), + FunctionCase( + 'exists on an absent field', + field('missing').exists(), + false, + ), + FunctionCase('exists on a null field', field('nullable').exists(), true), + FunctionCase( + 'static exists on an expression', + PipelineFunctions.exists(field('missing')), + false, + ), + FunctionCase('isAbsent', field('missing').isAbsent(), true), + FunctionCase( + 'isAbsent on a null field', + field('nullable').isAbsent(), + false, + ), + FunctionCase( + 'static isAbsent on an expression', + PipelineFunctions.isAbsent(field('title')), + false, + ), + FunctionCase( + 'ifAbsent', + field('missing').ifAbsent('fallback'), + 'fallback', + ), + FunctionCase( + 'ifAbsent on a present field', + field('title').ifAbsent('fallback'), + 'Dart Pipelines', + ), + FunctionCase( + 'ifAbsent on a null field', + field('nullable').ifAbsent('fallback'), + isNull, + ), + FunctionCase( + 'ifAbsent with an expression', + field('missing').ifAbsent(field('title')), + 'Dart Pipelines', + ), + // Only book 1 has `sparse`; a literal 'sparse' would come back as is. + FunctionCase( + 'static ifAbsent on a field name', + PipelineFunctions.ifAbsent('sparse', 'none'), + 'book 1 only', + ), + FunctionCase( + 'static ifAbsent on expressions', + PipelineFunctions.ifAbsent(field('missing'), field('prefix')), + 'Dart', + ), + FunctionCase('isError', divisionByZero.isError(), true), + FunctionCase('isError without an error', field('title').isError(), false), + FunctionCase( + 'static isError on an expression', + PipelineFunctions.isError(divisionByZero), + true, + ), + FunctionCase( + 'static isError on a plain value', + PipelineFunctions.isError(0), + false, + ), + FunctionCase('ifError', divisionByZero.ifError('was error'), 'was error'), + FunctionCase( + 'ifError with an expression', + divisionByZero.ifError(field('title')), + 'Dart Pipelines', + ), + FunctionCase( + 'ifError without an error', + field('title').ifError('caught'), + 'Dart Pipelines', + ), + FunctionCase( + 'ifError on a boolean expression', + divisionByZero.greaterThan(1).ifError(constant(true)).asBoolean().not(), + false, + ), + FunctionCase( + 'static ifError on expressions', + PipelineFunctions.ifError(divisionByZero, constant('was error')), + 'was error', + ), + FunctionCase( + 'static ifError on plain values', + PipelineFunctions.ifError(42, 'caught'), + isInt(42), + ), + ]); + + // A field-name String is a field reference: `exists('sparse')` holds for + // book 1 only, where a literal 'sparse' would hold for every book. + ctx.filterCases('debugging functions on field names', [ + FilterCase('static exists', PipelineFunctions.exists('sparse'), [ + 'Dart Pipelines', + ]), + FilterCase('static isAbsent', PipelineFunctions.isAbsent('sparse'), [ + 'Firestore Admin', + 'Inactive Draft', + ]), + ]); + + ctx.functionCases('type', [ + for (final MapEntry(key: fieldName, value: typeName) + in _typeNames.entries) + FunctionCase('type of $fieldName', field(fieldName).type(), typeName), + // A literal 'price' would be a string. + FunctionCase( + 'static type on a field name', + PipelineFunctions.type('price'), + 'int64', + ), + FunctionCase( + 'static type on an expression', + PipelineFunctions.type(field('location')), + 'geo_point', + ), + ]); + + // Each value is checked against a field of its type, where one can be + // written, and against a field of another type. The negative case alone + // still proves the backend accepts the type name. + ctx.functionCases('isType', [ + for (final type in PipelineValueType.values) + if (_isTypeFields(type) case (final matching, final other)) ...[ + if (matching != null) + FunctionCase( + '${type.name} on $matching', + field(matching).isType(type), + true, + ), + FunctionCase( + '${type.name} on $other', + field(other).isType(type), + false, + ), + ], + FunctionCase( + 'number on an integer', + field('price').isType(PipelineValueType.number), + true, + ), + FunctionCase( + 'decimal128 on an integer', + field('price').isType(PipelineValueType.decimal128), + false, + ), + FunctionCase('a type name String', field('price').isType('int64'), true), + FunctionCase( + 'a type name expression', + field('price').isType(constant('int64')), + true, + ), + // A literal 'price' would be a string. + FunctionCase( + 'static isType on a field name', + PipelineFunctions.isType('price', PipelineValueType.int64), + true, + ), + FunctionCase( + 'static isType on expressions', + PipelineFunctions.isType(field('score'), constant('float64')), + true, + ), + FunctionCase( + 'static isType with a type name String', + PipelineFunctions.isType('score', 'int64'), + false, + ), + ]); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/harness.dart b/packages/google_cloud_firestore/test/e2e/pipeline/harness.dart new file mode 100644 index 00000000..2b0ca3f2 --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/harness.dart @@ -0,0 +1,590 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Shared harness for the live Firestore Pipelines E2E suite. +/// +/// Every `*_test.dart` file in this directory: +/// +/// - is tagged `@Tags(['prod'])`; +/// - imports only `package:google_cloud_firestore/google_cloud_firestore.dart`, +/// `package:test/test.dart` and this harness (plus `dart:` libraries), so a +/// public API that is missing from the barrel fails to compile here; +/// - wraps its tests in a single [pipelineE2E] call, which seeds the +/// documents described in [buildSeed] once per file and deletes them +/// afterwards. +/// +/// Prefer the table-driven helpers ([PipelineE2EContext.functionCases], +/// [PipelineE2EContext.aggregateCases], [PipelineE2EContext.filterCases]): +/// each case is its own test, so one `INVALID_ARGUMENT` never hides another. +/// +/// `test/pipeline_e2e_coverage_test.dart` statically checks that these files +/// exercise every public Pipeline API member, every optional parameter (both +/// supplied and omitted) and every value position (with an expression and +/// with a plain value). Adding a public Pipeline function without an E2E case +/// here makes that guard fail. +library; + +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +/// Environment variable naming the project that holds the Pipelines database. +const projectIdEnv = 'FIRESTORE_PIPELINE_E2E_PROJECT_ID'; + +/// Environment variable naming the Enterprise-edition database to run against. +const databaseIdEnv = 'FIRESTORE_PIPELINE_E2E_DATABASE_ID'; + +/// The collection every E2E file seeds into. +/// +/// Kept stable on purpose: the vector index the `findNearest` tests need is +/// declared on this collection group (see `test/e2e/README.md`), and per-run +/// document IDs keep concurrent runs apart. +const pipelineE2ECollection = 'pipeline_e2e_books'; + +/// Why the suite is skipped when it is not configured. +const pipelineE2ESkipReason = + 'Set both FIRESTORE_PIPELINE_E2E_PROJECT_ID and ' + 'FIRESTORE_PIPELINE_E2E_DATABASE_ID to run against a real Firebase ' + 'project. The database must be Enterprise edition; Pipelines are not ' + 'available on Standard. CI authenticates with Application Default ' + 'Credentials, for example via Workload Identity Federation.'; + +/// The alias [PipelineE2EContext.functionCases] and +/// [PipelineE2EContext.aggregateCases] store each evaluated expression under. +const valueAlias = 'v'; + +/// Declares a group of live Pipeline tests sharing one seeded data set. +/// +/// The group is skipped unless both [projectIdEnv] and [databaseIdEnv] are +/// set. Otherwise the documents described in [buildSeed] are written once in +/// `setUpAll`, under a run ID unique to this call, and deleted in +/// `tearDownAll`. +/// +/// [body] runs at declaration time, like any group body. Everything on the +/// context ([PipelineE2EContext.firestore], [PipelineE2EContext.runId], the +/// document references and the Pipeline builders) is already usable there, +/// so cases may embed `ctx.book2Ref` and similar; only executing a Pipeline +/// has to wait for a test body. +void pipelineE2E( + String description, + void Function(PipelineE2EContext ctx) body, +) { + final target = _E2ETarget.fromEnvironment(); + final ctx = PipelineE2EContext._(target); + + group(description, skip: target.enabled ? null : pipelineE2ESkipReason, () { + setUpAll(ctx._seed); + tearDownAll(ctx._deleteSeed); + body(ctx); + }); +} + +final class _E2ETarget { + const _E2ETarget(this.projectId, this.databaseId); + + factory _E2ETarget.fromEnvironment() { + final projectId = + Platform.environment[projectIdEnv] ?? + Platform.environment['GOOGLE_CLOUD_PROJECT'] ?? + Platform.environment['GCLOUD_PROJECT']; + // No '(default)' fallback on purpose. CI credential helpers such as + // google-github-actions/auth export GOOGLE_CLOUD_PROJECT, so a project-only + // guard would silently arm this suite against whatever project happened to + // be authenticated, using its default database — which may be Standard + // edition (no Pipelines at all) or simply lack the vector index this suite + // needs. Both variables must be set deliberately. + final databaseId = Platform.environment[databaseIdEnv]; + return _E2ETarget(projectId, databaseId); + } + + final String? projectId; + final String? databaseId; + + bool get enabled => + projectId != null && + projectId!.isNotEmpty && + databaseId != null && + databaseId!.isNotEmpty; +} + +var _lastRunMicros = 0; + +/// A run ID unique within this isolate, in the `run_` format the +/// README's cleanup advice refers to. +String _nextRunId() { + var micros = DateTime.now().microsecondsSinceEpoch; + if (micros <= _lastRunMicros) micros = _lastRunMicros + 1; + _lastRunMicros = micros; + return 'run_$micros'; +} + +/// What a [pipelineE2E] body builds its tests from. +final class PipelineE2EContext { + PipelineE2EContext._(this._target) : runId = _nextRunId(); + + final _E2ETarget _target; + + /// Identifies this file's seeded documents; every seeded document carries + /// it in its `runId` field, and their IDs start with it. + final String runId; + + /// The Firestore instance under test. + /// + /// Creating it does no I/O. When the suite is skipped it points at a + /// placeholder project, so declaration-time code can still build document + /// references. + late final Firestore firestore = Firestore( + settings: _target.enabled + ? Settings(projectId: _target.projectId, databaseId: _target.databaseId) + : const Settings( + projectId: 'pipeline-e2e-skipped', + databaseId: 'pipeline-e2e-skipped', + ), + ); + + /// The seeded collection, [pipelineE2ECollection]. + String get collectionPath => pipelineE2ECollection; + + /// The seeded documents: `[book1Ref, book2Ref, book3Ref]`. + late final List> refs = [ + for (var i = 1; i <= 3; i++) + firestore.doc('$pipelineE2ECollection/${runId}_book_$i'), + ]; + + /// Book 1, `'Dart Pipelines'`. Function cases are evaluated against it. + DocumentReference get book1Ref => refs[0]; + + /// Book 2, `'Firestore Admin'`. + DocumentReference get book2Ref => refs[1]; + + /// Book 3, `'Inactive Draft'`. + DocumentReference get book3Ref => refs[2]; + + /// The exact data written for each of [refs], as built by [buildSeed]. + late final List seed = buildSeed(runId, refs); + + /// Restricts a Pipeline to this run's documents, optionally also requiring + /// [condition]. + /// + /// Without [condition] this is a bare equality rather than a one-operand + /// `and`, a shape the Node SDK's `and(first, second, ...more)` never sends. + PipelineBooleanExpression runFilter([PipelineBooleanExpression? condition]) { + final inRun = Expression.field('runId').equal(runId); + return condition == null + ? inRun + : PipelineFunctions.and([inRun, condition]); + } + + /// A Pipeline over this run's three seeded documents. + Pipeline runPipeline() { + return firestore.pipeline().collection(collectionPath).where(runFilter()); + } + + /// A Pipeline over book 1 only. + Pipeline book1Pipeline() { + return firestore + .pipeline() + .collection(collectionPath) + .where(runFilter(Expression.field('title').equal('Dart Pipelines'))); + } + + /// Evaluates [expression] against book 1 and returns the result. + /// + /// Selects `expression.as(valueAlias)` from [book1Pipeline] and reads it + /// back, failing unless exactly one result comes back. + Future evaluate(PipelineExpression expression) async { + final snapshot = await book1Pipeline() + .select([expression.as(valueAlias)]) + .limit(1) + .execute(); + expect(snapshot.results, hasLength(1)); + return snapshot.results.single.get(valueAlias); + } + + /// Aggregates [accumulator] over this run's three documents and returns the + /// result. + Future evaluateAggregate(PipelineExpression accumulator) async { + final snapshot = await runPipeline().aggregate([ + accumulator.as(valueAlias), + ]).execute(); + expect(snapshot.results, hasLength(1)); + return snapshot.results.single.get(valueAlias); + } + + /// Returns the titles [pipeline] produces, in order. + Future> titlesOf(Pipeline pipeline) async { + final snapshot = await pipeline.execute(); + return [for (final result in snapshot.results) result.get('title')]; + } + + /// Declares one test per case, evaluating each expression on book 1. + /// + /// See [FunctionCase] for how [FunctionCase.expected] is compared. + void functionCases(String description, List cases) { + group(description, () { + for (final functionCase in cases) { + test(functionCase.name, () async { + expect( + await evaluate(functionCase.expression), + functionCase.expected, + reason: '$description: ${functionCase.name}', + ); + }); + } + }); + } + + /// Declares one test per case, aggregating each accumulator over the run's + /// three documents. + void aggregateCases(String description, List cases) { + group(description, () { + for (final aggregateCase in cases) { + test(aggregateCase.name, () async { + expect( + await evaluateAggregate(aggregateCase.accumulator), + aggregateCase.expected, + reason: '$description: ${aggregateCase.name}', + ); + }); + } + }); + } + + /// Declares one test per case, filtering the run's documents with each + /// condition and comparing the matching titles, sorted by ascending price + /// (so always in book order). + void filterCases(String description, List cases) { + group(description, () { + for (final filterCase in cases) { + test(filterCase.name, () async { + expect( + await titlesOf( + firestore + .pipeline() + .collection(collectionPath) + .where(runFilter(filterCase.condition)) + .sort([Expression.field('price').ascending()]) + .select([Expression.field('title')]), + ), + filterCase.expectedTitles, + reason: '$description: ${filterCase.name}', + ); + }); + } + }); + } + + Future _seed() async { + await Future.wait([ + for (var i = 0; i < refs.length; i++) refs[i].set(seed[i]), + ]); + } + + Future _deleteSeed() async { + try { + await Future.wait([for (final ref in refs) ref.delete()]); + } finally { + await firestore.terminate(); + } + } +} + +/// An expression evaluated on book 1 by [PipelineE2EContext.functionCases]. +final class FunctionCase { + /// Creates a case named [name]. + const FunctionCase(this.name, this.expression, this.expected); + + /// The test name. + final String name; + + /// The expression under test. Do not alias it; the harness does. + final PipelineExpression expression; + + /// The expected value: a [Matcher], or a value compared with `equals`. + /// + /// `equals` treats `13` and `13.0` as equal; use [isInt] or [isDouble] + /// when the numeric type is part of what is being tested. + final Object? expected; +} + +/// An accumulator evaluated over the run's three documents by +/// [PipelineE2EContext.aggregateCases]. +final class AggregateCase { + /// Creates a case named [name]. + const AggregateCase(this.name, this.accumulator, this.expected); + + /// The test name. + final String name; + + /// The aggregate expression under test. Do not alias it; the harness does. + final PipelineExpression accumulator; + + /// The expected value: a [Matcher], or a value compared with `equals`. + final Object? expected; +} + +/// A condition applied to the run's three documents by +/// [PipelineE2EContext.filterCases]. +final class FilterCase { + /// Creates a case named [name]. + const FilterCase(this.name, this.condition, this.expectedTitles); + + /// The test name. + final String name; + + /// The `where` condition under test; the run filter is added for you. + final PipelineBooleanExpression condition; + + /// The titles of the matching books, in book order: any subset of + /// `'Dart Pipelines'`, `'Firestore Admin'`, `'Inactive Draft'`. + final List expectedTitles; +} + +/// Matches an [int] equal to [value]. +/// +/// Results decode by wire type: an `integerValue` becomes an [int] and a +/// `doubleValue` a [double], so the backend's choice of result type is +/// observable here (unlike in the Node SDK, where both are a JS `number`). +/// For example `add` on two int64 fields returns int64, while `average` returns +/// float64. Use [isInt] / [isDouble] when the result type is part of what a +/// case checks, and [isNumber], which accepts either, when it is not. +Matcher isInt(int value) => allOf(isA(), equals(value)); + +/// Matches a [double] within [tolerance] of [value]. See [isInt]. +Matcher isDouble(num value, {double tolerance = 1e-9}) { + return allOf(isA(), closeTo(value, tolerance)); +} + +/// Matches an [int] or a [double] within [tolerance] of [value]. See [isInt]. +Matcher isNumber(num value, {double tolerance = 1e-9}) { + return allOf(isA(), closeTo(value, tolerance)); +} + +/// Builds the data written for each of [refs] (books 1, 2 and 3). +/// +/// This is the single source of truth for expectations: compute expected +/// values from the table below, never by guessing. Every field exists on all +/// three books unless noted. No document has a field named `missing`, so use +/// it for absent-field cases. +/// +/// | Field | Book 1 | Book 2 | Book 3 | +/// | --- | --- | --- | --- | +/// | `runId` | [runId] | [runId] | [runId] | +/// | `title` | `'Dart Pipelines'` (14 chars) | `'Firestore Admin'` | `'Inactive Draft'` | +/// | `active` | `true` | `true` | `false` | +/// | `archived` | `false` | `false` | `true` | +/// | `price` (int) | `10` | `20` | `30` | +/// | `rating` (int) | `5` | `4` | `2` | +/// | `discount` (int) | `2` | `3` | `4` | +/// | `flags` (int, bit flags) | `6` | `3` | `5` | +/// | `quantity` (int) | `7` | `0` | `-3` | +/// | `zero` (int) | `0` | `0` | `0` | +/// | `largeInt` (int, 2^53 + 1) | `9007199254740993` | same | same | +/// | `score` (double) | `-12.7` | `3.2` | `8.9` | +/// | `ratio` (double) | `0.5` | `1.25` | `2.0` | +/// | `half` (double) | `2.5` | `2.5` | `2.5` | +/// | `negativeHalf` (double) | `-2.5` | `-2.5` | `-2.5` | +/// | `precise` (double) | `4.123456` | same | same | +/// | `nullable` | `null` | `null` | `null` | +/// | `bytes` | `[0x0f, 0xf0]` | `[0xaa, 0x55]` | `[0xff, 0x00]` | +/// | `spaced` | `' Dart '` | `' Admin '` | `' Draft '` | +/// | `padded` | `'__Dart__'` | same | same | +/// | `empty` | `''` | `''` | `''` | +/// | `greeting` | `'Hello, World!'` | `'Hello, Admin!'` | `'Goodbye, Draft!'` | +/// | `prefix` | `'Dart'` | `'Fire'` | `'Draft'` | +/// | `unicode` | `'café ☕ 日本'` (9 code points, 16 UTF-8 bytes) | same | same | +/// | `email` | `'dev.team@example.com'` | `'admin@example.org'` | `'draft@example.net'` | +/// | `sentence` | `'Dart is fun and Dart is fast'` | same | same | +/// | `csv` | `'dart,firebase,admin'` | `'firestore,admin'` | `'inactive,draft'` | +/// | `semicolons` | `'a;b;c'` | same | same | +/// | `delimiter` | `';'` | same | same | +/// | `tags` | `['dart', 'firebase']` | `['firebase']` | `['draft']` | +/// | `words` | `['dart', 'firebase']` | `['firebase']` | `['draft']` | +/// | `numbers` (ints) | `[3, 1, 2, 3]` | `[4, 5]` | `[9]` | +/// | `scores` (doubles) | `[1.5, 2.5, -0.5]` | `[4.0]` | `[0.25, 0.75]` | +/// | `mixedNumbers` | `[1, 2.5, -3, 0]` | same | same | +/// | `booleans` | `[true, false, true]` | same | same | +/// | `withNulls` | `[1, null, 3]` | same | same | +/// | `emptyArray` | `[]` | `[]` | `[]` | +/// | `items` (maps) | `[{name: 'pen', qty: 2, price: 1.5}, {name: 'ink', qty: 1, price: 4.0}]` | `[{name: 'cap', qty: 5, price: 0.5}]` | `[]` | +/// | `metadata` | `{lang: 'dart', category: 'sdk'}` | `{lang: 'dart', category: 'admin'}` | `{lang: 'dart', category: 'draft'}` | +/// | `nested` | `{level1: {level2: {value: 42}}, 'dotted.key': 'dot', list: [1, 2]}` | same | same | +/// | `stats` | `{views: 100, likes: 7}` | `{views: 50, likes: 3}` | `{views: 0, likes: 0}` | +/// | `emptyMap` | `{}` | `{}` | `{}` | +/// | `createdAt` | `Timestamp(1700000000, 0)` (2023-11-14T22:13:20Z) | `Timestamp(1700003600, 0)` | `Timestamp(1700007200, 0)` | +/// | `updatedAt` (createdAt + 1 day + 0.123456 s) | `Timestamp(1700086400, 123456000)` | `Timestamp(1700090000, 123456000)` | `Timestamp(1700093600, 123456000)` | +/// | `unixSeconds` (= createdAt) | `1700000000` | `1700003600` | `1700007200` | +/// | `unixMillis` (= createdAt) | `1700000000000` | `1700003600000` | `1700007200000` | +/// | `unixMicros` (= createdAt) | `1700000000000000` | `1700003600000000` | `1700007200000000` | +/// | `location` (GeoPoint) | `(37.7749, -122.4194)` | `(40.7128, -74.006)` | `(51.5074, -0.1278)` | +/// | `pathRef` (reference) | book 2 | book 1 | book 1 | +/// | `embedding` (vector) | `[1, 0, 0]` | `[0, 1, 0]` | `[0, 0, 1]` | +/// | `sparse` | `'book 1 only'` | absent | absent | +/// | `first-name` | `'Ada'` | same | same | +/// | `last name` | `'Lovelace'` | same | same | +/// +/// Document IDs are `_book_1`, `_book_2` and `_book_3`. +List buildSeed( + String runId, + List> refs, +) { + Timestamp at(int seconds, [int nanoseconds = 0]) { + return Timestamp(seconds: seconds, nanoseconds: nanoseconds); + } + + // Fields whose values do not vary between books. + final shared = { + 'runId': runId, + 'zero': 0, + 'largeInt': 9007199254740993, + 'half': 2.5, + 'negativeHalf': -2.5, + 'precise': 4.123456, + 'nullable': null, + 'padded': '__Dart__', + 'empty': '', + 'unicode': 'café ☕ 日本', + 'sentence': 'Dart is fun and Dart is fast', + 'semicolons': 'a;b;c', + 'delimiter': ';', + 'mixedNumbers': [1, 2.5, -3, 0], + 'booleans': [true, false, true], + 'withNulls': [1, null, 3], + 'emptyArray': [], + 'nested': { + 'level1': { + 'level2': {'value': 42}, + }, + 'dotted.key': 'dot', + 'list': [1, 2], + }, + 'emptyMap': {}, + // Names that are not identifiers, which field paths must quote. + 'first-name': 'Ada', + 'last name': 'Lovelace', + }; + + return [ + { + ...shared, + 'title': 'Dart Pipelines', + 'active': true, + 'archived': false, + 'price': 10, + 'rating': 5, + 'discount': 2, + 'flags': 6, + 'quantity': 7, + 'score': -12.7, + 'ratio': 0.5, + 'bytes': Uint8List.fromList([0x0f, 0xf0]), + 'spaced': ' Dart ', + 'greeting': 'Hello, World!', + 'prefix': 'Dart', + 'email': 'dev.team@example.com', + 'csv': 'dart,firebase,admin', + 'tags': ['dart', 'firebase'], + 'words': ['dart', 'firebase'], + 'numbers': [3, 1, 2, 3], + 'scores': [1.5, 2.5, -0.5], + 'items': [ + {'name': 'pen', 'qty': 2, 'price': 1.5}, + {'name': 'ink', 'qty': 1, 'price': 4.0}, + ], + 'metadata': {'lang': 'dart', 'category': 'sdk'}, + 'stats': {'views': 100, 'likes': 7}, + 'createdAt': at(1700000000), + 'updatedAt': at(1700086400, 123456000), + 'unixSeconds': 1700000000, + 'unixMillis': 1700000000000, + 'unixMicros': 1700000000000000, + 'location': GeoPoint(latitude: 37.7749, longitude: -122.4194), + 'pathRef': refs[1], + 'embedding': FieldValue.vector([1, 0, 0]), + 'sparse': 'book 1 only', + }, + { + ...shared, + 'title': 'Firestore Admin', + 'active': true, + 'archived': false, + 'price': 20, + 'rating': 4, + 'discount': 3, + 'flags': 3, + 'quantity': 0, + 'score': 3.2, + 'ratio': 1.25, + 'bytes': Uint8List.fromList([0xaa, 0x55]), + 'spaced': ' Admin ', + 'greeting': 'Hello, Admin!', + 'prefix': 'Fire', + 'email': 'admin@example.org', + 'csv': 'firestore,admin', + 'tags': ['firebase'], + 'words': ['firebase'], + 'numbers': [4, 5], + 'scores': [4.0], + 'items': [ + {'name': 'cap', 'qty': 5, 'price': 0.5}, + ], + 'metadata': {'lang': 'dart', 'category': 'admin'}, + 'stats': {'views': 50, 'likes': 3}, + 'createdAt': at(1700003600), + 'updatedAt': at(1700090000, 123456000), + 'unixSeconds': 1700003600, + 'unixMillis': 1700003600000, + 'unixMicros': 1700003600000000, + 'location': GeoPoint(latitude: 40.7128, longitude: -74.006), + 'pathRef': refs[0], + 'embedding': FieldValue.vector([0, 1, 0]), + }, + { + ...shared, + 'title': 'Inactive Draft', + 'active': false, + 'archived': true, + 'price': 30, + 'rating': 2, + 'discount': 4, + 'flags': 5, + 'quantity': -3, + 'score': 8.9, + 'ratio': 2.0, + 'bytes': Uint8List.fromList([0xff, 0x00]), + 'spaced': ' Draft ', + 'greeting': 'Goodbye, Draft!', + 'prefix': 'Draft', + 'email': 'draft@example.net', + 'csv': 'inactive,draft', + 'tags': ['draft'], + 'words': ['draft'], + 'numbers': [9], + 'scores': [0.25, 0.75], + 'items': [], + 'metadata': {'lang': 'dart', 'category': 'draft'}, + 'stats': {'views': 0, 'likes': 0}, + 'createdAt': at(1700007200), + 'updatedAt': at(1700093600, 123456000), + 'unixSeconds': 1700007200, + 'unixMillis': 1700007200000, + 'unixMicros': 1700007200000000, + 'location': GeoPoint(latitude: 51.5074, longitude: -0.1278), + 'pathRef': refs[0], + 'embedding': FieldValue.vector([0, 0, 1]), + }, + ]; +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/results_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/results_test.dart new file mode 100644 index 00000000..776db50d --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/results_test.dart @@ -0,0 +1,243 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of `PipelineSnapshot` and `PipelineResult`: accessors, +/// document metadata and value decoding. +@Tags(['prod']) +library; + +import 'dart:typed_data'; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +import 'harness.dart'; + +/// [timestamp] in whole microseconds, for ordering comparisons. +int _micros(Timestamp timestamp) { + return timestamp.seconds * 1000000 + timestamp.nanoseconds ~/ 1000; +} + +void main() { + pipelineE2E('Pipeline results', (ctx) { + test('exposes document metadata', () async { + final snapshot = await ctx.book1Pipeline().limit(1).execute(); + final result = snapshot.results.single; + + expect(result.ref, isNotNull); + expect(result.ref!.path, ctx.book1Ref.path); + expect(result.id, '${ctx.runId}_book_1'); + // The full resource name of the document. + expect(result.name, startsWith('projects/')); + expect(result.name, endsWith('/documents/${ctx.book1Ref.path}')); + + // Each book is written once, so it was last updated when created, and + // before the pipeline ran. + expect(result.createTime, isNotNull); + expect(result.updateTime, result.createTime); + expect(snapshot.executionTime, isNotNull); + expect( + _micros(result.createTime!), + lessThanOrEqualTo(_micros(snapshot.executionTime!)), + ); + }); + + test('get resolves dotted paths and FieldPaths', () async { + final snapshot = await ctx.book1Pipeline().limit(1).execute(); + final result = snapshot.results.single; + + expect(result.get('metadata.lang'), 'dart'); + expect(result.get(FieldPath(const ['metadata', 'category'])), 'sdk'); + expect(result.get('nested.level1.level2.value'), 42); + expect( + result.get(FieldPath(const ['nested', 'level1', 'level2', 'value'])), + 42, + ); + // A FieldPath segment may contain a dot; a dotted string splits on it. + expect(result.get(FieldPath(const ['nested', 'dotted.key'])), 'dot'); + expect(result.get('nested.dotted.key'), isNull); + // Missing segments, and segments that traverse a non-map, read null. + expect(result.get('metadata.missing'), isNull); + expect(result.get('missing'), isNull); + expect(result.get('title.length'), isNull); + }); + + test('data decodes every seeded value type', () async { + final snapshot = await ctx.book1Pipeline().limit(1).execute(); + final data = snapshot.results.single.data(); + final seeded = ctx.seed[0]; + + // Every seeded field comes back, including the null, empty and sparse + // ones, with the value that was written. + expect(data.keys, unorderedEquals(seeded.keys)); + for (final key in seeded.keys) { + if (key == 'pathRef') continue; + expect(data[key], equals(seeded[key]), reason: key); + } + + // Numbers decode by wire type. + expect(data['price'], isInt(10)); + expect(data['largeInt'], isInt(9007199254740993)); + expect(data['score'], isDouble(-12.7)); + expect(data['ratio'], isDouble(0.5)); + expect(data['numbers'], [isInt(3), isInt(1), isInt(2), isInt(3)]); + expect(data['scores'], [isDouble(1.5), isDouble(2.5), isDouble(-0.5)]); + expect(data['mixedNumbers'], [ + isInt(1), + isDouble(2.5), + isInt(-3), + isInt(0), + ]); + expect(data['items'], [ + {'name': 'pen', 'qty': isInt(2), 'price': isDouble(1.5)}, + {'name': 'ink', 'qty': isInt(1), 'price': isDouble(4)}, + ]); + + // The other value types decode to their Dart classes. + expect(data['nullable'], isNull); + expect(data['bytes'], isA()); + expect(data['createdAt'], Timestamp(seconds: 1700000000, nanoseconds: 0)); + expect( + data['updatedAt'], + Timestamp(seconds: 1700086400, nanoseconds: 123456000), + ); + expect( + data['location'], + GeoPoint(latitude: 37.7749, longitude: -122.4194), + ); + expect(data['embedding'], isA()); + expect((data['embedding']! as VectorValue).toArray(), [1, 0, 0]); + expect(data['pathRef'], isA>()); + expect( + (data['pathRef']! as DocumentReference).path, + ctx.book2Ref.path, + ); + + // The returned map is read-only. + expect(() => data['title'] = 'changed', throwsUnsupportedError); + }); + + test('decodes a non-document reference the same at every depth', () async { + // Book 1's `pathRef` is book 2, a top-level document, so its parent is + // the database root: a reference that names no document, which decodes + // to its resource name rather than to a DocumentReference. + final root = Expression.field('pathRef').parent(); + final document = Expression.field('pathRef'); + final snapshot = await ctx + .book1Pipeline() + .select([ + root.as('root'), + PipelineFunctions.map([ + 'root', + root, + 'document', + document, + ]).as('inMap'), + PipelineFunctions.array([root, document]).as('inArray'), + PipelineFunctions.map([ + 'list', + PipelineFunctions.array([ + PipelineFunctions.map(['root', root]), + ]), + ]).as('nested'), + ]) + .limit(1) + .execute(); + final result = snapshot.results.single; + final isBook2 = isA>().having( + (reference) => reference.path, + 'path', + ctx.book2Ref.path, + ); + + final rootValue = result.get('root'); + expect(rootValue, isA()); + expect(result.get('inMap'), {'root': rootValue, 'document': isBook2}); + expect(result.get('inArray'), [rootValue, isBook2]); + expect(result.get('nested'), { + 'list': [ + {'root': rootValue}, + ], + }); + }); + + test('snapshot exposes its pipeline, size and execution time', () async { + final pipeline = ctx.runPipeline().sort([ascending('price')]); + final before = DateTime.now(); + final snapshot = await pipeline.execute(); + final after = DateTime.now(); + + expect(snapshot.pipeline, same(pipeline)); + expect(snapshot.size, 3); + expect(snapshot.empty, isFalse); + expect(snapshot.results, hasLength(3)); + expect(snapshot.results.map((result) => result.id), [ + '${ctx.runId}_book_1', + '${ctx.runId}_book_2', + '${ctx.runId}_book_3', + ]); + // The execution time is the backend's clock; allow for skew. + final executionTime = snapshot.executionTime!.toDate(); + expect( + executionTime.isAfter(before.subtract(const Duration(minutes: 5))), + isTrue, + reason: '$executionTime vs $before', + ); + expect( + executionTime.isBefore(after.add(const Duration(minutes: 5))), + isTrue, + reason: '$executionTime vs $after', + ); + }); + + test('an empty snapshot still has an execution time', () async { + final snapshot = await ctx.runPipeline().limit(0).execute(); + + expect(snapshot.empty, isTrue); + expect(snapshot.size, 0); + expect(snapshot.results, isEmpty); + expect(snapshot.executionTime, isNotNull); + }); + + test('results for the same document compare equal', () async { + final first = await ctx.book1Pipeline().execute(); + final second = await ctx.book1Pipeline().execute(); + final others = await ctx + .runPipeline() + .where(Expression.field('title').notEqual('Dart Pipelines')) + .execute(); + + expect(first.results.single, second.results.single); + expect(first.results.single.hashCode, second.results.single.hashCode); + expect(others.results, hasLength(2)); + for (final other in others.results) { + expect(first.results.single, isNot(other)); + } + }); + + test('aggregate results carry no document metadata', () async { + final snapshot = await ctx.runPipeline().aggregate([ + PipelineFunctions.countAll().as('books'), + ]).execute(); + final result = snapshot.results.single; + + expect(result.data(), {'books': 3}); + expect(result.ref, isNull); + expect(result.id, isNull); + expect(result.name, isNull); + expect(result.createTime, isNull); + expect(result.updateTime, isNull); + }); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/sources_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/sources_test.dart new file mode 100644 index 00000000..b895e17a --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/sources_test.dart @@ -0,0 +1,352 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of `Firestore.pipeline` and the `PipelineSource` stages. +@Tags(['prod']) +library; + +import 'dart:math' as math; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +import 'harness.dart'; + +/// The IDs of the documents [query] returns, in order. +Future> _queryIds(Query query) async { + final snapshot = await query.get(); + return [for (final doc in snapshot.docs) doc.id]; +} + +/// The IDs of the documents [vectorQuery] returns, in order. +Future> _vectorQueryIds( + VectorQuery vectorQuery, +) async { + final snapshot = await vectorQuery.get(); + return [for (final doc in snapshot.docs) doc.id]; +} + +/// The IDs of the documents [pipeline] returns, in order. +Future> _pipelineIds(Pipeline pipeline) async { + final snapshot = await pipeline.execute(); + return [for (final result in snapshot.results) result.id]; +} + +void main() { + pipelineE2E('Pipeline sources', (ctx) { + final book1 = ctx.book1Ref.id; + final book2 = ctx.book2Ref.id; + final book3 = ctx.book3Ref.id; + + /// A Query over this run's three documents. + Query runQuery() { + return ctx.firestore + .collection(ctx.collectionPath) + .where('runId', WhereFilter.equal, ctx.runId); + } + + /// [runQuery], as a vector query from [3, 2, 1] on `embedding`. + /// + /// The query vector ranks the books 1, 2, 3 under every measure: + /// euclidean distances 3, sqrt(11) ~ 3.317 and sqrt(13) ~ 3.606; cosine + /// distances 1 - 3/sqrt(14) ~ 0.198, 1 - 2/sqrt(14) ~ 0.465 and + /// 1 - 1/sqrt(14) ~ 0.733; dot products 3, 2 and 1. + VectorQuery runVectorQuery( + DistanceMeasure distanceMeasure, { + double? distanceThreshold, + String? distanceResultField, + }) { + return runQuery().findNearest( + vectorField: 'embedding', + queryVector: const [3, 2, 1], + limit: 3, + distanceMeasure: distanceMeasure, + distanceThreshold: distanceThreshold, + distanceResultField: distanceResultField, + ); + } + + test('executes a documents source stage', () async { + final snapshot = await ctx.firestore + .pipeline() + .documents([ctx.book1Ref, ctx.book3Ref]) + .sort([Expression.field('price').ascending()]) + .select([Expression.field('title')]) + .execute(); + + expect(snapshot.results.map((result) => result.get('title')), [ + 'Dart Pipelines', + 'Inactive Draft', + ]); + }); + + test('documents takes document paths and references', () async { + // A path is read like Firestore.doc reads it, ignoring a leading slash. + expect( + await ctx.titlesOf( + ctx.firestore + .pipeline() + .documents([ + ctx.book1Ref.path, + '/${ctx.book2Ref.path}', + ctx.book3Ref, + ]) + .sort([ascending('price')]) + .select(['title']), + ), + ['Dart Pipelines', 'Firestore Admin', 'Inactive Draft'], + ); + }); + + test('executes a collection group source stage', () async { + final snapshot = await ctx.firestore + .pipeline() + .collectionGroup(ctx.collectionPath) + .where(ctx.runFilter(Expression.field('active').equal(true))) + .sort([Expression.field('price').ascending()]) + .select([Expression.field('title')]) + .execute(); + + expect(snapshot.results.map((result) => result.get('title')), [ + 'Dart Pipelines', + 'Firestore Admin', + ]); + }); + + test('collectionReference starts from that collection', () async { + expect( + await ctx.titlesOf( + ctx.firestore + .pipeline() + .collectionReference(ctx.firestore.collection(ctx.collectionPath)) + .where(ctx.runFilter()) + .sort([ascending('price')]) + .select(['title']), + ), + ['Dart Pipelines', 'Firestore Admin', 'Inactive Draft'], + ); + }); + + test('database starts from every document', () async { + // The database source cannot be filtered before its first stage, so the + // run filter comes straight after it. + expect( + await ctx.titlesOf( + ctx.firestore + .pipeline() + .database() + .where(ctx.runFilter()) + .sort([ascending('price')]) + .select(['title']), + ), + ['Dart Pipelines', 'Firestore Admin', 'Inactive Draft'], + ); + }); + + test('executes a pipeline created from a query', () async { + // Translates the filter, the descending order and the limit. + expect( + await ctx.titlesOf( + ctx.firestore.pipeline().createFrom( + ctx.firestore + .collection(ctx.collectionPath) + .where('runId', WhereFilter.equal, ctx.runId) + .orderBy('price', descending: true) + .limit(2), + ), + ), + ['Inactive Draft', 'Firestore Admin'], + ); + }); + + test('executes a pipeline created from a vector query', () async { + // Euclidean distances from [3, 2, 1]: book 1 = 3, book 2 = sqrt(11), + // book 3 = sqrt(13). + expect( + await ctx.titlesOf( + ctx.firestore.pipeline().createFrom( + ctx.firestore + .collection(ctx.collectionPath) + .where('runId', WhereFilter.equal, ctx.runId) + .findNearest( + vectorField: 'embedding', + queryVector: const [3, 2, 1], + limit: 2, + distanceMeasure: DistanceMeasure.euclidean, + ), + ), + ), + ['Dart Pipelines', 'Firestore Admin'], + ); + }); + + test('executes a pipeline created from a collection group query', () async { + final snapshot = await ctx.firestore + .pipeline() + .createFrom( + ctx.firestore + .collectionGroup(ctx.collectionPath) + .where('runId', WhereFilter.equal, ctx.runId) + .where('active', WhereFilter.equal, true) + .orderBy('price'), + ) + .execute(); + + expect(snapshot.results.map((result) => result.get('title')), [ + 'Dart Pipelines', + 'Firestore Admin', + ]); + }); + + // != and not-in are inequalities, so createFrom sorts by the filtered + // field, then by the document key, as a Standard database orders the + // Query. Ratings are 5, 4 and 2: excluding 4 leaves book 3, then book 1. + // + // Only the documents the Query returns are compared, not their order: the + // Node SDK's system tests skip the != comparison on Enterprise databases, + // whose implicit Query order is not settled. + for (final (name, filter) in [ + ( + '!=', + (Query query) => + query.where('rating', WhereFilter.notEqual, 4), + ), + ( + 'not-in', + (Query query) => + query.where('rating', WhereFilter.notIn, [4]), + ), + ]) { + test('createFrom orders a $name query by its field first', () async { + final query = filter(runQuery()); + + expect(await _pipelineIds(ctx.firestore.pipeline().createFrom(query)), [ + book3, + book1, + ]); + expect(await _queryIds(query), unorderedEquals([book3, book1])); + }); + } + + // A cursor is a position in the query's order: on a descending rating + // (5, 4, 2), startAt(4) keeps 4 and below. + for (final (name, cursor, expected) in [ + ( + 'startAt', + (Query query) => query.startAt([4]), + [book2, book3], + ), + ( + 'startAfter', + (Query query) => query.startAfter([4]), + [book3], + ), + ( + 'endBefore', + (Query query) => query.endBefore([2]), + [book1, book2], + ), + ( + 'endAt with limitToLast', + (Query query) => query.endAt([4]).limitToLast(1), + [book2], + ), + ]) { + test('createFrom keeps the $name side of a descending cursor', () async { + final query = cursor(runQuery().orderBy('rating', descending: true)); + + expect(await _queryIds(query), expected); + expect( + await _pipelineIds(ctx.firestore.pipeline().createFrom(query)), + expected, + ); + }); + } + + test( + 'a DocumentSnapshot cursor on a != query orders by that field', + () async { + // The cursor orders by rating, then the document key, so starting at + // book 3 (rating 2) keeps book 3, then book 1 (rating 5). Ordering by + // the document key alone would keep book 3 only. + final query = runQuery() + .where('rating', WhereFilter.notEqual, 4) + .startAtDocument(await ctx.book3Ref.get()); + + expect(await _queryIds(query), [book3, book1]); + expect(await _pipelineIds(ctx.firestore.pipeline().createFrom(query)), [ + book3, + book1, + ]); + }, + ); + + // Each threshold keeps books 1 and 2 and drops book 3: at most 3.5 for + // euclidean, at most 0.5 for cosine, at least 1.5 for dot product. + for (final (distanceMeasure, distanceThreshold) in [ + (DistanceMeasure.euclidean, 3.5), + (DistanceMeasure.cosine, 0.5), + (DistanceMeasure.dotProduct, 1.5), + ]) { + test( + 'createFrom keeps a ${distanceMeasure.name} distance threshold', + () async { + final vectorQuery = runVectorQuery( + distanceMeasure, + distanceThreshold: distanceThreshold, + ); + + expect(await _vectorQueryIds(vectorQuery), [book1, book2]); + expect( + await _pipelineIds( + ctx.firestore.pipeline().createFrom(vectorQuery), + ), + [book1, book2], + ); + }, + ); + } + + test( + 'createFrom filters a distance threshold on its result field', + () async { + final vectorQuery = runVectorQuery( + DistanceMeasure.euclidean, + distanceThreshold: 3.5, + distanceResultField: 'distance', + ); + + final querySnapshot = await vectorQuery.get(); + final pipelineSnapshot = await ctx.firestore + .pipeline() + .createFrom(vectorQuery) + .execute(); + + // Each result carries its euclidean distance in the result field. + final distances = [isNumber(3), isNumber(math.sqrt(11))]; + expect([for (final doc in querySnapshot.docs) doc.id], [book1, book2]); + expect([ + for (final doc in querySnapshot.docs) doc.data()['distance'], + ], distances); + expect( + [for (final result in pipelineSnapshot.results) result.id], + [book1, book2], + ); + expect([ + for (final result in pipelineSnapshot.results) result.get('distance'), + ], distances); + }, + ); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline/stages_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline/stages_test.dart new file mode 100644 index 00000000..31736441 --- /dev/null +++ b/packages/google_cloud_firestore/test/e2e/pipeline/stages_test.dart @@ -0,0 +1,698 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Live coverage of the `Pipeline` stage methods, orderings and aliases. +@Tags(['prod']) +library; + +import 'dart:math' as math; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:test/test.dart'; + +import 'harness.dart'; + +/// The titles of the three seeded books, in book order. +const _allTitles = ['Dart Pipelines', 'Firestore Admin', 'Inactive Draft']; + +/// The data of every result [pipeline] produces, in order. +Future> _dataOf(Pipeline pipeline) async { + final snapshot = await pipeline.execute(); + return [for (final result in snapshot.results) result.data()]; +} + +void main() { + pipelineE2E('Pipeline stages', (ctx) { + test('where, sort, select and limit', () async { + final snapshot = await ctx.firestore + .pipeline() + .collection(ctx.collectionPath) + .where(ctx.runFilter(Expression.field('active').asBoolean())) + .sort([Expression.field('price').ascending()]) + .select([ + Expression.field('title'), + Expression.field('price'), + Expression.field('title').toUpperCase().as('upperTitle'), + Expression.field('tags').arrayLength().as('tagCount'), + Expression.field( + 'metadata', + ).mapGetLiteral('category').as('category'), + Expression.field( + 'createdAt', + ).timestampToUnixSeconds().as('createdSeconds'), + ]) + .limit(2) + .execute(); + + expect(snapshot.results, hasLength(2)); + expect(snapshot.executionTime, isNotNull); + expect(snapshot.results.first.get('title'), 'Dart Pipelines'); + expect(snapshot.results.first.get('price'), 10); + expect(snapshot.results.first.get('upperTitle'), 'DART PIPELINES'); + expect(snapshot.results.first.get('tagCount'), 2); + expect(snapshot.results.first.get('category'), 'sdk'); + expect(snapshot.results.last.get('title'), 'Firestore Admin'); + }); + + test('select takes field names, fields and aliased expressions', () async { + final data = await _dataOf( + ctx.book1Pipeline().select([ + 'title', + 'price', + Expression.field('rating'), + Expression.field('discount').as('markdown'), + ]), + ); + + // Only the selected fields remain. + expect(data, [ + {'title': 'Dart Pipelines', 'price': 10, 'rating': 5, 'markdown': 2}, + ]); + }); + + test('select keeps field names that are not identifiers', () async { + // The backend reads select keys as field paths and rejects an unquoted + // `last name` ("Invalid property path"), so a String is keyed by its + // quoted path ("`last name`"), like a PipelineField. Whether a result + // comes back under the field name or the quoted path is not pinned + // down yet, so each value is read through whichever key comes back. + final data = await _dataOf( + ctx.book1Pipeline().select([field('first-name'), 'last name']), + ); + + expect(data, hasLength(1)); + final result = data.single; + expect(result, hasLength(2)); + Object? valueOf(String name) { + final key = result.keys.singleWhere( + (key) => key == name || key == '`$name`', + ); + return result[key]; + } + + expect(valueOf('first-name'), 'Ada'); + expect(valueOf('last name'), 'Lovelace'); + }); + + test('sort and select take FieldPaths with a dotted segment', () async { + // `dotted.key` is a single field of `nested`, 'dot' in every book, so + // the categories decide the order: sdk, draft, admin. + final snapshot = await ctx + .runPipeline() + .sort([ + ascending(FieldPath(const ['nested', 'dotted.key'])), + descending(FieldPath(const ['metadata', 'category'])), + ]) + .select([ + 'title', + FieldPath(const ['nested', 'dotted.key']), + ]) + .execute(); + + expect( + [for (final result in snapshot.results) result.get('title')], + ['Dart Pipelines', 'Inactive Draft', 'Firestore Admin'], + ); + // The selection is keyed by its quoted path, nested.`dotted.key`. + // Whether the value comes back nested under `nested` or under a single + // key is not pinned down yet, so any of them is accepted. + for (final result in snapshot.results) { + final data = result.data(); + expect( + result.get(FieldPath(const ['nested', 'dotted.key'])) ?? + data['nested.`dotted.key`'] ?? + data['nested.dotted.key'], + 'dot', + ); + } + }); + + test('an alias names the field its expression lands on', () async { + final aliased = Expression.field('price').as('cost'); + + expect(aliased.name, 'cost'); + expect(aliased.expression, isA()); + expect((aliased.expression as PipelineField).path, 'price'); + expect(await _dataOf(ctx.book1Pipeline().select([aliased])), [ + {'cost': 10}, + ]); + }); + + test('executes an addFields stage', () async { + final snapshot = await ctx.book1Pipeline().addFields([ + Expression.field('rating').as('copiedRating'), + Expression.constant(true).as('annotated'), + ]).execute(); + + expect(snapshot.results, hasLength(1)); + final result = snapshot.results.single; + expect(result.get('copiedRating'), 5); + expect(result.get('annotated'), true); + // Existing fields are kept alongside the added ones. + expect(result.get('title'), 'Dart Pipelines'); + + // A single field must still be sent as a map, not an alias function. + final single = await ctx.book1Pipeline().addFields([ + Expression.field('title').toUpperCase().as('upper'), + ]).execute(); + + expect(single.results.single.get('upper'), 'DART PIPELINES'); + }); + + test( + 'removeFields drops fields named by string, FieldPath or field', + () async { + final data = await _dataOf( + ctx.book1Pipeline().removeFields([ + Expression.field('price'), + 'rating', + FieldPath(const ['first-name']), + ]), + ); + + expect(data, hasLength(1)); + // Every other seeded field is still there. + const removed = {'price', 'rating', 'first-name'}; + expect( + data.single.keys, + unorderedEquals( + ctx.seed[0].keys.where((key) => !removed.contains(key)), + ), + ); + expect(data.single['title'], 'Dart Pipelines'); + }, + ); + + test('sort applies its orderings in sequence', () async { + // active descending puts the two active books first (false sorts before + // true); price descending then orders those two. + expect( + await ctx.titlesOf( + ctx.runPipeline().sort([ + descending(Expression.field('active')), + descending('price'), + ]), + ), + ['Firestore Admin', 'Dart Pipelines', 'Inactive Draft'], + ); + + // half is 2.5 on every book, so score descending decides the order. + expect( + await ctx.titlesOf( + ctx.runPipeline().sort([ + ascending('half'), + Expression.field('score').descending(), + ]), + ), + ['Inactive Draft', 'Firestore Admin', 'Dart Pipelines'], + ); + + // flags: book 1 = 6, book 2 = 3, book 3 = 5. + expect( + await ctx.titlesOf( + ctx.runPipeline().sort([ascending(Expression.field('flags'))]), + ), + ['Firestore Admin', 'Inactive Draft', 'Dart Pipelines'], + ); + }); + + test('an ordering exposes its expression and direction', () async { + final orderings = [ + descending('active'), + Expression.field('price').ascending(), + ]; + + expect( + [for (final ordering in orderings) ordering.direction], + ['descending', 'ascending'], + ); + // A field name given to descending() reads back as a field. + expect( + [ + for (final ordering in orderings) + (ordering.expr as PipelineField).path, + ], + ['active', 'price'], + ); + + // Orderings rebuilt from expr and direction sort the same way: the two + // active books first, by ascending price. + final rebuilt = [ + for (final ordering in orderings) + ordering.direction == 'ascending' + ? ordering.expr.ascending() + : ordering.expr.descending(), + ]; + expect(await ctx.titlesOf(ctx.runPipeline().sort(rebuilt)), _allTitles); + }); + + test('offset skips sorted inputs before limit', () async { + final sorted = ctx.runPipeline().sort([ + Expression.field('price').ascending(), + ]); + + expect(await ctx.titlesOf(sorted.offset(1)), [ + 'Firestore Admin', + 'Inactive Draft', + ]); + expect(await ctx.titlesOf(sorted.offset(1).limit(1)), [ + 'Firestore Admin', + ]); + expect(await ctx.titlesOf(sorted.offset(3)), isEmpty); + }); + + test('distinct returns each unique combination of groups', () async { + expect( + await _dataOf( + ctx + .runPipeline() + .distinct([ + 'active', + Expression.field('metadata').mapGetLiteral('lang').as('lang'), + ]) + .sort([ascending('active')]), + ), + [ + {'active': false, 'lang': 'dart'}, + {'active': true, 'lang': 'dart'}, + ], + ); + + // A FieldPath names a group as a String does. + expect( + await _dataOf( + ctx + .runPipeline() + .distinct([ + FieldPath(const ['active']), + ]) + .sort([ascending('active')]), + ), + [ + {'active': false}, + {'active': true}, + ], + ); + }); + + test('unnest by field name replaces the array with each element', () async { + expect( + await _dataOf( + ctx.book1Pipeline().unnest('tags').select(['title', 'tags']).sort([ + ascending('tags'), + ]), + ), + [ + {'title': 'Dart Pipelines', 'tags': 'dart'}, + {'title': 'Dart Pipelines', 'tags': 'firebase'}, + ], + ); + }); + + test('unnest by FieldPath replaces the array with each element', () async { + expect( + await _dataOf( + ctx + .book1Pipeline() + .unnest(FieldPath(const ['tags'])) + .select(['title', 'tags']) + .sort([ascending('tags')]), + ), + [ + {'title': 'Dart Pipelines', 'tags': 'dart'}, + {'title': 'Dart Pipelines', 'tags': 'firebase'}, + ], + ); + }); + + test('unnest an aliased field keeps the source array', () async { + expect( + await _dataOf( + ctx + .book1Pipeline() + .unnest(Expression.field('scores').as('element')) + .select(['scores', 'element']) + .sort([ascending('element')]), + ), + [ + { + 'scores': [1.5, 2.5, -0.5], + 'element': -0.5, + }, + { + 'scores': [1.5, 2.5, -0.5], + 'element': 1.5, + }, + { + 'scores': [1.5, 2.5, -0.5], + 'element': 2.5, + }, + ], + ); + }); + + test('unnest with an index field numbers each element', () async { + // tags: book 1 = [dart, firebase], book 2 = [firebase], book 3 = [draft]. + expect( + await _dataOf( + ctx + .runPipeline() + .unnest( + Expression.field('tags').as('tag'), + indexField: 'tagIndex', + ) + .select(['title', 'tag', 'tagIndex']) + .sort([ascending('tag'), ascending('tagIndex')]), + ), + [ + {'title': 'Dart Pipelines', 'tag': 'dart', 'tagIndex': 0}, + {'title': 'Inactive Draft', 'tag': 'draft', 'tagIndex': 0}, + {'title': 'Firestore Admin', 'tag': 'firebase', 'tagIndex': 0}, + {'title': 'Dart Pipelines', 'tag': 'firebase', 'tagIndex': 1}, + ], + ); + }); + + test('unnest rawOptions take precedence over indexField', () async { + // The raw index_field replaces the typed one, so only rawIndex is set. + final data = await _dataOf( + ctx + .book1Pipeline() + .unnest( + Expression.field('tags').as('tag'), + indexField: 'typedIndex', + rawOptions: {'index_field': Expression.field('rawIndex')}, + ) + .sort([ascending('rawIndex')]), + ); + + expect([for (final result in data) result['tag']], ['dart', 'firebase']); + expect([for (final result in data) result['rawIndex']], [0, 1]); + expect( + [for (final result in data) result.containsKey('typedIndex')], + [false, false], + ); + }); + + test('replaceWith a field name promotes that map', () async { + expect(await _dataOf(ctx.book1Pipeline().replaceWith('metadata')), [ + {'lang': 'dart', 'category': 'sdk'}, + ]); + expect( + await _dataOf( + ctx.book1Pipeline().replaceWith( + FieldPath(const ['nested', 'level1']), + ), + ), + [ + { + 'level2': {'value': 42}, + }, + ], + ); + }); + + test('replaceWith an expression uses the map it evaluates to', () async { + expect( + await _dataOf( + ctx.book1Pipeline().replaceWith(Expression.field('stats')), + ), + [ + {'views': 100, 'likes': 7}, + ], + ); + expect( + await _dataOf( + ctx.book1Pipeline().replaceWith( + PipelineFunctions.map([ + 'name', + Expression.field('title'), + 'answer', + 42, + ]), + ), + ), + [ + {'name': 'Dart Pipelines', 'answer': 42}, + ], + ); + }); + + test('union appends the other pipeline, keeping duplicates', () async { + expect( + await ctx.titlesOf( + ctx + .book1Pipeline() + .union(ctx.runPipeline()) + .sort([ascending('price')]) + .select(['title']), + ), + [ + 'Dart Pipelines', + 'Dart Pipelines', + 'Firestore Admin', + 'Inactive Draft', + ], + ); + }); + + test('sample by document count returns that many inputs', () async { + final titles = await ctx.titlesOf(ctx.runPipeline().sample(documents: 2)); + + expect(titles, hasLength(2)); + expect(titles.toSet(), hasLength(2)); + expect(titles, everyElement(isIn(_allTitles))); + }); + + test('sample by percentage returns a subset of the inputs', () async { + final titles = await ctx.titlesOf( + ctx.runPipeline().sample(percentage: 0.5), + ); + + // Each input is kept at random, so only the bounds are deterministic. + expect(titles.length, lessThanOrEqualTo(3)); + expect(titles.toSet(), hasLength(titles.length)); + expect(titles, everyElement(isIn(_allTitles))); + }); + + test('executes vector nearest-neighbor stage', () async { + final vectorSnapshot = await ctx + .runPipeline() + .findNearest( + vectorField: 'embedding', + queryVector: Expression.vector([1, 0, 0]), + distanceMeasure: DistanceMeasure.cosine, + limit: 1, + distanceResultField: 'distance', + ) + .execute(); + + expect(vectorSnapshot.results, hasLength(1)); + expect(vectorSnapshot.results.single.get('title'), 'Dart Pipelines'); + // Book 1's embedding is the query vector itself. + expect( + vectorSnapshot.results.single.get('distance'), + isNumber(0, tolerance: 1e-9), + ); + }); + + test('executes vector nearest-neighbor stage with a list', () async { + final vectorSnapshot = await ctx + .runPipeline() + .findNearest( + vectorField: 'embedding', + queryVector: const [1.0, 0.0, 0.0], + distanceMeasure: DistanceMeasure.cosine, + limit: 1, + distanceResultField: 'distance', + ) + .execute(); + + expect(vectorSnapshot.results, hasLength(1)); + expect(vectorSnapshot.results.single.get('title'), 'Dart Pipelines'); + }); + + // The query vector [3, 2, 1] ranks the books 1, 2, 3 under every measure: + // euclidean distances 3, sqrt(11), sqrt(13); dot products 3, 2, 1; cosine + // distances 1 - 3/sqrt(14), 1 - 2/sqrt(14), 1 - 1/sqrt(14). + + test('findNearest by euclidean distance, without a limit', () async { + final snapshot = await ctx + .runPipeline() + .findNearest( + vectorField: Expression.field('embedding'), + queryVector: const [3.0, 2.0, 1.0], + distanceMeasure: DistanceMeasure.euclidean, + distanceResultField: 'distance', + ) + .execute(); + + expect([ + for (final result in snapshot.results) result.get('title'), + ], _allTitles); + expect( + [for (final result in snapshot.results) result.get('distance')], + [isNumber(3), isNumber(math.sqrt(11)), isNumber(math.sqrt(13))], + ); + }); + + test('findNearest by dot product returns the largest first', () async { + final snapshot = await ctx + .runPipeline() + .findNearest( + vectorField: 'embedding', + queryVector: Expression.vector([3, 2, 1]), + distanceMeasure: DistanceMeasure.dotProduct, + limit: 2, + distanceResultField: 'similarity', + ) + .execute(); + + expect( + [for (final result in snapshot.results) result.get('title')], + ['Dart Pipelines', 'Firestore Admin'], + ); + expect( + [for (final result in snapshot.results) result.get('similarity')], + [isNumber(3), isNumber(2)], + ); + }); + + test('findNearest by cosine distance, with FieldPath fields', () async { + final snapshot = await ctx + .runPipeline() + .findNearest( + vectorField: FieldPath(const ['embedding']), + queryVector: const [3.0, 2.0, 1.0], + distanceMeasure: DistanceMeasure.cosine, + limit: 3, + distanceResultField: FieldPath(const ['distance']), + ) + .execute(); + + expect([ + for (final result in snapshot.results) result.get('title'), + ], _allTitles); + expect( + [for (final result in snapshot.results) result.get('distance')], + [ + isNumber(1 - 3 / math.sqrt(14)), + isNumber(1 - 2 / math.sqrt(14)), + isNumber(1 - 1 / math.sqrt(14)), + ], + ); + }); + + test('findNearest drops inputs beyond the distance threshold', () async { + // Euclidean distances 3 and sqrt(11) ~ 3.317 are within 3.5; + // sqrt(13) ~ 3.606 is not. + expect( + await ctx.titlesOf( + ctx.runPipeline().findNearest( + vectorField: 'embedding', + queryVector: const [3.0, 2.0, 1.0], + distanceMeasure: DistanceMeasure.euclidean, + limit: 3, + distanceThreshold: 3.5, + ), + ), + ['Dart Pipelines', 'Firestore Admin'], + ); + }); + + test('findNearest rawOptions take precedence over its options', () async { + // The raw limit replaces the typed one, and the raw distance_field sets + // the result field distanceResultField would. + final snapshot = await ctx + .runPipeline() + .findNearest( + vectorField: 'embedding', + queryVector: const [3.0, 2.0, 1.0], + distanceMeasure: DistanceMeasure.euclidean, + limit: 3, + rawOptions: { + 'limit': 1, + 'distance_field': Expression.field('rawDistance'), + }, + ) + .execute(); + + expect(snapshot.results, hasLength(1)); + expect(snapshot.results.single.get('title'), 'Dart Pipelines'); + expect(snapshot.results.single.get('rawDistance'), isNumber(3)); + }); + + test('rawStage sends stages by their backend name', () async { + // price > 15 keeps books 2 and 3; descending price puts book 3 first, + // and the offset skips it. + expect( + await ctx.titlesOf( + ctx + .runPipeline() + .rawStage('where', [Expression.field('price').greaterThan(15)]) + .rawStage('sort', [ + { + 'direction': 'descending', + 'expression': Expression.field('price'), + }, + ]) + .rawStage('offset', [1]) + .rawStage('limit', [1]), + ), + ['Firestore Admin'], + ); + }); + + test('rawStage select takes a map of expressions', () async { + expect( + await _dataOf( + ctx.book1Pipeline().rawStage('select', [ + { + 'name': Expression.field('title'), + 'cost': Expression.field('price'), + }, + ]), + ), + [ + {'name': 'Dart Pipelines', 'cost': 10}, + ], + ); + }); + + test('rawStage passes stage options', () async { + final data = await _dataOf( + ctx + .runPipeline() + .rawStage( + 'find_nearest', + [ + Expression.field('embedding'), + FieldValue.vector([3, 2, 1]), + 'euclidean', + ], + options: { + 'distance_field': Expression.field('distance'), + 'limit': 2, + }, + ) + .select(['title', 'distance']), + ); + + expect(data, [ + {'title': 'Dart Pipelines', 'distance': isNumber(3)}, + {'title': 'Firestore Admin', 'distance': isNumber(math.sqrt(11))}, + ]); + }); + }); +} diff --git a/packages/google_cloud_firestore/test/e2e/pipeline_e2e_test.dart b/packages/google_cloud_firestore/test/e2e/pipeline_e2e_test.dart deleted file mode 100644 index ce052acb..00000000 --- a/packages/google_cloud_firestore/test/e2e/pipeline_e2e_test.dart +++ /dev/null @@ -1,1264 +0,0 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -@Tags(['prod']) -library; - -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:google_cloud_firestore/google_cloud_firestore.dart'; -import 'package:test/test.dart'; - -const _projectIdEnv = 'FIRESTORE_PIPELINE_E2E_PROJECT_ID'; -const _databaseIdEnv = 'FIRESTORE_PIPELINE_E2E_DATABASE_ID'; -const _collectionPath = 'pipeline_e2e_books'; - -void main() { - final projectId = - Platform.environment[_projectIdEnv] ?? - Platform.environment['GOOGLE_CLOUD_PROJECT'] ?? - Platform.environment['GCLOUD_PROJECT']; - // No '(default)' fallback on purpose. CI credential helpers such as - // google-github-actions/auth export GOOGLE_CLOUD_PROJECT, so a project-only - // guard would silently arm this suite against whatever project happened to - // be authenticated, using its default database — which may be Standard - // edition (no Pipelines at all) or simply lack the vector index this suite - // needs. Both variables must be set deliberately. - final databaseId = Platform.environment[_databaseIdEnv]; - final shouldRun = - projectId != null && - projectId.isNotEmpty && - databaseId != null && - databaseId.isNotEmpty; - - group('Firestore Pipeline E2E', skip: shouldRun ? null : _skipReason, () { - late Firestore firestore; - late String runId; - late List> docs; - - setUp(() async { - firestore = Firestore( - settings: Settings(projectId: projectId, databaseId: databaseId), - ); - runId = 'run_${DateTime.now().microsecondsSinceEpoch}'; - docs = [ - firestore.doc('$_collectionPath/${runId}_book_1'), - firestore.doc('$_collectionPath/${runId}_book_2'), - firestore.doc('$_collectionPath/${runId}_book_3'), - ]; - - await Future.wait([ - docs[0].set({ - 'runId': runId, - 'title': 'Dart Pipelines', - 'active': true, - 'price': 10, - 'rating': 5, - 'discount': 2, - 'score': -12.7, - 'flags': 6, - 'bytes': Uint8List.fromList([0x0f, 0xf0]), - 'tags': ['dart', 'firebase'], - 'numbers': [3, 1, 2, 3], - 'words': ['dart', 'firebase'], - 'metadata': {'lang': 'dart', 'category': 'sdk'}, - 'pathRef': docs[1], - 'nullable': null, - 'spaced': ' Dart ', - 'csv': 'dart,firebase,admin', - 'createdAt': Timestamp(seconds: 1700000000, nanoseconds: 0), - 'embedding': FieldValue.vector([1, 0, 0]), - }), - docs[1].set({ - 'runId': runId, - 'title': 'Firestore Admin', - 'active': true, - 'price': 20, - 'rating': 4, - 'discount': 3, - 'score': 3.2, - 'flags': 3, - 'bytes': Uint8List.fromList([0xaa, 0x55]), - 'tags': ['firebase'], - 'numbers': [4, 5], - 'words': ['firebase'], - 'metadata': {'lang': 'dart', 'category': 'admin'}, - 'pathRef': docs[0], - 'nullable': null, - 'spaced': ' Admin ', - 'csv': 'firestore,admin', - 'createdAt': Timestamp(seconds: 1700003600, nanoseconds: 0), - 'embedding': FieldValue.vector([0, 1, 0]), - }), - docs[2].set({ - 'runId': runId, - 'title': 'Inactive Draft', - 'active': false, - 'price': 30, - 'rating': 2, - 'discount': 4, - 'score': 8.9, - 'flags': 5, - 'bytes': Uint8List.fromList([0xff, 0x00]), - 'tags': ['draft'], - 'numbers': [9], - 'words': ['draft'], - 'metadata': {'lang': 'dart', 'category': 'draft'}, - 'pathRef': docs[0], - 'nullable': null, - 'spaced': ' Draft ', - 'csv': 'inactive,draft', - 'createdAt': Timestamp(seconds: 1700007200, nanoseconds: 0), - 'embedding': FieldValue.vector([0, 0, 1]), - }), - ]); - }); - - tearDown(() async { - await Future.wait([for (final doc in docs) doc.delete()]); - }); - - test('executes a real pipeline with expressions and metadata', () async { - final snapshot = await firestore - .pipeline() - .collection(_collectionPath) - .where(_runFilter(runId, Expression.field('active').asBoolean())) - .sort([Expression.field('price').ascending()]) - .select([ - Expression.field('title'), - Expression.field('price'), - Expression.field('title').toUpperCase().as('upperTitle'), - Expression.field('tags').arrayLength().as('tagCount'), - Expression.field( - 'metadata', - ).mapGetLiteral('category').as('category'), - Expression.field( - 'createdAt', - ).timestampToUnixSeconds().as('createdSeconds'), - ]) - .limit(2) - .execute(); - - expect(snapshot.results, hasLength(2)); - expect(snapshot.executionTime, isNotNull); - expect(snapshot.results.first.get('title'), 'Dart Pipelines'); - expect(snapshot.results.first.get('price'), 10); - expect(snapshot.results.first.get('upperTitle'), 'DART PIPELINES'); - expect(snapshot.results.first.get('tagCount'), 2); - expect(snapshot.results.first.get('category'), 'sdk'); - - final metadataSnapshot = await firestore - .pipeline() - .collection(_collectionPath) - .where( - _runFilter( - runId, - Expression.field('title').equal('Dart Pipelines'), - ), - ) - .limit(1) - .execute(); - - expect(metadataSnapshot.results.single.createTime, isNotNull); - expect(metadataSnapshot.results.single.updateTime, isNotNull); - expect(metadataSnapshot.results.single.ref, isNotNull); - // get resolves nested dot-paths and FieldPaths, as in Node. - expect(metadataSnapshot.results.single.get('metadata.lang'), 'dart'); - expect( - metadataSnapshot.results.single.get( - FieldPath(const ['metadata', 'category']), - ), - 'sdk', - ); - expect(metadataSnapshot.results.single.get('metadata.missing'), isNull); - }); - - test('executes aggregate pipeline stages', () async { - final aggregateSnapshot = await firestore - .pipeline() - .collection(_collectionPath) - .where(_runFilter(runId, Expression.field('active').equal(true))) - .aggregate([ - Expression.field('price').sum().as('totalPrice'), - Expression.field('rating').average().as('averageRating'), - Expression.field('title').count().as('bookCount'), - ]) - .execute(); - - expect(aggregateSnapshot.results, hasLength(1)); - expect(aggregateSnapshot.results.single.get('totalPrice'), 30); - expect(aggregateSnapshot.results.single.get('averageRating'), 4.5); - expect(aggregateSnapshot.results.single.get('bookCount'), 2); - }); - - test('executes an addFields stage', () async { - final snapshot = await firestore - .pipeline() - .collection(_collectionPath) - .where( - _runFilter( - runId, - Expression.field('title').equal('Dart Pipelines'), - ), - ) - .addFields([ - Expression.field('rating').as('copiedRating'), - Expression.constant(true).as('annotated'), - ]) - .execute(); - - expect(snapshot.results, hasLength(1)); - final result = snapshot.results.single; - expect(result.get('copiedRating'), 5); - expect(result.get('annotated'), true); - // Existing fields are kept alongside the added ones. - expect(result.get('title'), 'Dart Pipelines'); - - // A single field must still be sent as a map, not an alias function. - final single = await firestore - .pipeline() - .collection(_collectionPath) - .where( - _runFilter( - runId, - Expression.field('title').equal('Dart Pipelines'), - ), - ) - .addFields([Expression.field('title').toUpperCase().as('upper')]) - .execute(); - - expect(single.results.single.get('upper'), 'DART PIPELINES'); - }); - - test('requests explain stats via typed options', () async { - final snapshot = await firestore - .pipeline() - .collection(_collectionPath) - .where(_runFilter(runId, Expression.field('active').equal(true))) - .execute( - explain: const PipelineExplainOptions( - mode: PipelineExplainMode.analyze, - outputFormat: PipelineExplainOutputFormat.text, - ), - ); - - // `analyze` runs the pipeline and returns planning stats alongside the - // results, which is what proves the option names reached the backend. - expect(snapshot.results, isNotEmpty); - expect(snapshot.explainStats, isNotNull); - expect(snapshot.explainStats!.text, isNotEmpty); - }); - - test('executes a pipeline inside a transaction', () async { - final titles = await firestore.runTransaction((transaction) async { - final snapshot = await transaction.executePipeline( - firestore - .pipeline() - .collection(_collectionPath) - .where(_runFilter(runId, Expression.field('active').equal(true))) - .sort([Expression.field('price').ascending()]) - .select([Expression.field('title')]), - ); - return [for (final result in snapshot.results) result.get('title')]; - }, transactionOptions: ReadOnlyTransactionOptions()); - - expect(titles, ['Dart Pipelines', 'Firestore Admin']); - }); - - test('executes a documents source stage', () async { - final snapshot = await firestore - .pipeline() - .documents([docs[0], docs[2]]) - .sort([Expression.field('price').ascending()]) - .select([Expression.field('title')]) - .execute(); - - expect(snapshot.results.map((result) => result.get('title')), [ - 'Dart Pipelines', - 'Inactive Draft', - ]); - }); - - test('filters with a search space holding expressions', () async { - // Regression: the list was sent as a literal array value, which the - // backend rejected with "Value type is not supported: - // FIELD_REFERENCE_VALUE". - final snapshot = await firestore - .pipeline() - .collection(_collectionPath) - .where( - _runFilter( - runId, - PipelineFunctions.equalAny('discount', [ - Expression.field('flags'), - 2, - ]), - ), - ) - .sort([Expression.field('price').ascending()]) - .select([Expression.field('title')]) - .execute(); - - // Book 1 matches the literal 2, book 2 its own `flags` (3). - expect(snapshot.results.map((result) => result.get('title')), [ - 'Dart Pipelines', - 'Firestore Admin', - ]); - }); - - test('executes a collection group source stage', () async { - final snapshot = await firestore - .pipeline() - .collectionGroup(_collectionPath) - .where(_runFilter(runId, Expression.field('active').equal(true))) - .sort([Expression.field('price').ascending()]) - .select([Expression.field('title')]) - .execute(); - - expect(snapshot.results.map((result) => result.get('title')), [ - 'Dart Pipelines', - 'Firestore Admin', - ]); - }); - - test('executes a pipeline created from a collection group query', () async { - final snapshot = await firestore - .pipeline() - .createFrom( - firestore - .collectionGroup(_collectionPath) - .where('runId', WhereFilter.equal, runId) - .where('active', WhereFilter.equal, true) - .orderBy('price'), - ) - .execute(); - - expect(snapshot.results.map((result) => result.get('title')), [ - 'Dart Pipelines', - 'Firestore Admin', - ]); - }); - - group('function catalog', () { - for (final scenario in _functionScenarios) { - test(scenario.name, () async { - final snapshot = await firestore - .pipeline() - .collection(_collectionPath) - .where( - _runFilter( - runId, - Expression.field('title').equal('Dart Pipelines'), - ), - ) - .select([ - for (final expectation in scenario.expectations) - expectation.expression.as(expectation.alias), - ]) - .limit(1) - .execute(); - - expect(snapshot.results, hasLength(1)); - final result = snapshot.results.single; - for (final expectation in scenario.expectations) { - _expectPipelineValue( - result.get(expectation.alias), - expectation.expected, - reason: '${scenario.name}.${expectation.alias}', - ); - } - }); - } - }); - - group('aggregate function catalog', () { - for (final scenario in _aggregateScenarios) { - test(scenario.name, () async { - final snapshot = await firestore - .pipeline() - .collection(_collectionPath) - .where(Expression.field('runId').equal(runId)) - .aggregate([ - for (final expectation in scenario.expectations) - expectation.expression.as(expectation.alias), - ]) - .execute(); - - expect(snapshot.results, hasLength(1)); - final result = snapshot.results.single; - for (final expectation in scenario.expectations) { - _expectPipelineValue( - result.get(expectation.alias), - expectation.expected, - reason: '${scenario.name}.${expectation.alias}', - ); - } - }); - } - }); - - test('executes vector nearest-neighbor stage', () async { - final vectorSnapshot = await firestore - .pipeline() - .collection(_collectionPath) - .where(Expression.field('runId').equal(runId)) - .findNearest( - vectorField: 'embedding', - queryVector: Expression.vector([1, 0, 0]), - distanceMeasure: DistanceMeasure.cosine, - limit: 1, - distanceResultField: 'distance', - ) - .execute(); - - expect(vectorSnapshot.results, hasLength(1)); - expect(vectorSnapshot.results.single.get('title'), 'Dart Pipelines'); - expect(vectorSnapshot.results.single.get('distance'), isNotNull); - }); - - test('executes vector nearest-neighbor stage with a list', () async { - final vectorSnapshot = await firestore - .pipeline() - .collection(_collectionPath) - .where(Expression.field('runId').equal(runId)) - .findNearest( - vectorField: 'embedding', - queryVector: const [1.0, 0.0, 0.0], - distanceMeasure: DistanceMeasure.cosine, - limit: 1, - distanceResultField: 'distance', - ) - .execute(); - - expect(vectorSnapshot.results, hasLength(1)); - expect(vectorSnapshot.results.single.get('title'), 'Dart Pipelines'); - }); - }); -} - -const _skipReason = - 'Set both FIRESTORE_PIPELINE_E2E_PROJECT_ID and ' - 'FIRESTORE_PIPELINE_E2E_DATABASE_ID to run against a real Firebase ' - 'project. The database must be Enterprise edition; Pipelines are not ' - 'available on Standard. CI authenticates with Application Default ' - 'Credentials, for example via Workload Identity Federation.'; - -PipelineBooleanExpression _runFilter( - String runId, [ - PipelineBooleanExpression? condition, -]) { - return PipelineFunctions.and([ - Expression.field('runId').equal(runId), - ?condition, - ]); -} - -final _aggregateScenarios = <_FunctionScenario>[ - _FunctionScenario('aggregate functions', [ - _FunctionExpectation('count', PipelineFunctions.count(), 3), - _FunctionExpectation( - 'countIf', - PipelineFunctions.countIf(Expression.field('active')), - 2, - ), - _FunctionExpectation( - 'countDistinct', - Expression.field('metadata').mapGetLiteral('lang').countDistinct(), - 1, - ), - _FunctionExpectation('sum', Expression.field('price').sum(), 60), - _FunctionExpectation( - 'average', - Expression.field('rating').average(), - closeTo(11 / 3, 0.0001), - ), - _FunctionExpectation('minimum', Expression.field('price').minimum(), 10), - _FunctionExpectation('maximum', Expression.field('price').maximum(), 30), - _FunctionExpectation( - 'first', - Expression.field('title').first(), - isA(), - ), - _FunctionExpectation( - 'last', - Expression.field('title').last(), - isA(), - ), - _FunctionExpectation( - 'arrayAgg', - Expression.field('title').arrayAgg(), - containsAll(['Dart Pipelines', 'Firestore Admin', 'Inactive Draft']), - ), - _FunctionExpectation( - 'arrayAggDistinct', - Expression.field('metadata').mapGetLiteral('lang').arrayAggDistinct(), - ['dart'], - ), - ]), -]; - -final _functionScenarios = <_FunctionScenario>[ - _FunctionScenario('arithmetic functions', [ - _FunctionExpectation( - 'abs', - Expression.field('score').abs(), - closeTo(12.7, 0.0001), - ), - _FunctionExpectation('add', Expression.field('price').add(2), 12), - _FunctionExpectation('subtract', Expression.field('price').subtract(3), 7), - _FunctionExpectation('multiply', Expression.field('price').multiply(2), 20), - _FunctionExpectation('divide', Expression.field('price').divide(2), 5), - _FunctionExpectation('mod', Expression.field('price').mod(4), 2), - _FunctionExpectation('ceil', Expression.constant(12.2).ceil(), 13), - _FunctionExpectation('floor', Expression.constant(12.8).floor(), 12), - _FunctionExpectation('round', Expression.constant(12.6).round(), 13), - _FunctionExpectation('trunc', Expression.constant(12.8).trunc(), 12), - _FunctionExpectation( - 'roundDecimalPlaces', - Expression.constant(12.68).round(1), - closeTo(12.7, 0.0001), - ), - _FunctionExpectation( - 'truncDecimalPlaces', - Expression.constant(12.89).trunc(1), - closeTo(12.8, 0.0001), - ), - _FunctionExpectation('pow', PipelineFunctions.pow(2, 3), 8), - _FunctionExpectation('sqrt', Expression.constant(9).sqrt(), 3), - _FunctionExpectation( - 'exp', - PipelineFunctions.exp(1), - closeTo(2.71828, 0.001), - ), - _FunctionExpectation( - 'ln', - PipelineFunctions.ln(2.718281828), - closeTo(1, 0.001), - ), - _FunctionExpectation('log', PipelineFunctions.log(8, 2), closeTo(3, 0.001)), - _FunctionExpectation( - 'log10', - PipelineFunctions.log10(100), - closeTo(2, 0.001), - ), - _FunctionExpectation('rand', PipelineFunctions.rand(), isA()), - ]), - _FunctionScenario('array functions', [ - _FunctionExpectation('array', Expression.array([1, 2, 3]), [1, 2, 3]), - _FunctionExpectation( - 'arrayConcat', - Expression.field('tags').arrayConcat(['admin']), - ['dart', 'firebase', 'admin'], - ), - _FunctionExpectation( - 'arrayConcatMultiple', - Expression.field('tags').arrayConcatMultiple([ - ['admin'], - ['sdk'], - ]), - ['dart', 'firebase', 'admin', 'sdk'], - ), - _FunctionExpectation( - 'arrayContainsValue', - Expression.field('tags').arrayContains('dart'), - true, - ), - _FunctionExpectation( - 'arrayContainsElement', - Expression.field('tags').arrayContains(Expression.constant('firebase')), - true, - ), - _FunctionExpectation( - 'arrayContainsAll', - Expression.field('tags').arrayContainsAll(['dart', 'firebase']), - true, - ), - _FunctionExpectation( - 'arrayContainsAllFrom', - Expression.field('tags').arrayContainsAllFrom(Expression.array(['dart'])), - true, - ), - _FunctionExpectation( - 'arrayContainsAny', - Expression.field('tags').arrayContainsAny(['missing', 'dart']), - true, - ), - _FunctionExpectation( - 'arrayFilter', - Expression.field( - 'tags', - ).arrayFilter('tag', Expression.variable('tag').notEqual('firebase')), - ['dart'], - ), - _FunctionExpectation( - 'arrayGet', - PipelineFunctions.arrayGet(Expression.field('numbers'), 1), - 1, - ), - _FunctionExpectation( - 'arrayLength', - Expression.field('numbers').arrayLength(), - 4, - ), - _FunctionExpectation( - 'arrayReverse', - Expression.field('numbers').arrayReverse(), - [3, 2, 1, 3], - ), - _FunctionExpectation( - 'arrayFirst', - Expression.field('numbers').arrayFirst(), - 3, - ), - _FunctionExpectation( - 'arrayFirstN', - Expression.field('numbers').arrayFirstN(2), - [3, 1], - ), - _FunctionExpectation( - 'arrayIndexOf', - Expression.field('numbers').arrayIndexOf(1), - 1, - ), - _FunctionExpectation( - 'arrayIndexOfAll', - Expression.field('numbers').arrayIndexOfAll(3), - [0, 3], - ), - _FunctionExpectation( - 'arrayLast', - Expression.field('numbers').arrayLast(), - 3, - ), - _FunctionExpectation( - 'arrayLastN', - Expression.field('numbers').arrayLastN(2), - [2, 3], - ), - _FunctionExpectation( - 'arrayLastIndexOf', - Expression.field('numbers').arrayLastIndexOf(3), - 3, - ), - _FunctionExpectation( - 'arraySlice', - Expression.field('numbers').arraySlice(1, 2), - [1, 2], - ), - _FunctionExpectation( - 'arrayTransform', - Expression.field( - 'numbers', - ).arrayTransform('n', Expression.variable('n').add(1)), - [4, 2, 3, 4], - ), - _FunctionExpectation( - 'arrayTransformWithIndex', - Expression.field('numbers').arrayTransformWithIndex( - 'n', - 'i', - Expression.variable('n').add(Expression.variable('i')), - ), - [3, 2, 4, 6], - ), - _FunctionExpectation( - 'arrayMaximum', - Expression.field('numbers').arrayMaximum(), - 3, - ), - _FunctionExpectation( - 'maximumN', - Expression.field('numbers').arrayMaximumN(2), - [3, 3], - ), - _FunctionExpectation( - 'arrayMinimum', - Expression.field('numbers').arrayMinimum(), - 1, - ), - _FunctionExpectation( - 'minimumN', - Expression.field('numbers').arrayMinimumN(2), - [1, 2], - ), - _FunctionExpectation('arraySum', Expression.field('numbers').arraySum(), 9), - // The static helpers must emit the same backend names as the fluent - // forms (`maximum`, not `array_maximum`). - _FunctionExpectation( - 'staticArrayMaximum', - PipelineFunctions.arrayMaximum('numbers'), - 3, - ), - _FunctionExpectation( - 'staticArrayMaximumN', - PipelineFunctions.arrayMaximumN('numbers', 2), - [3, 3], - ), - _FunctionExpectation( - 'staticArrayMinimum', - PipelineFunctions.arrayMinimum('numbers'), - 1, - ), - _FunctionExpectation( - 'staticArrayMinimumN', - PipelineFunctions.arrayMinimumN('numbers', 2), - [1, 2], - ), - _FunctionExpectation( - 'staticArraySum', - PipelineFunctions.arraySum('numbers'), - 9, - ), - _FunctionExpectation( - 'join', - Expression.field('words').joinLiteral('-'), - 'dart-firebase', - ), - _FunctionExpectation( - 'joinStatic', - PipelineFunctions.join('words', ', '), - 'dart, firebase', - ), - ]), - _FunctionScenario('comparison functions', [ - _FunctionExpectation('equal', Expression.field('price').equal(10), true), - _FunctionExpectation( - 'notEqual', - Expression.field('price').notEqual(11), - true, - ), - _FunctionExpectation( - 'greaterThan', - Expression.field('price').greaterThan(9), - true, - ), - _FunctionExpectation( - 'greaterThanOrEqual', - Expression.field('price').greaterThanOrEqual(10), - true, - ), - _FunctionExpectation( - 'lessThan', - Expression.field('price').lessThan(11), - true, - ), - _FunctionExpectation( - 'lessThanOrEqual', - Expression.field('price').lessThanOrEqual(10), - true, - ), - _FunctionExpectation( - 'cmp', - PipelineFunctions.cmp(Expression.field('price'), 10), - 0, - ), - ]), - _FunctionScenario('debugging functions', [ - _FunctionExpectation('exists', Expression.field('title').exists(), true), - _FunctionExpectation( - 'isAbsent', - Expression.field('missing').isAbsent(), - true, - ), - _FunctionExpectation( - 'ifAbsent', - Expression.field('missing').ifAbsent('fallback'), - 'fallback', - ), - _FunctionExpectation('isError', Expression.field('title').isError(), false), - _FunctionExpectation( - 'ifError', - Expression.field('title').ifError('caught'), - 'Dart Pipelines', - ), - ]), - _FunctionScenario('reference functions', [ - _FunctionExpectation( - 'collectionId', - Expression.field('pathRef').collectionId(), - isA(), - ), - _FunctionExpectation( - 'documentId', - Expression.field('pathRef').documentId(), - allOf(startsWith('run_'), endsWith('_book_2')), - ), - _FunctionExpectation( - 'parent', - Expression.field('pathRef').parent(), - isNotNull, - ), - _FunctionExpectation( - 'referenceSlice', - Expression.field('pathRef').referenceSlice(0, 2), - isNotNull, - ), - ]), - _FunctionScenario('logical functions', [ - _FunctionExpectation( - 'and', - PipelineFunctions.and([true, Expression.field('active')]), - true, - ), - _FunctionExpectation( - 'or', - PipelineFunctions.or([false, Expression.field('active')]), - true, - ), - _FunctionExpectation('xor', PipelineFunctions.xor([true, false]), true), - _FunctionExpectation('nor', PipelineFunctions.nor([false, false]), true), - _FunctionExpectation('not', PipelineFunctions.not(false), true), - _FunctionExpectation( - 'conditional', - PipelineFunctions.conditional(Expression.field('active'), 'yes', 'no'), - 'yes', - ), - _FunctionExpectation( - 'ifNull', - PipelineFunctions.ifNull(Expression.field('nullable'), 'fallback'), - 'fallback', - ), - // The first argument is a field position: a bare String means a field - // reference, not a string literal. Passing 'dart' here asked about a - // non-existent field named `dart`, which made equalAny false and made - // notEqualAny true for the wrong reason. - _FunctionExpectation( - 'equalAny', - PipelineFunctions.equalAny( - 'title', - Expression.array(['Dart Pipelines', 'Unrelated Title']), - ), - true, - ), - _FunctionExpectation( - 'notEqualAny', - PipelineFunctions.notEqualAny( - 'title', - Expression.array(['Unrelated Title', 'Another Title']), - ), - true, - ), - ]), - _FunctionScenario('map functions', [ - _FunctionExpectation('map', PipelineFunctions.map(['a', 1, 'b', 2]), { - 'a': 1, - 'b': 2, - }), - _FunctionExpectation( - 'mapGet', - Expression.field('metadata').mapGetLiteral('category'), - 'sdk', - ), - _FunctionExpectation( - 'mapSet', - Expression.field('metadata').mapSet('edition', 'enterprise'), - containsPair('edition', 'enterprise'), - ), - _FunctionExpectation( - 'mapRemove', - Expression.field('metadata').mapRemove('lang'), - isNot(contains('lang')), - ), - _FunctionExpectation( - 'mapRemoveExpressionKey', - Expression.field('metadata').mapRemove(Expression.constant('lang')), - isNot(contains('lang')), - ), - _FunctionExpectation( - 'mapRemoveChained', - Expression.field('metadata').mapRemove('lang').mapRemove('category'), - isEmpty, - ), - _FunctionExpectation( - 'mapMerge', - Expression.field('metadata').mapMerge([ - {'edition': 'enterprise'}, - ]), - containsPair('edition', 'enterprise'), - ), - _FunctionExpectation( - 'currentDocument', - currentDocument(), - isA>(), - ), - _FunctionExpectation( - 'mapKeys', - Expression.field('metadata').mapKeys(), - containsAll(['lang', 'category']), - ), - _FunctionExpectation( - 'mapValues', - Expression.field('metadata').mapValues(), - containsAll(['dart', 'sdk']), - ), - _FunctionExpectation( - 'mapEntries', - Expression.field('metadata').mapEntries(), - isA>(), - ), - ]), - // Collections that hold expressions must be built with array(...) / - // map(...); the backend rejects them inside a literal array or map value. - _FunctionScenario('collections holding expressions', [ - _FunctionExpectation( - 'equalAny', - PipelineFunctions.equalAny('rating', [Expression.field('score'), 5]), - true, - ), - _FunctionExpectation( - 'equalAnyMatchesExpression', - Expression.field( - 'price', - ).equalAny([Expression.field('rating').multiply(2), 3]), - true, - ), - _FunctionExpectation( - 'notEqualAny', - PipelineFunctions.notEqualAny('rating', [Expression.field('price'), 4]), - true, - ), - _FunctionExpectation( - 'arrayContainsAll', - PipelineFunctions.arrayContainsAll('tags', [ - Expression.field('metadata').mapGetLiteral('lang'), - 'firebase', - ]), - true, - ), - _FunctionExpectation( - 'arrayContainsAny', - Expression.field('tags').arrayContainsAny([ - Expression.field('metadata').mapGetLiteral('lang'), - 'missing', - ]), - true, - ), - _FunctionExpectation( - 'arrayConcat', - Expression.field('tags').arrayConcat([Expression.field('title')]), - ['dart', 'firebase', 'Dart Pipelines'], - ), - _FunctionExpectation( - 'mapMerge', - Expression.field('metadata').mapMerge([ - {'title': Expression.field('title')}, - ]), - containsPair('title', 'Dart Pipelines'), - ), - _FunctionExpectation( - 'equal', - Expression.field( - 'tags', - ).equal([Expression.field('metadata').mapGetLiteral('lang'), 'firebase']), - true, - ), - _FunctionExpectation( - 'nestedArray', - Expression.array([ - 1, - [Expression.field('price')], - ]), - [ - 1, - [10], - ], - ), - _FunctionExpectation( - 'nestedMap', - PipelineFunctions.map([ - 'nested', - {'price': Expression.field('price')}, - ]), - { - 'nested': {'price': 10}, - }, - ), - ]), - _FunctionScenario('string functions', [ - _FunctionExpectation( - 'byteLength', - Expression.field('title').byteLength(), - 14, - ), - _FunctionExpectation('charLength', Expression.field('title').length(), 14), - _FunctionExpectation( - 'startsWith', - Expression.field('title').startsWith('Dart'), - true, - ), - _FunctionExpectation( - 'endsWith', - Expression.field('title').endsWith('lines'), - true, - ), - _FunctionExpectation('like', Expression.field('title').like('Dart%'), true), - _FunctionExpectation( - 'regexContains', - Expression.field('title').regexContains('Pipe'), - true, - ), - _FunctionExpectation( - 'regexMatch', - Expression.field('title').regexMatch(r'^Dart.*'), - true, - ), - _FunctionExpectation( - 'regexFind', - Expression.field('title').regexFind('Pipe'), - isNotNull, - ), - _FunctionExpectation( - 'regexFindAll', - Expression.field('title').regexFindAll('[aei]'), - isA>(), - ), - _FunctionExpectation( - 'stringConcat', - Expression.field('title').concat([' v2']), - 'Dart Pipelines v2', - ), - _FunctionExpectation( - 'stringContains', - Expression.field('title').stringContains('Pipeline'), - true, - ), - _FunctionExpectation( - 'stringIndexOf', - Expression.field('title').stringIndexOf('Pipeline'), - 5, - ), - _FunctionExpectation( - 'toUpper', - Expression.field('title').toUpperCase(), - 'DART PIPELINES', - ), - _FunctionExpectation( - 'toLower', - Expression.field('title').toLowerCase(), - 'dart pipelines', - ), - _FunctionExpectation( - 'substring', - Expression.field('title').substringLiteral(0, 4), - 'Dart', - ), - // The second argument is a length, not an end index. - _FunctionExpectation( - 'substringLength', - Expression.field('title').substring(5, 3), - 'Pip', - ), - _FunctionExpectation( - 'substringToEnd', - Expression.field('title').substring(5), - 'Pipelines', - ), - _FunctionExpectation( - 'stringReverse', - PipelineFunctions.stringReverse(Expression.constant('Dart')), - 'traD', - ), - _FunctionExpectation( - 'stringRepeat', - Expression.constant('ha').stringRepeat(3), - 'hahaha', - ), - _FunctionExpectation( - 'stringReplaceAll', - Expression.field('title').stringReplaceAllLiteral('i', 'I'), - 'Dart PIpelInes', - ), - _FunctionExpectation( - 'stringReplaceOne', - Expression.field('title').stringReplaceOneLiteral('i', 'I'), - 'Dart PIpelines', - ), - _FunctionExpectation('trim', Expression.field('spaced').trim(), 'Dart'), - _FunctionExpectation('ltrim', Expression.field('spaced').ltrim(), 'Dart '), - _FunctionExpectation('rtrim', Expression.field('spaced').rtrim(), ' Dart'), - _FunctionExpectation('split', Expression.field('csv').splitLiteral(','), [ - 'dart', - 'firebase', - 'admin', - ]), - _FunctionExpectation('splitStatic', PipelineFunctions.split('csv', ','), [ - 'dart', - 'firebase', - 'admin', - ]), - ]), - _FunctionScenario('timestamp and type functions', [ - _FunctionExpectation( - 'currentTimestamp', - PipelineFunctions.currentTimestamp(), - isA(), - ), - _FunctionExpectation( - 'timestampTrunc', - Expression.field('createdAt').timestampTruncate('day', 'UTC'), - isA(), - ), - _FunctionExpectation( - 'unixMicrosToTimestamp', - PipelineFunctions.unixMicrosToTimestamp(1700000000000000), - isA(), - ), - _FunctionExpectation( - 'unixMillisToTimestamp', - PipelineFunctions.unixMillisToTimestamp(1700000000000), - isA(), - ), - _FunctionExpectation( - 'unixSecondsToTimestamp', - PipelineFunctions.unixSecondsToTimestamp(1700000000), - isA(), - ), - _FunctionExpectation( - 'timestampAdd', - Expression.field('createdAt').timestampAdd('second', 60), - isA(), - ), - _FunctionExpectation( - 'timestampSubtract', - Expression.field('createdAt').timestampSubtract('second', 60), - isA(), - ), - _FunctionExpectation( - 'timestampToUnixMicros', - Expression.field('createdAt').timestampToUnixMicros(), - 1700000000000000, - ), - _FunctionExpectation( - 'timestampToUnixMillis', - Expression.field('createdAt').timestampToUnixMillis(), - 1700000000000, - ), - _FunctionExpectation( - 'timestampToUnixSeconds', - Expression.field('createdAt').timestampToUnixSeconds(), - 1700000000, - ), - _FunctionExpectation( - 'timestampDiff', - PipelineFunctions.timestampDiff( - Expression.field('createdAt'), - PipelineFunctions.unixSecondsToTimestamp(1699999940), - 'second', - ), - 60, - ), - _FunctionExpectation( - 'timestampExtract', - Expression.field('createdAt').timestampExtract('year', 'UTC'), - 2023, - ), - _FunctionExpectation( - 'type', - Expression.field('price').type(), - isA(), - ), - _FunctionExpectation( - 'isType', - Expression.field('price').isType(PipelineValueType.number), - true, - ), - _FunctionExpectation( - 'isTypeInt64', - Expression.field('price').isType(PipelineValueType.int64), - true, - ), - _FunctionExpectation( - 'isTypeFloat64', - Expression.field('score').isType(PipelineValueType.double), - true, - ), - _FunctionExpectation( - 'isTypeNotFloat64', - Expression.field('price').isType(PipelineValueType.double), - false, - ), - ]), - _FunctionScenario('vector functions', [ - _FunctionExpectation( - 'cosineDistance', - Expression.field( - 'embedding', - ).cosineDistance(Expression.vector([1, 0, 0])), - closeTo(0, 0.0001), - ), - _FunctionExpectation( - 'dotProduct', - Expression.field('embedding').dotProduct(Expression.vector([1, 0, 0])), - closeTo(1, 0.0001), - ), - _FunctionExpectation( - 'euclideanDistance', - Expression.field( - 'embedding', - ).euclideanDistance(Expression.vector([1, 0, 0])), - closeTo(0, 0.0001), - ), - _FunctionExpectation( - 'vectorLength', - Expression.field('embedding').vectorLength(), - 3, - ), - ]), - // A plain list of numbers must reach the backend as a vector, not an array. - _FunctionScenario('vector functions with list arguments', [ - _FunctionExpectation( - 'cosineDistance', - Expression.field('embedding').cosineDistance(const [1.0, 0.0, 0.0]), - closeTo(0, 0.0001), - ), - _FunctionExpectation( - 'dotProduct', - PipelineFunctions.dotProduct('embedding', const [1, 0, 0]), - closeTo(1, 0.0001), - ), - _FunctionExpectation( - 'euclideanDistance', - Expression.field('embedding').euclideanDistance(const [1.0, 0.0, 0.0]), - closeTo(0, 0.0001), - ), - ]), -]; - -final class _FunctionScenario { - const _FunctionScenario(this.name, this.expectations); - - final String name; - final List<_FunctionExpectation> expectations; -} - -final class _FunctionExpectation { - const _FunctionExpectation(this.alias, this.expression, this.expected); - - final String alias; - final PipelineExpression expression; - final Object? expected; -} - -void _expectPipelineValue( - Object? actual, - Object? expected, { - required String reason, -}) { - if (expected is Matcher) { - expect(actual, expected, reason: reason); - return; - } - expect(actual, expected, reason: reason); -} diff --git a/packages/google_cloud_firestore/test/fixtures/node_pipeline_api.json b/packages/google_cloud_firestore/test/fixtures/node_pipeline_api.json new file mode 100644 index 00000000..694f6d2e --- /dev/null +++ b/packages/google_cloud_firestore/test/fixtures/node_pipeline_api.json @@ -0,0 +1,5023 @@ +{ + "classes": { + "AggregateFunction": { + "abstract": false, + "constructors": [ + { + "params": [ + {"kinds":["string"],"name":"name","type":"string"}, + {"elements":{"kinds":["expression"]},"kinds":["array"],"name":"params","type":"Expression[]"} + ], + "returns": {"kinds":["any"],"type":null} + } + ], + "extends": null, + "implements": [], + "methods": { + "as": { + "overloads": [ + { + "params": [{"kinds":["string"],"name":"name","type":"string"}], + "returns": {"kinds":["aliasedAggregate"],"type":"AliasedAggregate"} + } + ], + "static": false + } + }, + "properties": { + "expressionType": { + "aliases": ["ExpressionType"], + "kinds": ["literal"], + "optional": false, + "readonly": false, + "static": false, + "type": "ExpressionType" + } + } + }, + "AliasedAggregate": {"abstract":false,"extends":null,"implements":[],"methods":{},"properties":{}}, + "AliasedExpression": { + "abstract": false, + "extends": null, + "implements": ["Selectable"], + "methods": {}, + "properties": { + "expressionType": { + "aliases": ["ExpressionType"], + "kinds": ["literal"], + "optional": false, + "readonly": false, + "static": false, + "type": "ExpressionType" + }, + "selectable": {"kinds":["boolean"],"optional":false,"readonly":false,"static":false,"type":"true"} + } + }, + "BooleanExpression": { + "abstract": true, + "extends": "Expression", + "implements": [], + "methods": { + "conditional": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"thenExpr","type":"Expression"}, + {"kinds":["expression"],"name":"elseExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "countIf": { + "overloads": [{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}], + "static": false + }, + "ifError": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"catchValue","type":"unknown"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["boolean"],"name":"catchValue","type":"boolean"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["booleanExpression"],"name":"catchValue","type":"BooleanExpression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"catchValue","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "not": { + "overloads": [{"params":[],"returns":{"kinds":["booleanExpression"],"type":"BooleanExpression"}}], + "static": false + } + }, + "properties": {} + }, + "Constant": { + "abstract": false, + "extends": "Expression", + "implements": [], + "methods": {}, + "properties": { + "expressionType": { + "aliases": ["ExpressionType"], + "kinds": ["literal"], + "optional": false, + "readonly": true, + "static": false, + "type": "ExpressionType" + } + } + }, + "ExplainStats": { + "abstract": false, + "extends": null, + "implements": [], + "methods": {}, + "properties": { + "rawData": { + "kinds": ["map"], + "optional": false, + "readonly": true, + "static": false, + "type": "{type_url?: string | null; value?: Uint8Array | null;}" + }, + "text": {"kinds":["string"],"optional":false,"readonly":true,"static":false,"type":"string"} + } + }, + "Expression": { + "abstract": true, + "extends": null, + "implements": [], + "methods": { + "abs": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "add": { + "overloads": [ + { + "params": [ + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arrayAgg": { + "overloads": [{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}], + "static": false + }, + "arrayAggDistinct": { + "overloads": [{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}], + "static": false + }, + "arrayConcat": { + "overloads": [ + { + "params": [ + { + "elements": {"kinds":["any"]}, + "kinds": ["array","expression"], + "name": "secondArray", + "type": "Expression | unknown[]" + }, + { + "elements": {"kinds":["any"]}, + "kinds": ["array","expression"], + "name": "otherArrays", + "rest": true, + "type": "Array" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arrayContains": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "arrayContainsAll": { + "overloads": [ + { + "params": [ + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"arrayExpression","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "arrayContainsAny": { + "overloads": [ + { + "params": [ + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"arrayExpression","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "arrayFilter": { + "overloads": [ + { + "params": [ + {"kinds":["string"],"name":"alias","type":"string"}, + {"kinds":["booleanExpression"],"name":"filter","type":"BooleanExpression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arrayFirst": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "arrayFirstN": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"n","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["number"],"name":"n","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arrayGet": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"indexExpr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["number"],"name":"index","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arrayIndexOf": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"search","type":"unknown"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"search","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arrayIndexOfAll": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"search","type":"unknown"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"search","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arrayLast": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "arrayLastIndexOf": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"search","type":"unknown"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"search","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arrayLastN": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"n","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["number"],"name":"n","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arrayLength": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "arrayMaximum": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "arrayMaximumN": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"n","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["number"],"name":"n","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arrayMinimum": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "arrayMinimumN": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"n","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["number"],"name":"n","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arraySlice": { + "overloads": [ + { + "params": [ + {"kinds":["expression","number"],"name":"offset","type":"number | Expression"}, + {"kinds":["expression","number"],"name":"length","optional":true,"type":"number | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arraySum": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "arrayTransform": { + "overloads": [ + { + "params": [ + {"kinds":["string"],"name":"elementAlias","type":"string"}, + {"kinds":["expression"],"name":"transform","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "arrayTransformWithIndex": { + "overloads": [ + { + "params": [ + {"kinds":["string"],"name":"elementAlias","type":"string"}, + {"kinds":["string"],"name":"indexAlias","type":"string"}, + {"kinds":["expression"],"name":"transform","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "as": { + "overloads": [ + { + "params": [{"kinds":["string"],"name":"name","type":"string"}], + "returns": {"kinds":["aliasedExpression"],"type":"AliasedExpression"} + } + ], + "static": false + }, + "asBoolean": { + "overloads": [{"params":[],"returns":{"kinds":["booleanExpression"],"type":"BooleanExpression"}}], + "static": false + }, + "ascending": {"overloads":[{"params":[],"returns":{"kinds":["ordering"],"type":"Ordering"}}],"static":false}, + "average": { + "overloads": [{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}], + "static": false + }, + "byteLength": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "ceil": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "charLength": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "coalesce": { + "overloads": [ + { + "params": [ + {"kinds":["any","expression"],"name":"replacement","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "collectionId": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "cosineDistance": { + "overloads": [ + { + "params": [ + { + "elements": {"kinds":["number"]}, + "kinds": ["array","vectorValue"], + "name": "vector", + "type": "VectorValue | number[]" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"vectorExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "count": { + "overloads": [{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}], + "static": false + }, + "countDistinct": { + "overloads": [{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}], + "static": false + }, + "descending": {"overloads":[{"params":[],"returns":{"kinds":["ordering"],"type":"Ordering"}}],"static":false}, + "divide": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"divisor","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["number"],"name":"divisor","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "documentId": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "dotProduct": { + "overloads": [ + { + "params": [ + { + "elements": {"kinds":["number"]}, + "kinds": ["array","vectorValue"], + "name": "vector", + "type": "VectorValue | number[]" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"vectorExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "endsWith": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"suffix","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"suffix","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "equal": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "equalAny": { + "overloads": [ + { + "params": [ + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"arrayExpression","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "euclideanDistance": { + "overloads": [ + { + "params": [ + { + "elements": {"kinds":["number"]}, + "kinds": ["array","vectorValue"], + "name": "vector", + "type": "VectorValue | number[]" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"vectorExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "exists": { + "overloads": [{"params":[],"returns":{"kinds":["booleanExpression"],"type":"BooleanExpression"}}], + "static": false + }, + "exp": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "first": { + "overloads": [{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}], + "static": false + }, + "floor": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "getField": { + "overloads": [ + { + "params": [{"kinds":["expression","string"],"name":"key","type":"string | Expression"}], + "returns": {"kinds":["expression"],"type":"Expression"} + } + ], + "static": false + }, + "greaterThan": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "greaterThanOrEqual": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "ifAbsent": { + "overloads": [ + { + "params": [{"kinds":["any","expression"],"name":"elseValueOrExpression","type":"Expression | unknown"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["any"],"name":"elseExpression","type":"unknown"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["any"],"name":"elseValue","type":"unknown"}], + "returns": {"kinds":["expression"],"type":"Expression"} + } + ], + "static": false + }, + "ifError": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"catchValue","type":"unknown"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"catchExpr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "ifNull": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"elseValue","type":"unknown"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"elseExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "isAbsent": { + "overloads": [{"params":[],"returns":{"kinds":["booleanExpression"],"type":"BooleanExpression"}}], + "static": false + }, + "isError": { + "overloads": [{"params":[],"returns":{"kinds":["booleanExpression"],"type":"BooleanExpression"}}], + "static": false + }, + "isType": { + "overloads": [ + { + "params": [{"kinds":["string"],"name":"type","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "join": { + "overloads": [ + { + "params": [{"kinds":["expression","string"],"name":"delimeterValueOrExpression","type":"string | Expression"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["expression"],"name":"delimiterExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["string"],"name":"delimiter","type":"string"}], + "returns": {"kinds":["expression"],"type":"Expression"} + } + ], + "static": false + }, + "last": { + "overloads": [{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}], + "static": false + }, + "length": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "lessThan": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "lessThanOrEqual": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "like": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"pattern","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"pattern","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "ln": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "log10": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "logicalMaximum": { + "overloads": [ + { + "params": [ + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "logicalMinimum": { + "overloads": [ + { + "params": [ + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "ltrim": { + "overloads": [ + { + "params": [ + { + "kinds": ["bytes","expression","string"], + "name": "valueToTrim", + "optional": true, + "type": "string | Expression | Uint8Array | Buffer" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "mapEntries": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "mapGet": { + "overloads": [ + { + "params": [{"kinds":["string"],"name":"subfield","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "mapKeys": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "mapMerge": { + "overloads": [ + { + "params": [ + {"kinds":["expression","map"],"name":"secondMap","type":"Record | Expression"}, + { + "kinds": ["expression","map"], + "name": "otherMaps", + "rest": true, + "type": "Array | Expression>" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "mapRemove": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"keyExpr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"key","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "mapSet": { + "overloads": [ + { + "params": [ + {"kinds":["expression","string"],"name":"key","type":"string | Expression"}, + {"kinds":["any"],"name":"value","type":"unknown"}, + {"kinds":["any"],"name":"moreKeyValues","rest":true,"type":"unknown[]"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "mapValues": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "maximum": { + "overloads": [{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}], + "static": false + }, + "minimum": { + "overloads": [{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}], + "static": false + }, + "mod": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["number"],"name":"value","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "multiply": { + "overloads": [ + { + "params": [ + {"kinds":["expression","number"],"name":"second","type":"Expression | number"}, + {"kinds":["expression","number"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "notEqual": { + "overloads": [ + { + "params": [{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "notEqualAny": { + "overloads": [ + { + "params": [ + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"arrayExpression","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "parent": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "pow": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"exponent","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["number"],"name":"exponent","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "regexContains": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"pattern","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"pattern","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "regexFind": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"pattern","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"pattern","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "regexFindAll": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"pattern","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"pattern","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "regexMatch": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"pattern","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"pattern","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "reverse": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "round": { + "overloads": [ + {"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}, + { + "params": [{"kinds":["expression"],"name":"decimalPlaces","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["number"],"name":"decimalPlaces","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "rtrim": { + "overloads": [ + { + "params": [ + { + "kinds": ["bytes","expression","string"], + "name": "valueToTrim", + "optional": true, + "type": "string | Expression | Uint8Array | Buffer" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "split": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"delimiter","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"delimiter","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "sqrt": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "startsWith": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"prefix","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"prefix","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "stringConcat": { + "overloads": [ + { + "params": [ + {"kinds":["expression","string"],"name":"secondString","type":"Expression | string"}, + {"kinds":["expression","string"],"name":"otherStrings","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "stringContains": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"substring","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ], + "static": false + }, + "stringIndexOf": { + "overloads": [ + { + "params": [ + { + "kinds": ["bytes","expression","string"], + "name": "search", + "type": "string | Expression | Uint8Array | Buffer" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "stringRepeat": { + "overloads": [ + { + "params": [{"kinds":["expression","number"],"name":"repetitions","type":"number | Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "stringReplaceAll": { + "overloads": [ + { + "params": [ + { + "kinds": ["bytes","expression","string"], + "name": "find", + "type": "string | Expression | Uint8Array | Buffer" + }, + { + "kinds": ["bytes","expression","string"], + "name": "replacement", + "type": "string | Expression | Uint8Array | Buffer" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "stringReplaceOne": { + "overloads": [ + { + "params": [ + { + "kinds": ["bytes","expression","string"], + "name": "find", + "type": "string | Expression | Uint8Array | Buffer" + }, + { + "kinds": ["bytes","expression","string"], + "name": "replacement", + "type": "string | Expression | Uint8Array | Buffer" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "stringReverse": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "substring": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"position","type":"Expression"}, + {"kinds":["expression"],"name":"length","optional":true,"type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["number"],"name":"position","type":"number"}, + {"kinds":["number"],"name":"length","optional":true,"type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "subtract": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"subtrahend","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["number"],"name":"subtrahend","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "sum": { + "overloads": [{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}], + "static": false + }, + "timestampAdd": { + "overloads": [ + { + "params": [ + {"aliases":["TimeUnit"],"kinds":["literal"],"name":"unit","type":"TimeUnit"}, + {"kinds":["number"],"name":"amount","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"unit","type":"Expression"}, + {"kinds":["expression"],"name":"amount","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "timestampDiff": { + "overloads": [ + { + "params": [ + {"kinds":["expression","string"],"name":"start","type":"string | Expression"}, + {"aliases":["TimeUnit"],"kinds":["literal"],"name":"unit","type":"TimeUnit"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"start","type":"Expression"}, + {"kinds":["expression"],"name":"unit","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "timestampExtract": { + "overloads": [ + { + "params": [ + {"aliases":["TimePart"],"kinds":["literal"],"name":"part","type":"TimePart"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"part","type":"Expression"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "timestampSubtract": { + "overloads": [ + { + "params": [ + {"aliases":["TimeUnit"],"kinds":["literal"],"name":"unit","type":"TimeUnit"}, + {"kinds":["number"],"name":"amount","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"unit","type":"Expression"}, + {"kinds":["expression"],"name":"amount","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "timestampToUnixMicros": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "timestampToUnixMillis": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "timestampToUnixSeconds": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "timestampTruncate": { + "overloads": [ + { + "params": [ + {"aliases":["TimeGranularity"],"kinds":["literal"],"name":"granularity","type":"TimeGranularity"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"granularity","type":"Expression"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "toLower": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "toUpper": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "trim": { + "overloads": [ + { + "params": [ + { + "kinds": ["bytes","expression","string"], + "name": "valueToTrim", + "optional": true, + "type": "string | Expression | Uint8Array | Buffer" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "trunc": { + "overloads": [ + {"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}, + { + "params": [{"kinds":["expression"],"name":"decimalPlaces","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["number"],"name":"decimalPlaces","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ], + "static": false + }, + "type": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "unixMicrosToTimestamp": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "unixMillisToTimestamp": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "unixSecondsToTimestamp": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false}, + "vectorLength": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}],"static":false} + }, + "properties": { + "expressionType": { + "aliases": ["ExpressionType"], + "kinds": ["literal"], + "optional": false, + "readonly": true, + "static": false, + "type": "ExpressionType" + } + } + }, + "Field": { + "abstract": false, + "extends": "Expression", + "implements": ["Selectable"], + "methods": { + "geoDistance": { + "overloads": [ + { + "params": [{"kinds":["expression","geoPoint"],"name":"location","type":"GeoPoint | Expression"}], + "returns": {"kinds":["expression"],"type":"Expression"} + } + ], + "static": false + } + }, + "properties": { + "expressionType": { + "aliases": ["ExpressionType"], + "kinds": ["literal"], + "optional": false, + "readonly": true, + "static": false, + "type": "ExpressionType" + }, + "fieldName": {"kinds":["string"],"optional":false,"readonly":true,"static":false,"type":"string"}, + "selectable": {"kinds":["boolean"],"optional":false,"readonly":false,"static":false,"type":"true"} + } + }, + "FunctionExpression": { + "abstract": false, + "constructors": [ + { + "params": [ + {"kinds":["string"],"name":"name","type":"string"}, + {"elements":{"kinds":["expression"]},"kinds":["array"],"name":"params","type":"Expression[]"} + ], + "returns": {"kinds":["any"],"type":null} + } + ], + "extends": "Expression", + "implements": [], + "methods": {}, + "properties": { + "expressionType": { + "aliases": ["ExpressionType"], + "kinds": ["literal"], + "optional": false, + "readonly": true, + "static": false, + "type": "ExpressionType" + } + } + }, + "Ordering": { + "abstract": false, + "extends": null, + "implements": [], + "methods": {}, + "properties": { + "direction": { + "kinds": ["literal"], + "literals": ["ascending","descending"], + "optional": false, + "readonly": true, + "static": false, + "type": "'ascending' | 'descending'" + }, + "expr": {"kinds":["expression"],"optional":false,"readonly":true,"static":false,"type":"Expression"} + } + }, + "Pipeline": { + "abstract": false, + "extends": null, + "implements": [], + "methods": { + "addFields": { + "overloads": [ + { + "params": [ + { + "kinds": ["options"], + "name": "options", + "options": ["AddFieldsStageOptions"], + "type": "AddFieldsStageOptions" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [ + {"kinds":["selectable"],"name":"field","type":"Selectable"}, + {"kinds":["selectable"],"name":"additionalFields","rest":true,"type":"Selectable[]"} + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "aggregate": { + "overloads": [ + { + "params": [ + {"kinds":["aliasedAggregate"],"name":"accumulator","type":"AliasedAggregate"}, + {"kinds":["aliasedAggregate"],"name":"additionalAccumulators","rest":true,"type":"AliasedAggregate[]"} + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [ + { + "kinds": ["options"], + "name": "options", + "options": ["AggregateStageOptions"], + "type": "AggregateStageOptions" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "define": { + "overloads": [ + { + "params": [ + {"kinds":["aliasedExpression"],"name":"aliasedExpression","type":"AliasedExpression"}, + {"kinds":["aliasedExpression"],"name":"additionalExpressions","rest":true,"type":"AliasedExpression[]"} + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [{"kinds":["options"],"name":"options","options":["DefineStageOptions"],"type":"DefineStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "delete": {"overloads":[{"params":[],"returns":{"kinds":["pipeline"],"type":"Pipeline"}}],"static":false}, + "distinct": { + "overloads": [ + { + "params": [{"kinds":["options"],"name":"options","options":["DistinctStageOptions"],"type":"DistinctStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [ + {"kinds":["selectable","string"],"name":"group","type":"string | Selectable"}, + { + "kinds": ["selectable","string"], + "name": "additionalGroups", + "rest": true, + "type": "Array" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "execute": { + "overloads": [ + { + "params": [ + { + "kinds": ["options"], + "name": "pipelineExecuteOptions", + "optional": true, + "options": ["PipelineExecuteOptions"], + "type": "PipelineExecuteOptions" + } + ], + "returns": {"elements":{"kinds":["pipelineSnapshot"]},"kinds":["promise"],"type":"Promise"} + } + ], + "static": false + }, + "findNearest": { + "overloads": [ + { + "params": [ + { + "kinds": ["options"], + "name": "options", + "options": ["FindNearestStageOptions"], + "type": "FindNearestStageOptions" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "limit": { + "overloads": [ + { + "params": [{"kinds":["number"],"name":"limit","type":"number"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [{"kinds":["options"],"name":"options","options":["LimitStageOptions"],"type":"LimitStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "offset": { + "overloads": [ + { + "params": [{"kinds":["number"],"name":"offset","type":"number"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [{"kinds":["options"],"name":"options","options":["OffsetStageOptions"],"type":"OffsetStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "rawStage": { + "overloads": [ + { + "params": [ + {"kinds":["string"],"name":"name","type":"string"}, + {"elements":{"kinds":["any"]},"kinds":["array"],"name":"params","type":"any[]"} + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "removeFields": { + "overloads": [ + { + "params": [ + {"kinds":["field","string"],"name":"fieldValue","type":"Field | string"}, + {"kinds":["field","string"],"name":"additionalFields","rest":true,"type":"Array"} + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [ + { + "kinds": ["options"], + "name": "options", + "options": ["RemoveFieldsStageOptions"], + "type": "RemoveFieldsStageOptions" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "replaceWith": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [ + { + "kinds": ["options"], + "name": "options", + "options": ["ReplaceWithStageOptions"], + "type": "ReplaceWithStageOptions" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "sample": { + "overloads": [ + { + "params": [{"kinds":["number"],"name":"documents","type":"number"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [{"kinds":["options"],"name":"options","options":["SampleStageOptions"],"type":"SampleStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "search": { + "overloads": [ + { + "params": [{"kinds":["options"],"name":"options","options":["SearchStageOptions"],"type":"SearchStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "select": { + "overloads": [ + { + "params": [{"kinds":["options"],"name":"options","options":["SelectStageOptions"],"type":"SelectStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [ + {"kinds":["selectable","string"],"name":"selection","type":"Selectable | string"}, + { + "kinds": ["selectable","string"], + "name": "additionalSelections", + "rest": true, + "type": "Array" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "sort": { + "overloads": [ + { + "params": [{"kinds":["options"],"name":"options","options":["SortStageOptions"],"type":"SortStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [ + {"kinds":["ordering"],"name":"ordering","type":"Ordering"}, + {"kinds":["ordering"],"name":"additionalOrderings","rest":true,"type":"Ordering[]"} + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "stream": { + "overloads": [{"params":[],"returns":{"kinds":["readableStream"],"type":"NodeJS.ReadableStream"}}], + "static": false + }, + "toArrayExpression": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"Expression"}}],"static":false}, + "toScalarExpression": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"Expression"}}],"static":false}, + "union": { + "overloads": [ + { + "params": [{"kinds":["options"],"name":"options","options":["UnionStageOptions"],"type":"UnionStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [{"kinds":["pipeline"],"name":"other","type":"Pipeline"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "unnest": { + "overloads": [ + { + "params": [{"kinds":["options"],"name":"options","options":["UnnestStageOptions"],"type":"UnnestStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [ + {"kinds":["selectable"],"name":"selectable","type":"Selectable"}, + {"kinds":["string"],"name":"indexField","optional":true,"type":"string"} + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "update": { + "overloads": [ + {"params":[],"returns":{"kinds":["pipeline"],"type":"Pipeline"}}, + { + "params": [ + { + "elements": {"kinds":["aliasedExpression"]}, + "kinds": ["array"], + "name": "transformedFields", + "type": "AliasedExpression[]" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "where": { + "overloads": [ + { + "params": [{"kinds":["booleanExpression"],"name":"condition","type":"BooleanExpression"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [{"kinds":["options"],"name":"options","options":["WhereStageOptions"],"type":"WhereStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + } + }, + "properties": {} + }, + "PipelineResult": { + "abstract": false, + "extends": null, + "implements": [], + "methods": { + "data": {"overloads":[{"params":[],"returns":{"kinds":["documentData"],"type":"DocumentData"}}],"static":false}, + "get": { + "overloads": [ + { + "params": [{"kinds":["fieldPath","string"],"name":"fieldPath","type":"string | FieldPath"}], + "returns": {"kinds":["any"],"type":"any"} + } + ], + "static": false + }, + "isEqual": { + "overloads": [ + { + "params": [{"kinds":["pipelineResult"],"name":"other","type":"PipelineResult"}], + "returns": {"kinds":["boolean"],"type":"boolean"} + } + ], + "static": false + } + }, + "properties": { + "createTime": { + "kinds": ["timestamp","undefined"], + "optional": false, + "readonly": true, + "static": false, + "type": "Timestamp | undefined" + }, + "id": {"kinds":["string","undefined"],"optional":false,"readonly":true,"static":false,"type":"string | undefined"}, + "ref": { + "kinds": ["documentReference","undefined"], + "optional": false, + "readonly": true, + "static": false, + "type": "DocumentReference | undefined" + }, + "updateTime": { + "kinds": ["timestamp","undefined"], + "optional": false, + "readonly": true, + "static": false, + "type": "Timestamp | undefined" + } + } + }, + "PipelineSnapshot": { + "abstract": false, + "extends": null, + "implements": [], + "methods": {}, + "properties": { + "executionTime": {"kinds":["timestamp"],"optional":false,"readonly":true,"static":false,"type":"Timestamp"}, + "explainStats": { + "kinds": ["explainStats","undefined"], + "optional": false, + "readonly": true, + "static": false, + "type": "ExplainStats | undefined" + }, + "pipeline": {"kinds":["pipeline"],"optional":false,"readonly":true,"static":false,"type":"Pipeline"}, + "results": { + "elements": {"kinds":["pipelineResult"]}, + "kinds": ["array"], + "optional": false, + "readonly": true, + "static": false, + "type": "PipelineResult[]" + } + } + }, + "PipelineSource": { + "abstract": false, + "extends": null, + "implements": [], + "methods": { + "collection": { + "overloads": [ + { + "params": [{"kinds":["collectionReference","string"],"name":"collectionPath","type":"string | CollectionReference"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [ + { + "kinds": ["options"], + "name": "options", + "options": ["CollectionStageOptions"], + "type": "CollectionStageOptions" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "collectionGroup": { + "overloads": [ + { + "params": [ + { + "kinds": ["options"], + "name": "options", + "options": ["CollectionGroupStageOptions"], + "type": "CollectionGroupStageOptions" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [{"kinds":["string"],"name":"collectionId","type":"string"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "createFrom": { + "overloads": [ + { + "params": [{"kinds":["query"],"name":"query","type":"Query"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "database": { + "overloads": [ + {"params":[],"returns":{"kinds":["pipeline"],"type":"Pipeline"}}, + { + "params": [{"kinds":["options"],"name":"options","options":["DatabaseStageOptions"],"type":"DatabaseStageOptions"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + }, + "documents": { + "overloads": [ + { + "params": [ + { + "elements": {"kinds":["documentReference","string"]}, + "kinds": ["array"], + "name": "docs", + "type": "Array" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [ + { + "kinds": ["options"], + "name": "options", + "options": ["DocumentsStageOptions"], + "type": "DocumentsStageOptions" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ], + "static": false + } + }, + "properties": {} + } + }, + "entryPoints": { + "Firestore.pipeline": {"overloads":[{"params":[],"returns":{"kinds":["pipelineSource"],"type":"Pipelines.PipelineSource"}}]}, + "Transaction.execute": { + "overloads": [ + { + "params": [{"kinds":["pipeline"],"name":"pipeline","type":"Pipelines.Pipeline"}], + "returns": {"elements":{"kinds":["pipelineSnapshot"]},"kinds":["promise"],"type":"Promise"} + } + ] + } + }, + "formatVersion": 1, + "functions": { + "abs": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "add": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"first","type":"Expression"}, + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "and": { + "overloads": [ + { + "params": [ + {"kinds":["booleanExpression"],"name":"first","type":"BooleanExpression"}, + {"kinds":["booleanExpression"],"name":"second","type":"BooleanExpression"}, + {"kinds":["booleanExpression"],"name":"more","rest":true,"type":"BooleanExpression[]"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "array": { + "overloads": [ + { + "params": [{"elements":{"kinds":["any"]},"kinds":["array"],"name":"elements","type":"unknown[]"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayAgg": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + } + ] + }, + "arrayAggDistinct": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + } + ] + }, + "arrayConcat": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"firstArray","type":"Expression"}, + { + "elements": {"kinds":["any"]}, + "kinds": ["array","expression"], + "name": "secondArray", + "type": "Expression | unknown[]" + }, + { + "elements": {"kinds":["any"]}, + "kinds": ["array","expression"], + "name": "otherArrays", + "rest": true, + "type": "Array" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"firstArrayField","type":"string"}, + { + "elements": {"kinds":["any"]}, + "kinds": ["array","expression"], + "name": "secondArray", + "type": "Expression | unknown[]" + }, + { + "elements": {"kinds":["any"]}, + "kinds": ["array","expression"], + "name": "otherArrays", + "rest": true, + "type": "Array" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayContains": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"array","type":"Expression"}, + {"kinds":["any"],"name":"element","type":"unknown"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"array","type":"Expression"}, + {"kinds":["expression"],"name":"element","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["any"],"name":"element","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"element","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "arrayContainsAll": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"array","type":"Expression"}, + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"array","type":"Expression"}, + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "arrayContainsAny": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"array","type":"Expression"}, + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"array","type":"Expression"}, + {"kinds":["expression"],"name":"values","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"values","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "arrayFilter": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["string"],"name":"alias","type":"string"}, + {"kinds":["booleanExpression"],"name":"filter","type":"BooleanExpression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["string"],"name":"alias","type":"string"}, + {"kinds":["booleanExpression"],"name":"filter","type":"BooleanExpression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayFirst": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"arrayExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayFirstN": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["expression"],"name":"n","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["number"],"name":"n","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"n","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["number"],"name":"n","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayGet": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["expression"],"name":"indexExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["number"],"name":"index","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"arrayField","type":"string"}, + {"kinds":["expression"],"name":"indexExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"arrayField","type":"string"},{"kinds":["number"],"name":"index","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayIndexOf": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["any","expression"],"name":"search","type":"unknown | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["any","expression"],"name":"search","type":"unknown | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayIndexOfAll": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["any","expression"],"name":"search","type":"unknown | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["any","expression"],"name":"search","type":"unknown | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayLast": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"arrayExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayLastIndexOf": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["any","expression"],"name":"search","type":"unknown | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["any","expression"],"name":"search","type":"unknown | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayLastN": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["expression"],"name":"n","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["number"],"name":"n","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"n","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["number"],"name":"n","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayLength": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"array","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayMaximum": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"arrayExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayMaximumN": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["expression"],"name":"n","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["number"],"name":"n","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"n","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["number"],"name":"n","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayMinimum": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"arrayExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayMinimumN": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["expression"],"name":"n","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["number"],"name":"n","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"n","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["number"],"name":"n","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayReverse": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"arrayExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arraySlice": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["expression","number"],"name":"offset","type":"number | Expression"}, + {"kinds":["expression","number"],"name":"length","optional":true,"type":"number | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"arrayName","type":"string"}, + {"kinds":["expression","number"],"name":"offset","type":"number | Expression"}, + {"kinds":["expression","number"],"name":"length","optional":true,"type":"number | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arraySum": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayTransform": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["string"],"name":"elementAlias","type":"string"}, + {"kinds":["expression"],"name":"transform","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["string"],"name":"elementAlias","type":"string"}, + {"kinds":["expression"],"name":"transform","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "arrayTransformWithIndex": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["string"],"name":"elementAlias","type":"string"}, + {"kinds":["string"],"name":"indexAlias","type":"string"}, + {"kinds":["expression"],"name":"transform","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["string"],"name":"elementAlias","type":"string"}, + {"kinds":["string"],"name":"indexAlias","type":"string"}, + {"kinds":["expression"],"name":"transform","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "ascending": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["ordering"],"type":"Ordering"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["ordering"],"type":"Ordering"} + } + ] + }, + "average": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + } + ] + }, + "byteLength": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "ceil": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "charLength": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"stringExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "coalesce": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["any","expression"],"name":"replacement","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["any","expression"],"name":"replacement","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "collectionId": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "concat": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"first","type":"Expression"}, + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "conditional": { + "overloads": [ + { + "params": [ + {"kinds":["booleanExpression"],"name":"condition","type":"BooleanExpression"}, + {"kinds":["expression"],"name":"thenExpr","type":"Expression"}, + {"kinds":["expression"],"name":"elseExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "constant": { + "overloads": [ + { + "params": [{"kinds":["boolean"],"name":"value","type":"boolean"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["bytes"],"name":"value","type":"Buffer | Uint8Array"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["date"],"name":"value","type":"Date"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["documentReference"],"name":"value","type":"DocumentReference"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["geoPoint"],"name":"value","type":"GeoPoint"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["null"],"name":"value","type":"null"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["number"],"name":"value","type":"number"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["string"],"name":"value","type":"string"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["timestamp"],"name":"value","type":"Timestamp"}], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["vectorValue"],"name":"value","type":"VectorValue"}], + "returns": {"kinds":["expression"],"type":"Expression"} + } + ] + }, + "cosineDistance": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"vectorExpression","type":"Expression"}, + { + "elements": {"kinds":["number"]}, + "kinds": ["array","expression"], + "name": "vector", + "type": "number[] | Expression" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"vectorExpression","type":"Expression"}, + {"kinds":["expression"],"name":"otherVectorExpression","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + { + "elements": {"kinds":["number"]}, + "kinds": ["array","vectorValue"], + "name": "vector", + "type": "number[] | VectorValue" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"vectorExpression","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "count": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + } + ] + }, + "countAll": {"overloads":[{"params":[],"returns":{"kinds":["aggregateFunction"],"type":"AggregateFunction"}}]}, + "countDistinct": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + } + ] + }, + "countIf": { + "overloads": [ + { + "params": [{"kinds":["booleanExpression"],"name":"booleanExpr","type":"BooleanExpression"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + } + ] + }, + "currentDocument": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"Expression"}}]}, + "currentTimestamp": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}]}, + "descending": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["ordering"],"type":"Ordering"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["ordering"],"type":"Ordering"} + } + ] + }, + "divide": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"dividend","type":"Expression"}, + {"kinds":["any"],"name":"divisor","type":"unknown"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"dividend","type":"Expression"}, + {"kinds":["expression"],"name":"divisor","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"dividend","type":"string"},{"kinds":["any"],"name":"divisor","type":"unknown"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"dividend","type":"string"}, + {"kinds":["expression"],"name":"divisor","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "documentId": { + "overloads": [ + { + "params": [{"kinds":["documentReference","string"],"name":"documentPath","type":"string | DocumentReference"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"documentPathExpr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "documentMatches": { + "overloads": [ + { + "params": [{"kinds":["expression","string"],"name":"rquery","type":"string | Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "dotProduct": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"vectorExpression","type":"Expression"}, + { + "elements": {"kinds":["number"]}, + "kinds": ["array","vectorValue"], + "name": "vector", + "type": "number[] | VectorValue" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"vectorExpression","type":"Expression"}, + {"kinds":["expression"],"name":"otherVectorExpression","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + { + "elements": {"kinds":["number"]}, + "kinds": ["array","vectorValue"], + "name": "vector", + "type": "number[] | VectorValue" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"vectorExpression","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "endsWith": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["expression"],"name":"suffix","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["string"],"name":"suffix","type":"string"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"suffix","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["string"],"name":"suffix","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "equal": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["any"],"name":"value","type":"unknown"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"left","type":"Expression"}, + {"kinds":["expression"],"name":"right","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"expression","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "equalAny": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "euclideanDistance": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"vectorExpression","type":"Expression"}, + { + "elements": {"kinds":["number"]}, + "kinds": ["array","vectorValue"], + "name": "vector", + "type": "number[] | VectorValue" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"vectorExpression","type":"Expression"}, + {"kinds":["expression"],"name":"otherVectorExpression","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + { + "elements": {"kinds":["number"]}, + "kinds": ["array","vectorValue"], + "name": "vector", + "type": "number[] | VectorValue" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"vectorExpression","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "exists": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"value","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "exp": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "field": { + "overloads": [ + { + "params": [{"kinds":["fieldPath"],"name":"path","type":"FieldPath"}], + "returns": {"kinds":["field"],"type":"Field"} + }, + {"params":[{"kinds":["string"],"name":"name","type":"string"}],"returns":{"kinds":["field"],"type":"Field"}} + ] + }, + "first": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + } + ] + }, + "geoDistance": { + "overloads": [ + { + "params": [ + {"kinds":["field","string"],"name":"fieldName","type":"string | Field"}, + {"kinds":["expression","geoPoint"],"name":"location","type":"GeoPoint | Expression"} + ], + "returns": {"kinds":["expression"],"type":"Expression"} + } + ] + }, + "getField": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["expression"],"name":"keyExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["string"],"name":"key","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"keyExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["string"],"name":"key","type":"string"}], + "returns": {"kinds":["expression"],"type":"Expression"} + } + ] + }, + "greaterThan": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["any"],"name":"value","type":"unknown"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"left","type":"Expression"}, + {"kinds":["expression"],"name":"right","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"expression","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "greaterThanOrEqual": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["any"],"name":"value","type":"unknown"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"left","type":"Expression"}, + {"kinds":["expression"],"name":"right","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"value","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "ifAbsent": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"ifExpr","type":"Expression"}, + {"kinds":["any"],"name":"elseValue","type":"unknown"} + ], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"ifExpr","type":"Expression"}, + {"kinds":["expression"],"name":"elseExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [ + {"kinds":["string"],"name":"ifFieldName","type":"string"}, + {"kinds":["expression"],"name":"elseExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"Expression"} + } + ] + }, + "ifError": { + "overloads": [ + { + "params": [ + {"kinds":["booleanExpression"],"name":"tryExpr","type":"BooleanExpression"}, + {"kinds":["booleanExpression"],"name":"catchExpr","type":"BooleanExpression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"tryExpr","type":"Expression"}, + {"kinds":["any"],"name":"catchValue","type":"unknown"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"tryExpr","type":"Expression"}, + {"kinds":["expression"],"name":"catchExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "ifNull": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"ifExpr","type":"Expression"}, + {"kinds":["any"],"name":"elseValue","type":"unknown"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"ifExpr","type":"Expression"}, + {"kinds":["expression"],"name":"elseExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"ifFieldName","type":"string"}, + {"kinds":["any"],"name":"elseValue","type":"unknown"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"ifFieldName","type":"string"}, + {"kinds":["expression"],"name":"elseExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "isAbsent": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"value","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"field","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "isError": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"value","type":"Expression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "isType": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["string"],"name":"type","type":"string"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["string"],"name":"type","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "join": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["expression"],"name":"delimiterExpression","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"}, + {"kinds":["string"],"name":"delimiter","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"Expression"} + }, + { + "params": [ + {"kinds":["string"],"name":"arrayFieldName","type":"string"}, + {"kinds":["string"],"name":"delimiter","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"Expression"} + } + ] + }, + "last": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + } + ] + }, + "length": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "lessThan": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["any"],"name":"value","type":"unknown"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"left","type":"Expression"}, + {"kinds":["expression"],"name":"right","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"expression","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "lessThanOrEqual": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["any"],"name":"value","type":"unknown"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"left","type":"Expression"}, + {"kinds":["expression"],"name":"right","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"expression","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "like": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["expression"],"name":"pattern","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["string"],"name":"pattern","type":"string"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"pattern","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["string"],"name":"pattern","type":"string"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "ln": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "log10": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "logicalMaximum": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"first","type":"Expression"}, + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "logicalMinimum": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"first","type":"Expression"}, + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "map": { + "overloads": [ + { + "params": [{"kinds":["map"],"name":"elements","type":"Record"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "mapEntries": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"mapExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"mapField","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "mapGet": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"mapExpression","type":"Expression"}, + {"kinds":["string"],"name":"subField","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["string"],"name":"subField","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "mapKeys": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"mapExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"mapField","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "mapMerge": { + "overloads": [ + { + "params": [ + {"kinds":["expression","map"],"name":"firstMap","type":"Record | Expression"}, + {"kinds":["expression","map"],"name":"secondMap","type":"Record | Expression"}, + { + "kinds": ["expression","map"], + "name": "otherMaps", + "rest": true, + "type": "Array | Expression>" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"mapField","type":"string"}, + {"kinds":["expression","map"],"name":"secondMap","type":"Record | Expression"}, + { + "kinds": ["expression","map"], + "name": "otherMaps", + "rest": true, + "type": "Array | Expression>" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "mapRemove": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"mapExpr","type":"Expression"}, + {"kinds":["expression"],"name":"keyExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"mapExpr","type":"Expression"}, + {"kinds":["string"],"name":"key","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"mapField","type":"string"}, + {"kinds":["expression"],"name":"keyExpr","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"mapField","type":"string"},{"kinds":["string"],"name":"key","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "mapSet": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"mapExpression","type":"Expression"}, + {"kinds":["expression","string"],"name":"key","type":"string | Expression"}, + {"kinds":["any"],"name":"value","type":"unknown"}, + {"kinds":["any"],"name":"moreKeyValues","rest":true,"type":"unknown[]"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"mapField","type":"string"}, + {"kinds":["expression","string"],"name":"key","type":"string | Expression"}, + {"kinds":["any"],"name":"value","type":"unknown"}, + {"kinds":["any"],"name":"moreKeyValues","rest":true,"type":"unknown[]"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "mapValues": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"mapExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"mapField","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "maximum": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + } + ] + }, + "minimum": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + } + ] + }, + "mod": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["any"],"name":"value","type":"unknown"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"left","type":"Expression"}, + {"kinds":["expression"],"name":"right","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"expression","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "multiply": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"first","type":"Expression"}, + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["any","expression"],"name":"second","type":"Expression | unknown"}, + {"kinds":["any","expression"],"name":"others","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "nor": { + "overloads": [ + { + "params": [ + {"kinds":["booleanExpression"],"name":"first","type":"BooleanExpression"}, + {"kinds":["booleanExpression"],"name":"second","type":"BooleanExpression"}, + {"kinds":["booleanExpression"],"name":"more","rest":true,"type":"BooleanExpression[]"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "not": { + "overloads": [ + { + "params": [{"kinds":["booleanExpression"],"name":"booleanExpr","type":"BooleanExpression"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "notEqual": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["any"],"name":"value","type":"unknown"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"left","type":"Expression"}, + {"kinds":["expression"],"name":"right","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["any"],"name":"value","type":"unknown"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"expression","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "notEqualAny": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"element","type":"Expression"}, + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"element","type":"Expression"}, + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + { + "elements": {"kinds":["any","expression"]}, + "kinds": ["array"], + "name": "values", + "type": "Array" + } + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"arrayExpression","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "or": { + "overloads": [ + { + "params": [ + {"kinds":["booleanExpression"],"name":"first","type":"BooleanExpression"}, + {"kinds":["booleanExpression"],"name":"second","type":"BooleanExpression"}, + {"kinds":["booleanExpression"],"name":"more","rest":true,"type":"BooleanExpression[]"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "parent": { + "overloads": [ + { + "params": [{"kinds":["documentReference","string"],"name":"documentPath","type":"string | DocumentReference"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"documentPathExpr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "pow": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"base","type":"Expression"}, + {"kinds":["expression"],"name":"exponent","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"base","type":"Expression"}, + {"kinds":["number"],"name":"exponent","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"base","type":"string"}, + {"kinds":["expression"],"name":"exponent","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"base","type":"string"},{"kinds":["number"],"name":"exponent","type":"number"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "rand": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"FunctionExpression"}}]}, + "regexContains": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["expression"],"name":"pattern","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["string"],"name":"pattern","type":"string"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"pattern","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["string"],"name":"pattern","type":"string"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "regexFind": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["expression"],"name":"pattern","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["string"],"name":"pattern","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"pattern","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["string"],"name":"pattern","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "regexFindAll": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["expression"],"name":"pattern","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["string"],"name":"pattern","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"pattern","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["string"],"name":"pattern","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "regexMatch": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["expression"],"name":"pattern","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["string"],"name":"pattern","type":"string"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"pattern","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["string"],"name":"pattern","type":"string"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "reverse": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"stringExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"field","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "round": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["expression","number"],"name":"decimalPlaces","type":"number | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression","number"],"name":"decimalPlaces","type":"number | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "score": {"overloads":[{"params":[],"returns":{"kinds":["expression"],"type":"Expression"}}]}, + "split": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["expression"],"name":"delimiter","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["string"],"name":"delimiter","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"delimiter","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["string"],"name":"delimiter","type":"string"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "sqrt": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "startsWith": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["expression"],"name":"prefix","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["string"],"name":"prefix","type":"string"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"prefix","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"},{"kinds":["string"],"name":"prefix","type":"string"}], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "stringConcat": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"firstString","type":"Expression"}, + {"kinds":["expression","string"],"name":"secondString","type":"Expression | string"}, + {"kinds":["expression","string"],"name":"otherStrings","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression","string"],"name":"secondString","type":"Expression | string"}, + {"kinds":["expression","string"],"name":"otherStrings","rest":true,"type":"Array"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "stringContains": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["expression"],"name":"substring","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["string"],"name":"substring","type":"string"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"substring","type":"Expression"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["string"],"name":"substring","type":"string"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + }, + "stringReverse": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"stringExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"field","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "subcollection": { + "overloads": [ + { + "params": [ + { + "kinds": ["options"], + "name": "options", + "options": ["SubcollectionStageOptions"], + "type": "SubcollectionStageOptions" + } + ], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + }, + { + "params": [{"kinds":["string"],"name":"path","type":"string"}], + "returns": {"kinds":["pipeline"],"type":"Pipeline"} + } + ] + }, + "substring": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"input","type":"Expression"}, + {"kinds":["expression"],"name":"position","type":"Expression"}, + {"kinds":["expression"],"name":"length","optional":true,"type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"input","type":"Expression"}, + {"kinds":["number"],"name":"position","type":"number"}, + {"kinds":["number"],"name":"length","optional":true,"type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"field","type":"string"}, + {"kinds":["expression"],"name":"position","type":"Expression"}, + {"kinds":["expression"],"name":"length","optional":true,"type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"field","type":"string"}, + {"kinds":["number"],"name":"position","type":"number"}, + {"kinds":["number"],"name":"length","optional":true,"type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "subtract": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"minuend","type":"Expression"}, + {"kinds":["any"],"name":"subtrahend","type":"unknown"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"minuend","type":"Expression"}, + {"kinds":["expression"],"name":"subtrahend","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"minuendFieldName","type":"string"}, + {"kinds":["any"],"name":"subtrahend","type":"unknown"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"minuendFieldName","type":"string"}, + {"kinds":["expression"],"name":"subtrahend","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "sum": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["aggregateFunction"],"type":"AggregateFunction"} + } + ] + }, + "switchOn": { + "overloads": [ + { + "params": [ + {"kinds":["booleanExpression"],"name":"condition","type":"BooleanExpression"}, + {"kinds":["expression"],"name":"result","type":"Expression"}, + { + "kinds": ["booleanExpression","expression"], + "name": "others", + "rest": true, + "type": "Array" + } + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "timestampAdd": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"timestamp","type":"Expression"}, + {"aliases":["TimeUnit"],"kinds":["literal"],"name":"unit","type":"TimeUnit"}, + {"kinds":["number"],"name":"amount","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"timestamp","type":"Expression"}, + {"kinds":["expression"],"name":"unit","type":"Expression"}, + {"kinds":["expression"],"name":"amount","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"aliases":["TimeUnit"],"kinds":["literal"],"name":"unit","type":"TimeUnit"}, + {"kinds":["number"],"name":"amount","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "timestampDiff": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"endExpression","type":"Expression"}, + {"kinds":["expression"],"name":"startExpression","type":"Expression"}, + {"aliases":["TimeUnit"],"kinds":["expression","literal"],"name":"unit","type":"TimeUnit | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"endExpression","type":"Expression"}, + {"kinds":["string"],"name":"startFieldName","type":"string"}, + {"aliases":["TimeUnit"],"kinds":["expression","literal"],"name":"unit","type":"TimeUnit | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"endFieldName","type":"string"}, + {"kinds":["expression"],"name":"startExpression","type":"Expression"}, + {"aliases":["TimeUnit"],"kinds":["expression","literal"],"name":"unit","type":"TimeUnit | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"endFieldName","type":"string"}, + {"kinds":["string"],"name":"startFieldName","type":"string"}, + {"aliases":["TimeUnit"],"kinds":["expression","literal"],"name":"unit","type":"TimeUnit | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "timestampExtract": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"timestampExpression","type":"Expression"}, + {"aliases":["TimePart"],"kinds":["literal"],"name":"part","type":"TimePart"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"timestampExpression","type":"Expression"}, + {"kinds":["expression"],"name":"part","type":"Expression"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"aliases":["TimePart"],"kinds":["literal"],"name":"part","type":"TimePart"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"part","type":"Expression"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "timestampSubtract": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"timestamp","type":"Expression"}, + {"aliases":["TimeUnit"],"kinds":["literal"],"name":"unit","type":"TimeUnit"}, + {"kinds":["number"],"name":"amount","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"timestamp","type":"Expression"}, + {"kinds":["expression"],"name":"unit","type":"Expression"}, + {"kinds":["expression"],"name":"amount","type":"Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"aliases":["TimeUnit"],"kinds":["literal"],"name":"unit","type":"TimeUnit"}, + {"kinds":["number"],"name":"amount","type":"number"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "timestampToUnixMicros": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "timestampToUnixMillis": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "timestampToUnixSeconds": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "timestampTruncate": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"timestampExpression","type":"Expression"}, + {"aliases":["TimeGranularity"],"kinds":["literal"],"name":"granularity","type":"TimeGranularity"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["expression"],"name":"timestampExpression","type":"Expression"}, + {"kinds":["expression"],"name":"granularity","type":"Expression"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"aliases":["TimeGranularity"],"kinds":["literal"],"name":"granularity","type":"TimeGranularity"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression"],"name":"granularity","type":"Expression"}, + {"kinds":["expression","string"],"name":"timezone","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "toLower": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"stringExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "toUpper": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"stringExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "trim": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"stringExpression","type":"Expression"}, + {"kinds":["expression","string"],"name":"valueToTrim","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression","string"],"name":"valueToTrim","optional":true,"type":"string | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "trunc": { + "overloads": [ + { + "params": [ + {"kinds":["expression"],"name":"expression","type":"Expression"}, + {"kinds":["expression","number"],"name":"decimalPlaces","type":"number | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [ + {"kinds":["string"],"name":"fieldName","type":"string"}, + {"kinds":["expression","number"],"name":"decimalPlaces","type":"number | Expression"} + ], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "type": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "unixMicrosToTimestamp": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "unixMillisToTimestamp": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "unixSecondsToTimestamp": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"expr","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "variable": { + "overloads": [ + { + "params": [{"kinds":["string"],"name":"name","type":"string"}], + "returns": {"kinds":["expression"],"type":"Expression"} + } + ] + }, + "vectorLength": { + "overloads": [ + { + "params": [{"kinds":["expression"],"name":"vectorExpression","type":"Expression"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + }, + { + "params": [{"kinds":["string"],"name":"fieldName","type":"string"}], + "returns": {"kinds":["expression"],"type":"FunctionExpression"} + } + ] + }, + "xor": { + "overloads": [ + { + "params": [ + {"kinds":["booleanExpression"],"name":"first","type":"BooleanExpression"}, + {"kinds":["booleanExpression"],"name":"second","type":"BooleanExpression"}, + {"kinds":["booleanExpression"],"name":"additionalConditions","rest":true,"type":"BooleanExpression[]"} + ], + "returns": {"kinds":["booleanExpression"],"type":"BooleanExpression"} + } + ] + } + }, + "interfaces": { + "Selectable": { + "abstract": false, + "extends": null, + "implements": [], + "methods": {}, + "properties": {"selectable":{"kinds":["boolean"],"optional":false,"readonly":false,"static":false,"type":"true"}} + } + }, + "runtimeExports": [ + "AggregateFunction", + "AliasedAggregate", + "AliasedExpression", + "BooleanExpression", + "Constant", + "Expression", + "Field", + "FunctionExpression", + "Ordering", + "Pipeline", + "PipelineResult", + "PipelineSnapshot", + "PipelineSource", + "abs", + "add", + "and", + "array", + "arrayAgg", + "arrayAggDistinct", + "arrayConcat", + "arrayContains", + "arrayContainsAll", + "arrayContainsAny", + "arrayFilter", + "arrayFirst", + "arrayFirstN", + "arrayGet", + "arrayIndexOf", + "arrayIndexOfAll", + "arrayLast", + "arrayLastIndexOf", + "arrayLastN", + "arrayLength", + "arrayMaximum", + "arrayMaximumN", + "arrayMinimum", + "arrayMinimumN", + "arrayReverse", + "arraySlice", + "arraySum", + "arrayTransform", + "arrayTransformWithIndex", + "ascending", + "average", + "byteLength", + "ceil", + "charLength", + "coalesce", + "collectionId", + "concat", + "conditional", + "constant", + "cosineDistance", + "count", + "countAll", + "countDistinct", + "countIf", + "currentDocument", + "currentTimestamp", + "descending", + "divide", + "documentId", + "documentMatches", + "dotProduct", + "endsWith", + "equal", + "equalAny", + "euclideanDistance", + "exists", + "exp", + "field", + "first", + "floor", + "geoDistance", + "getField", + "greaterThan", + "greaterThanOrEqual", + "ifAbsent", + "ifError", + "ifNull", + "isAbsent", + "isError", + "isType", + "join", + "last", + "length", + "lessThan", + "lessThanOrEqual", + "like", + "ln", + "log10", + "logicalMaximum", + "logicalMinimum", + "ltrim", + "map", + "mapEntries", + "mapGet", + "mapKeys", + "mapMerge", + "mapRemove", + "mapSet", + "mapValues", + "maximum", + "minimum", + "mod", + "multiply", + "nor", + "not", + "notEqual", + "notEqualAny", + "or", + "parent", + "pow", + "rand", + "regexContains", + "regexFind", + "regexFindAll", + "regexMatch", + "reverse", + "round", + "rtrim", + "score", + "split", + "sqrt", + "startsWith", + "stringConcat", + "stringContains", + "stringIndexOf", + "stringRepeat", + "stringReplaceAll", + "stringReplaceOne", + "stringReverse", + "subcollection", + "substring", + "subtract", + "sum", + "switchOn", + "timestampAdd", + "timestampDiff", + "timestampExtract", + "timestampSubtract", + "timestampToUnixMicros", + "timestampToUnixMillis", + "timestampToUnixSeconds", + "timestampTruncate", + "toLower", + "toUpper", + "trim", + "trunc", + "type", + "unixMicrosToTimestamp", + "unixMillisToTimestamp", + "unixSecondsToTimestamp", + "variable", + "vectorLength", + "xor" + ], + "runtimeOnlyFunctions": { + "floor": {"module":"expression","params":[{"name":"expr"}]}, + "ltrim": {"module":"expression","params":[{"name":"expr"},{"name":"valueToTrim"}]}, + "rtrim": {"module":"expression","params":[{"name":"expr"},{"name":"valueToTrim"}]}, + "stringIndexOf": {"module":"expression","params":[{"name":"expr"},{"name":"search"}]}, + "stringRepeat": {"module":"expression","params":[{"name":"expr"},{"name":"repetitions"}]}, + "stringReplaceAll": {"module":"expression","params":[{"name":"expr"},{"name":"find"},{"name":"replacement"}]}, + "stringReplaceOne": {"module":"expression","params":[{"name":"expr"},{"name":"find"},{"name":"replacement"}]} + }, + "runtimeOnlyMembers": { + "Expression": { + "arrayReverse": {"kind":"method","module":"expression.js","params":[]}, + "concat": {"kind":"method","module":"expression.js","params":[{"name":"second"},{"name":"others","rest":true}]} + }, + "PipelineResult": {"protoField":{"kind":"method","module":"pipelines.js","params":[{"name":"field"}]}} + }, + "source": { + "generator": "packages/google_cloud_firestore/tool/pipeline_api_parity", + "namespace": "FirebaseFirestore.Pipelines", + "package": "@google-cloud/firestore", + "runtimeIndex": "build/src/pipelines/index.js", + "typings": "types/firestore.d.ts", + "version": "9.3.1" + }, + "types": { + "AddFieldsStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": {"fields":{"elements":{"kinds":["selectable"]},"kinds":["array"],"optional":false,"type":"Selectable[]"}} + }, + "AggregateStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": { + "accumulators": {"elements":{"kinds":["aliasedAggregate"]},"kinds":["array"],"optional":false,"type":"AliasedAggregate[]"}, + "groups": { + "elements": {"kinds":["selectable","string"]}, + "kinds": ["array"], + "optional": true, + "type": "Array" + } + } + }, + "CollectionGroupStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": { + "collectionId": {"kinds":["string"],"optional":false,"type":"string"}, + "forceIndex": {"kinds":["string"],"optional":true,"type":"string"} + } + }, + "CollectionStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": { + "collection": {"kinds":["collectionReference","string"],"optional":false,"type":"string | CollectionReference"}, + "forceIndex": {"kinds":["string"],"optional":true,"type":"string"} + } + }, + "DatabaseStageOptions": {"extends":["StageOptions"],"kind":"object","properties":{}}, + "DefineStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": { + "variables": {"elements":{"kinds":["aliasedExpression"]},"kinds":["array"],"optional":false,"type":"AliasedExpression[]"} + } + }, + "DistinctStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": { + "groups": { + "elements": {"kinds":["selectable","string"]}, + "kinds": ["array"], + "optional": false, + "type": "Array" + } + } + }, + "DocumentsStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": { + "docs": { + "elements": {"kinds":["documentReference","string"]}, + "kinds": ["array"], + "optional": false, + "type": "Array" + } + } + }, + "ExplainStatsFieldValue": { + "elements": {"kinds":["explainStatsFieldValue"]}, + "kind": "other", + "kinds": ["array","boolean","map","null","number","string"], + "type": "null | string | number | boolean | {[key: string]: ExplainStatsFieldValue;} | ExplainStatsFieldValue[]" + }, + "ExpressionType": { + "kind": "stringUnion", + "values": ["AggregateFunction","AliasedExpression","Constant","Field","Function","ListOfExprs","PipelineValue","Variable"] + }, + "FindNearestStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": { + "distanceField": {"kinds":["string"],"optional":true,"type":"string"}, + "distanceMeasure": { + "kinds": ["literal"], + "literals": ["cosine","dot_product","euclidean"], + "optional": false, + "type": "'euclidean' | 'cosine' | 'dot_product'" + }, + "field": {"kinds":["field","string"],"optional":false,"type":"Field | string"}, + "limit": {"kinds":["number"],"optional":true,"type":"number"}, + "vectorValue": { + "elements": {"kinds":["number"]}, + "kinds": ["array","vectorValue"], + "optional": false, + "type": "VectorValue | number[]" + } + } + }, + "LimitStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": {"limit":{"kinds":["number"],"optional":false,"type":"number"}} + }, + "OffsetStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": {"offset":{"kinds":["number"],"optional":false,"type":"number"}} + }, + "PipelineExecuteOptions": { + "kind": "object", + "properties": { + "explainOptions": { + "kinds": ["map"], + "optional": true, + "properties": { + "mode": {"kinds":["literal"],"literals":["analyze","execute"],"optional":true,"type":"'execute' | 'analyze'"}, + "outputFormat": {"kinds":["literal"],"literals":["text"],"optional":true,"type":"'text'"} + }, + "type": "{mode?: 'execute' | 'analyze'; outputFormat?: 'text';}" + }, + "indexMode": {"kinds":["literal"],"literals":["recommended"],"optional":true,"type":"'recommended'"}, + "rawOptions": { + "kinds": ["map"], + "optional": true, + "properties": {"[key]":{"kinds":["any"],"optional":true,"type":"unknown"}}, + "type": "{[name: string]: unknown;}" + } + } + }, + "RemoveFieldsStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": { + "fields": {"elements":{"kinds":["field","string"]},"kinds":["array"],"optional":false,"type":"Array"} + } + }, + "ReplaceWithStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": {"map":{"kinds":["expression","string"],"optional":false,"type":"Expression | string"}} + }, + "SampleStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "oneOf": [["documents","percentage"]], + "properties": { + "documents": {"kinds":["number"],"optional":true,"type":"number"}, + "percentage": {"kinds":["number"],"optional":true,"type":"number"} + } + }, + "SearchStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": { + "addFields": {"elements":{"kinds":["selectable"]},"kinds":["array"],"optional":true,"type":"Selectable[]"}, + "languageCode": {"kinds":["string"],"optional":true,"type":"string"}, + "limit": {"kinds":["number"],"optional":true,"type":"number"}, + "offset": {"kinds":["number"],"optional":true,"type":"number"}, + "query": {"kinds":["booleanExpression","string"],"optional":false,"type":"BooleanExpression | string"}, + "retrievalDepth": {"kinds":["number"],"optional":true,"type":"number"}, + "sort": {"elements":{"kinds":["ordering"]},"kinds":["array","ordering"],"optional":true,"type":"Ordering | Ordering[]"} + } + }, + "SelectStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": { + "selections": { + "elements": {"kinds":["selectable","string"]}, + "kinds": ["array"], + "optional": false, + "type": "Array" + } + } + }, + "SortStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": {"orderings":{"elements":{"kinds":["ordering"]},"kinds":["array"],"optional":false,"type":"Ordering[]"}} + }, + "StageOptions": { + "kind": "object", + "properties": { + "rawOptions": { + "kinds": ["map"], + "optional": true, + "properties": {"[key]":{"kinds":["any"],"optional":true,"type":"unknown"}}, + "type": "{[name: string]: unknown;}" + } + } + }, + "SubcollectionStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": {"path":{"kinds":["string"],"optional":false,"type":"string"}} + }, + "TimeGranularity": { + "kind": "stringUnion", + "values": [ + "day", + "hour", + "isoweek", + "isoyear", + "microsecond", + "millisecond", + "minute", + "month", + "quarter", + "second", + "week", + "week(friday)", + "week(monday)", + "week(saturday)", + "week(sunday)", + "week(thursday)", + "week(tuesday)", + "week(wednesday)", + "year" + ] + }, + "TimePart": { + "kind": "stringUnion", + "values": [ + "day", + "dayofweek", + "dayofyear", + "hour", + "isoweek", + "isoyear", + "microsecond", + "millisecond", + "minute", + "month", + "quarter", + "second", + "week", + "week(friday)", + "week(monday)", + "week(saturday)", + "week(sunday)", + "week(thursday)", + "week(tuesday)", + "week(wednesday)", + "year" + ] + }, + "TimeUnit": {"kind":"stringUnion","values":["day","hour","microsecond","millisecond","minute","second"]}, + "Type": { + "kind": "stringUnion", + "values": [ + "array", + "boolean", + "bytes", + "decimal128", + "float64", + "geo_point", + "int32", + "int64", + "map", + "max_key", + "min_key", + "null", + "number", + "object_id", + "reference", + "regex", + "request_timestamp", + "string", + "timestamp", + "vector" + ] + }, + "UnionStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": {"other":{"kinds":["pipeline"],"optional":false,"type":"Pipeline"}} + }, + "UnnestStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": { + "indexField": {"kinds":["string"],"optional":true,"type":"string"}, + "selectable": {"kinds":["selectable"],"optional":false,"type":"Selectable"} + } + }, + "WhereStageOptions": { + "extends": ["StageOptions"], + "kind": "object", + "properties": {"condition":{"kinds":["booleanExpression"],"optional":false,"type":"BooleanExpression"}} + } + } +} diff --git a/packages/google_cloud_firestore/test/fixtures/pipeline_golden/aggregates.json b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/aggregates.json new file mode 100644 index 00000000..da8f05f6 --- /dev/null +++ b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/aggregates.json @@ -0,0 +1,943 @@ +{ + "generator": "tool/pipeline_golden (npm run generate)", + "sdk": "@google-cloud/firestore@9.3.1", + "cases": { + "aggregates/arrayAgg/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_agg", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/arrayAgg/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_agg", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/arrayAgg/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_agg", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/arrayAggDistinct/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_agg_distinct", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/arrayAggDistinct/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_agg_distinct", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/arrayAggDistinct/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_agg_distinct", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/average/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "average", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/average/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "average", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/average/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "average", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/count/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "count", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/count/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "count", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/count/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "count", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/countAll/static": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": {"result": {"functionValue": {"name": "count"}}} + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/countDistinct/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "count_distinct", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/countDistinct/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "count_distinct", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/countDistinct/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "count_distinct", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/countIf/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "count_if", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/countIf/static": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "count_if", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/first/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "first", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/first/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "first", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/first/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "first", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/last/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "last", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/last/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "last", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/last/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "last", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/maximum/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/maximum/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/maximum/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/minimum/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/minimum/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/minimum/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/sum/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "sum", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/sum/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "sum", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "aggregates/sum/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "sum", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + } + } +} diff --git a/packages/google_cloud_firestore/test/fixtures/pipeline_golden/functions.json b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/functions.json new file mode 100644 index 00000000..258cd2d7 --- /dev/null +++ b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/functions.json @@ -0,0 +1,17743 @@ +{ + "generator": "tool/pipeline_golden (npm run generate)", + "sdk": "@google-cloud/firestore@9.3.1", + "cases": { + "functions/abs/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "abs", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/abs/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "abs", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/abs/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "abs", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/add/method-double": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "add", + "args": [ + {"fieldReferenceValue": "rating"}, + {"doubleValue": 0.5} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/add/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "add", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "bonus"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/add/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "add", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/add/method-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "add", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"}, + {"fieldReferenceValue": "bonus"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/add/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "add", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "bonus"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/add/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "add", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/add/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "add", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "bonus"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/add/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "add", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/add/static-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "add", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/and/static-three": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "published"}, + {"integerValue": "1950"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/and/static-two": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/array/static-empty": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"functionValue": {"name": "array"}}} + } + } + ] + } + ] + } + } + }, + "functions/array/static-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array", + "args": [ + {"integerValue": "1"}, + {"stringValue": "a"}, + {"booleanValue": true}, + {"nullValue": null} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/array/static-nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array", + "args": [ + { + "functionValue": { + "name": "array", + "args": [ + {"integerValue": "1"}, + {"fieldReferenceValue": "a"} + ] + } + }, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "k"}, + {"fieldReferenceValue": "b"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/array/static-with-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array", + "args": [ + {"fieldReferenceValue": "title"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayConcat/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_concat", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "moreTags"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayConcat/method-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_concat", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "functionValue": { + "name": "array", + "args": [ + {"stringValue": "x"}, + {"stringValue": "y"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayConcat/method-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_concat", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "moreTags"}, + { + "functionValue": { + "name": "array", + "args": [{"stringValue": "z"}] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayConcat/static-expression-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_concat", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "moreTags"}, + { + "functionValue": { + "name": "array", + "args": [{"stringValue": "z"}] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayConcat/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_concat", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "moreTags"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayConcat/static-field-name-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_concat", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "functionValue": { + "name": "array", + "args": [ + {"stringValue": "x"}, + {"stringValue": "y"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayContains/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContains/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"} + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContains/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContains/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"} + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContains/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContains/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"} + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAll/method-array-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_all", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAll/method-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_all", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + {"stringValue": "epic"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAll/method-list-with-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_all", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAll/static-expression-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_all", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + {"stringValue": "epic"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAll/static-field-name-array-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_all", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAll/static-field-name-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_all", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + {"stringValue": "epic"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAll/static-field-name-list-nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_all", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + { + "functionValue": { + "name": "array", + "args": [{"stringValue": "epic"}] + } + } + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAll/static-field-name-list-with-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_all", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAny/method-array-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_any", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAny/method-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_any", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + {"stringValue": "epic"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAny/method-list-with-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_any", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAny/static-expression-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_any", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + {"stringValue": "epic"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAny/static-field-name-array-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_any", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAny/static-field-name-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_any", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + {"stringValue": "epic"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAny/static-field-name-list-nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_any", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + { + "functionValue": { + "name": "array", + "args": [{"stringValue": "epic"}] + } + } + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayContainsAny/static-field-name-list-with-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "array_contains_any", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/arrayFilter/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_filter", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "tag"}, + { + "functionValue": { + "name": "equal", + "args": [ + {"variableReferenceValue": "tag"}, + {"stringValue": "magic"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayFilter/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_filter", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "tag"}, + { + "functionValue": { + "name": "equal", + "args": [ + {"variableReferenceValue": "tag"}, + {"stringValue": "magic"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayFilter/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_filter", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "tag"}, + { + "functionValue": { + "name": "equal", + "args": [ + {"variableReferenceValue": "tag"}, + {"stringValue": "magic"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayFirst/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_first", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayFirst/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_first", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayFirst/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_first", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayFirstN/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_first_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayFirstN/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_first_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayFirstN/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_first_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayFirstN/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_first_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayFirstN/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_first_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayFirstN/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_first_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayGet/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_get", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "index"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayGet/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_get", + "args": [ + {"fieldReferenceValue": "tags"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayGet/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_get", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "index"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayGet/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_get", + "args": [ + {"fieldReferenceValue": "tags"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayGet/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_get", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "index"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayGet/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_get", + "args": [ + {"fieldReferenceValue": "tags"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOf/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"}, + {"stringValue": "first"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOf/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"}, + {"stringValue": "first"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOf/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"}, + {"stringValue": "first"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOf/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"}, + {"stringValue": "first"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOf/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"}, + {"stringValue": "first"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOf/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"}, + {"stringValue": "first"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOfAll/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of_all", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOfAll/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of_all", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOfAll/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of_all", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOfAll/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of_all", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOfAll/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of_all", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayIndexOfAll/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of_all", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLast/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_last", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLast/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_last", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLast/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_last", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastIndexOf/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"}, + {"stringValue": "last"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastIndexOf/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"}, + {"stringValue": "last"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastIndexOf/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"}, + {"stringValue": "last"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastIndexOf/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"}, + {"stringValue": "last"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastIndexOf/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "wanted"}, + {"stringValue": "last"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastIndexOf/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_index_of", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"}, + {"stringValue": "last"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastN/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_last_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastN/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_last_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastN/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_last_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastN/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_last_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastN/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_last_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLastN/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_last_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLength/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_length", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLength/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_length", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayLength/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_length", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMaximum/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMaximum/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMaximum/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMaximumN/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMaximumN/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMaximumN/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMaximumN/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMaximumN/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMaximumN/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMinimum/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMinimum/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMinimum/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMinimumN/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMinimumN/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMinimumN/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMinimumN/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMinimumN/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"fieldReferenceValue": "n"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayMinimumN/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum_n", + "args": [ + {"fieldReferenceValue": "scores"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayReverse/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_reverse", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayReverse/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_reverse", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayReverse/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_reverse", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arraySlice/method-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_slice", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "start"}, + {"fieldReferenceValue": "count"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arraySlice/method-offset": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_slice", + "args": [ + {"fieldReferenceValue": "tags"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arraySlice/method-offset-length": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_slice", + "args": [ + {"fieldReferenceValue": "tags"}, + {"integerValue": "1"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arraySlice/static-expression-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_slice", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "start"}, + {"fieldReferenceValue": "count"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arraySlice/static-field-name-offset": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_slice", + "args": [ + {"fieldReferenceValue": "tags"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arraySlice/static-field-name-offset-length": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_slice", + "args": [ + {"fieldReferenceValue": "tags"}, + {"integerValue": "1"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arraySum/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "sum", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arraySum/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "sum", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arraySum/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "sum", + "args": [{"fieldReferenceValue": "tags"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayTransform/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_transform", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "tag"}, + { + "functionValue": { + "name": "to_upper", + "args": [{"variableReferenceValue": "tag"}] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayTransform/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_transform", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "tag"}, + { + "functionValue": { + "name": "to_upper", + "args": [{"variableReferenceValue": "tag"}] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayTransform/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_transform", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "tag"}, + { + "functionValue": { + "name": "to_upper", + "args": [{"variableReferenceValue": "tag"}] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayTransformWithIndex/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_transform", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "tag"}, + {"stringValue": "i"}, + { + "functionValue": { + "name": "string_concat", + "args": [ + {"variableReferenceValue": "tag"}, + {"variableReferenceValue": "i"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayTransformWithIndex/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_transform", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "tag"}, + {"stringValue": "i"}, + { + "functionValue": { + "name": "string_concat", + "args": [ + {"variableReferenceValue": "tag"}, + {"variableReferenceValue": "i"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/arrayTransformWithIndex/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "array_transform", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "tag"}, + {"stringValue": "i"}, + { + "functionValue": { + "name": "string_concat", + "args": [ + {"variableReferenceValue": "tag"}, + {"variableReferenceValue": "i"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/byteLength/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "byte_length", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/byteLength/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "byte_length", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/byteLength/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "byte_length", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ceil/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ceil", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ceil/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ceil", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ceil/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ceil", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/charLength/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "char_length", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/charLength/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "char_length", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/charLength/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "char_length", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/coalesce/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "coalesce", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"stringValue": "anonymous"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/coalesce/method-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "coalesce", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"fieldReferenceValue": "name"}, + {"stringValue": "anonymous"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/coalesce/static-expression-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "coalesce", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"fieldReferenceValue": "name"}, + {"stringValue": "anonymous"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/coalesce/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "coalesce", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"stringValue": "anonymous"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/collectionId/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "collection_id", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/collectionId/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "collection_id", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/collectionId/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "collection_id", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/concat/method-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "concat", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "functionValue": { + "name": "array", + "args": [{"stringValue": "extra"}] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/concat/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "concat", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "!"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/concat/method-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "concat", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": " by "}, + {"fieldReferenceValue": "author"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/concat/static-expression-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "concat", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": " by "}, + {"fieldReferenceValue": "author"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/concat/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "concat", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "!"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/conditional/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "conditional", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + {"fieldReferenceValue": "title"}, + {"stringValue": "n/a"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/conditional/static": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "conditional", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + {"fieldReferenceValue": "title"}, + {"stringValue": "n/a"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/cosineDistance/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "cosine_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + {"fieldReferenceValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/cosineDistance/method-number-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "cosine_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/cosineDistance/method-vector-value": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "cosine_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/cosineDistance/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "cosine_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + {"fieldReferenceValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/cosineDistance/static-expression-number-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "cosine_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/cosineDistance/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "cosine_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + {"fieldReferenceValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/cosineDistance/static-field-name-integer-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "cosine_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 1}, + {"doubleValue": 2}, + {"doubleValue": 3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/cosineDistance/static-field-name-number-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "cosine_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/cosineDistance/static-field-name-vector-value": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "cosine_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/currentDocument/static": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": {"functionValue": {"name": "current_document"}} + } + } + } + ] + } + ] + } + } + }, + "functions/currentTimestamp/static": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": {"functionValue": {"name": "current_timestamp"}} + } + } + } + ] + } + ] + } + } + }, + "functions/divide/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "count"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/divide/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/divide/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "count"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/divide/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/divide/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "count"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/divide/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/documentId/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "document_id", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/documentId/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "document_id", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/documentId/static-path": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "document_id", + "args": [{"stringValue": "books/book1"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/documentId/static-reference": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "document_id", + "args": [ + { + "referenceValue": "projects/test-project/databases/(default)/documents/books/book1" + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/documentMatches/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "document_matches", + "args": [{"fieldReferenceValue": "query"}] + } + } + ] + } + ] + } + } + }, + "functions/documentMatches/static-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "document_matches", + "args": [{"stringValue": "breakfast"}] + } + } + ] + } + ] + } + } + }, + "functions/dotProduct/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "dot_product", + "args": [ + {"fieldReferenceValue": "embedding"}, + {"fieldReferenceValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/dotProduct/method-number-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "dot_product", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/dotProduct/method-vector-value": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "dot_product", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/dotProduct/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "dot_product", + "args": [ + {"fieldReferenceValue": "embedding"}, + {"fieldReferenceValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/dotProduct/static-expression-number-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "dot_product", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/dotProduct/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "dot_product", + "args": [ + {"fieldReferenceValue": "embedding"}, + {"fieldReferenceValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/dotProduct/static-field-name-integer-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "dot_product", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 1}, + {"doubleValue": 2}, + {"doubleValue": 3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/dotProduct/static-field-name-number-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "dot_product", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/dotProduct/static-field-name-vector-value": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "dot_product", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/endsWith/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "ends_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "suffix"} + ] + } + } + ] + } + ] + } + } + }, + "functions/endsWith/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "ends_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Rings"} + ] + } + } + ] + } + ] + } + } + }, + "functions/endsWith/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "ends_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "suffix"} + ] + } + } + ] + } + ] + } + } + }, + "functions/endsWith/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "ends_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Rings"} + ] + } + } + ] + } + ] + } + } + }, + "functions/endsWith/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "ends_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "suffix"} + ] + } + } + ] + } + ] + } + } + }, + "functions/endsWith/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "ends_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Rings"} + ] + } + } + ] + } + ] + } + } + }, + "functions/equal/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/equal/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/equal/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/equal/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/equal/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/equal/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/equal/static-field-name-string": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + ] + } + } + }, + "functions/equalAny/method-array-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + {"fieldReferenceValue": "genres"} + ] + } + } + ] + } + ] + } + } + }, + "functions/equalAny/method-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"stringValue": "Sci-Fi"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/equalAny/method-list-with-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"fieldReferenceValue": "favorite"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/equalAny/static-expression-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"stringValue": "Sci-Fi"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/equalAny/static-field-name-array-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + {"fieldReferenceValue": "genres"} + ] + } + } + ] + } + ] + } + } + }, + "functions/equalAny/static-field-name-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"stringValue": "Sci-Fi"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/equalAny/static-field-name-list-nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + { + "functionValue": { + "name": "array", + "args": [{"stringValue": "Sci-Fi"}] + } + } + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/equalAny/static-field-name-list-with-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"fieldReferenceValue": "favorite"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/euclideanDistance/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "euclidean_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + {"fieldReferenceValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/euclideanDistance/method-number-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "euclidean_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/euclideanDistance/method-vector-value": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "euclidean_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/euclideanDistance/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "euclidean_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + {"fieldReferenceValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/euclideanDistance/static-expression-number-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "euclidean_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/euclideanDistance/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "euclidean_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + {"fieldReferenceValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/euclideanDistance/static-field-name-integer-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "euclidean_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 1}, + {"doubleValue": 2}, + {"doubleValue": 3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/euclideanDistance/static-field-name-number-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "euclidean_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/euclideanDistance/static-field-name-vector-value": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "euclidean_distance", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/exists/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "title"}] + } + } + ] + } + ] + } + } + }, + "functions/exists/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "title"}] + } + } + ] + } + ] + } + } + }, + "functions/exists/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "title"}] + } + } + ] + } + ] + } + } + }, + "functions/exp/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "exp", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/exp/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "exp", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/exp/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "exp", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/floor/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "floor", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/floor/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "floor", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/floor/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "floor", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/geoDistance/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "geo_distance", + "args": [ + {"fieldReferenceValue": "location"}, + {"fieldReferenceValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/geoDistance/method-geo-point": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "geo_distance", + "args": [ + {"fieldReferenceValue": "location"}, + { + "geoPointValue": { + "latitude": 37.7749, + "longitude": -122.4194 + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/geoDistance/static-field-geo-point": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "geo_distance", + "args": [ + {"fieldReferenceValue": "location"}, + { + "geoPointValue": { + "latitude": 37.7749, + "longitude": -122.4194 + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/geoDistance/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "geo_distance", + "args": [ + {"fieldReferenceValue": "location"}, + {"fieldReferenceValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/geoDistance/static-field-name-geo-point": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "geo_distance", + "args": [ + {"fieldReferenceValue": "location"}, + { + "geoPointValue": { + "latitude": 37.7749, + "longitude": -122.4194 + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/getField/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "get_field", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"fieldReferenceValue": "key"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/getField/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "get_field", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/getField/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "get_field", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"fieldReferenceValue": "key"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/getField/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "get_field", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/getField/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "get_field", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"fieldReferenceValue": "key"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/getField/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "get_field", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/greaterThan/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThan/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThan/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThan/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThan/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThan/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThan/static-field-name-string": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThanOrEqual/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThanOrEqual/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThanOrEqual/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThanOrEqual/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThanOrEqual/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThanOrEqual/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/greaterThanOrEqual/static-field-name-string": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than_or_equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + ] + } + } + }, + "functions/ifAbsent/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_absent", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"fieldReferenceValue": "name"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifAbsent/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_absent", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"stringValue": "anonymous"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifAbsent/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_absent", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"fieldReferenceValue": "name"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifAbsent/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_absent", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"stringValue": "anonymous"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifAbsent/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_absent", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"fieldReferenceValue": "name"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifAbsent/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_absent", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"stringValue": "anonymous"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifError/method-boolean-boolean": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "if_error", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + {"booleanValue": false} + ] + } + } + ] + } + ] + } + } + }, + "functions/ifError/method-boolean-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "if_error", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + {"booleanValue": false} + ] + } + } + ] + } + ] + } + } + }, + "functions/ifError/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_error", + "args": [ + { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "0"} + ] + } + }, + {"fieldReferenceValue": "fallback"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifError/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_error", + "args": [ + { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "0"} + ] + } + }, + {"integerValue": "0"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifError/static-boolean-boolean": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "if_error", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + {"booleanValue": false} + ] + } + } + ] + } + ] + } + } + }, + "functions/ifError/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_error", + "args": [ + { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "0"} + ] + } + }, + {"fieldReferenceValue": "fallback"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifError/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_error", + "args": [ + { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "0"} + ] + } + }, + {"integerValue": "0"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifNull/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_null", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"fieldReferenceValue": "name"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifNull/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_null", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"stringValue": "anonymous"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifNull/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_null", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"fieldReferenceValue": "name"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifNull/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_null", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"stringValue": "anonymous"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifNull/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_null", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"fieldReferenceValue": "name"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ifNull/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "if_null", + "args": [ + {"fieldReferenceValue": "nickname"}, + {"stringValue": "anonymous"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/isAbsent/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_absent", + "args": [{"fieldReferenceValue": "title"}] + } + } + ] + } + ] + } + } + }, + "functions/isAbsent/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_absent", + "args": [{"fieldReferenceValue": "title"}] + } + } + ] + } + ] + } + } + }, + "functions/isAbsent/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_absent", + "args": [{"fieldReferenceValue": "title"}] + } + } + ] + } + ] + } + } + }, + "functions/isError/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_error", + "args": [ + { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "0"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/isError/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_error", + "args": [ + { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "0"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "array"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-boolean": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "boolean"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-bytes": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "bytes"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-decimal128": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "decimal128"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-float64": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "float64"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-geo-point": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "geo_point"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-int32": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "int32"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-int64": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "int64"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-map": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "map"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-max-key": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "max_key"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-min-key": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "min_key"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-null": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "null"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-number": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "number"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-object-id": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "object_id"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-reference": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "reference"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-regex": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "regex"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-request-timestamp": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "request_timestamp"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-string": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "string"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-timestamp": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "timestamp"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/method-vector": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "vector"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "string"} + ] + } + } + ] + } + ] + } + } + }, + "functions/isType/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "is_type", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "string"} + ] + } + } + ] + } + ] + } + } + }, + "functions/join/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "join", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "delimiter"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/join/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "join", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": ", "} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/join/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "join", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "delimiter"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/join/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "join", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": ", "} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/join/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "join", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "delimiter"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/join/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "join", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": ", "} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/length/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "length", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/length/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "length", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/length/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "length", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/lessThan/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThan/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThan/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThan/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThan/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThan/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThan/static-field-name-string": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThanOrEqual/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThanOrEqual/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThanOrEqual/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThanOrEqual/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThanOrEqual/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThanOrEqual/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/lessThanOrEqual/static-field-name-string": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than_or_equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + ] + } + } + }, + "functions/like/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "like", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + ] + } + ] + } + } + }, + "functions/like/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "like", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "The%"} + ] + } + } + ] + } + ] + } + } + }, + "functions/like/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "like", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + ] + } + ] + } + } + }, + "functions/like/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "like", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "The%"} + ] + } + } + ] + } + ] + } + } + }, + "functions/like/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "like", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + ] + } + ] + } + } + }, + "functions/like/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "like", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "The%"} + ] + } + } + ] + } + ] + } + } + }, + "functions/ln/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ln", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ln/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ln", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ln/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ln", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/log10/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "log10", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/log10/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "log10", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/log10/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "log10", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/logicalMaximum/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/logicalMaximum/method-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "floor"}, + {"integerValue": "3"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/logicalMaximum/static-expression-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "floor"}, + {"integerValue": "3"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/logicalMaximum/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "maximum", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/logicalMinimum/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "5"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/logicalMinimum/method-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "ceiling"}, + {"integerValue": "3"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/logicalMinimum/static-expression-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "ceiling"}, + {"integerValue": "3"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/logicalMinimum/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "minimum", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "5"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ltrim/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ltrim", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ltrim/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ltrim", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "chars"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ltrim/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ltrim", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "-"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ltrim/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ltrim", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ltrim/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ltrim", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "chars"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ltrim/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ltrim", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/ltrim/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "ltrim", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "-"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/map/static-empty": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"functionValue": {"name": "map"}}} + } + } + ] + } + ] + } + } + }, + "functions/map/static-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "a"}, + {"integerValue": "1"}, + {"stringValue": "b"}, + {"stringValue": "x"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/map/static-nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "inner"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "k"}, + {"fieldReferenceValue": "y"} + ] + } + }, + {"stringValue": "list"}, + { + "functionValue": { + "name": "array", + "args": [ + {"fieldReferenceValue": "z"}, + {"integerValue": "1"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/map/static-with-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "a"}, + {"fieldReferenceValue": "title"}, + {"stringValue": "b"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapEntries/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_entries", + "args": [{"fieldReferenceValue": "metadata"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapEntries/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_entries", + "args": [{"fieldReferenceValue": "metadata"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapEntries/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_entries", + "args": [{"fieldReferenceValue": "metadata"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapGet/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_get", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapGet/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_get", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapGet/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_get", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapKeys/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_keys", + "args": [{"fieldReferenceValue": "metadata"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapKeys/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_keys", + "args": [{"fieldReferenceValue": "metadata"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapKeys/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_keys", + "args": [{"fieldReferenceValue": "metadata"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapMerge/method-map": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_merge", + "args": [ + {"fieldReferenceValue": "metadata"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "extra"}, + {"integerValue": "1"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapMerge/method-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_merge", + "args": [ + {"fieldReferenceValue": "metadata"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "extra"}, + {"fieldReferenceValue": "title"} + ] + } + }, + {"fieldReferenceValue": "overrides"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapMerge/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_merge", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"fieldReferenceValue": "overrides"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapMerge/static-field-name-map": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_merge", + "args": [ + {"fieldReferenceValue": "metadata"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "extra"}, + {"integerValue": "1"} + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapMerge/static-field-name-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_merge", + "args": [ + {"fieldReferenceValue": "metadata"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "extra"}, + {"fieldReferenceValue": "title"} + ] + } + }, + {"fieldReferenceValue": "overrides"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapRemove/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_remove", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"fieldReferenceValue": "key"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapRemove/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_remove", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapRemove/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_remove", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"fieldReferenceValue": "key"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapRemove/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_remove", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapRemove/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_remove", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"fieldReferenceValue": "key"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapRemove/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_remove", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapSet/method-expression-key": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_set", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"fieldReferenceValue": "key"}, + {"fieldReferenceValue": "value"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapSet/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_set", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"}, + {"stringValue": "en"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapSet/method-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_set", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"}, + {"stringValue": "en"}, + {"stringValue": "pages"}, + {"fieldReferenceValue": "pageCount"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapSet/static-expression-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_set", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"}, + {"stringValue": "en"}, + {"stringValue": "pages"}, + {"fieldReferenceValue": "pageCount"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapSet/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_set", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "lang"}, + {"stringValue": "en"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapValues/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_values", + "args": [{"fieldReferenceValue": "metadata"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapValues/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_values", + "args": [{"fieldReferenceValue": "metadata"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mapValues/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "map_values", + "args": [{"fieldReferenceValue": "metadata"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mod/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "mod", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "divisor"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mod/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "mod", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mod/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "mod", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "divisor"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mod/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "mod", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mod/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "mod", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "divisor"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/mod/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "mod", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/multiply/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "weight"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/multiply/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/multiply/method-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"}, + {"fieldReferenceValue": "weight"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/multiply/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "weight"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/multiply/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/multiply/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "weight"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/multiply/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/multiply/static-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/nor/static-three": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "nor", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "published"}, + {"integerValue": "1950"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/nor/static-two": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "nor", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/not/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/not/static": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqual/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqual/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqual/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqual/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqual/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "threshold"} + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqual/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqual/static-field-name-string": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqualAny/method-array-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + {"fieldReferenceValue": "genres"} + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqualAny/method-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"stringValue": "Sci-Fi"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqualAny/method-list-with-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"fieldReferenceValue": "favorite"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqualAny/static-expression-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"stringValue": "Sci-Fi"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqualAny/static-field-name-array-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + {"fieldReferenceValue": "genres"} + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqualAny/static-field-name-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"stringValue": "Sci-Fi"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqualAny/static-field-name-list-nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + { + "functionValue": { + "name": "array", + "args": [{"stringValue": "Sci-Fi"}] + } + } + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/notEqualAny/static-field-name-list-with-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"fieldReferenceValue": "favorite"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/or/static-three": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "published"}, + {"integerValue": "1950"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/or/static-two": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/parent/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "parent", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/parent/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "parent", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/parent/static-path": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "parent", + "args": [{"stringValue": "books/book1"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/parent/static-reference": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "parent", + "args": [ + { + "referenceValue": "projects/test-project/databases/(default)/documents/books/book1" + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/pow/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "pow", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "exponent"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/pow/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "pow", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/pow/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "pow", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "exponent"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/pow/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "pow", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/pow/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "pow", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "exponent"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/pow/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "pow", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/rand/static": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"functionValue": {"name": "rand"}}} + } + } + ] + } + ] + } + } + }, + "functions/regexContains/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + ] + } + ] + } + } + }, + "functions/regexContains/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Lord"} + ] + } + } + ] + } + ] + } + } + }, + "functions/regexContains/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + ] + } + ] + } + } + }, + "functions/regexContains/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Lord"} + ] + } + } + ] + } + ] + } + } + }, + "functions/regexContains/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + ] + } + ] + } + } + }, + "functions/regexContains/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Lord"} + ] + } + } + ] + } + ] + } + } + }, + "functions/regexFind/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexFind/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "[A-Z]\\w+"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexFind/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexFind/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "[A-Z]\\w+"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexFind/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexFind/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "[A-Z]\\w+"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexFindAll/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find_all", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexFindAll/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find_all", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "[A-Z]\\w+"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexFindAll/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find_all", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexFindAll/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find_all", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "[A-Z]\\w+"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexFindAll/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find_all", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexFindAll/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "regex_find_all", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "[A-Z]\\w+"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/regexMatch/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_match", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + ] + } + ] + } + } + }, + "functions/regexMatch/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_match", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "^The.*"} + ] + } + } + ] + } + ] + } + } + }, + "functions/regexMatch/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_match", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + ] + } + ] + } + } + }, + "functions/regexMatch/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_match", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "^The.*"} + ] + } + } + ] + } + ] + } + } + }, + "functions/regexMatch/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_match", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "pattern"} + ] + } + } + ] + } + ] + } + } + }, + "functions/regexMatch/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "regex_match", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "^The.*"} + ] + } + } + ] + } + ] + } + } + }, + "functions/reverse/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "reverse", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/reverse/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "reverse", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/reverse/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "reverse", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/round/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "round", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/round/method-places-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "round", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "places"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/round/method-places-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "round", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/round/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "round", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/round/static-expression-places-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "round", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "places"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/round/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "round", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/round/static-field-name-places-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "round", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/rtrim/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "rtrim", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/rtrim/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "rtrim", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "chars"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/rtrim/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "rtrim", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "-"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/rtrim/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "rtrim", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/rtrim/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "rtrim", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "chars"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/rtrim/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "rtrim", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/rtrim/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "rtrim", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "-"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/score/static": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"functionValue": {"name": "score"}}} + } + } + ] + } + ] + } + } + }, + "functions/split/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "split", + "args": [ + {"fieldReferenceValue": "csv"}, + {"fieldReferenceValue": "delimiter"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/split/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "split", + "args": [ + {"fieldReferenceValue": "csv"}, + {"stringValue": ","} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/split/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "split", + "args": [ + {"fieldReferenceValue": "csv"}, + {"fieldReferenceValue": "delimiter"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/split/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "split", + "args": [ + {"fieldReferenceValue": "csv"}, + {"stringValue": ","} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/split/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "split", + "args": [ + {"fieldReferenceValue": "csv"}, + {"fieldReferenceValue": "delimiter"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/split/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "split", + "args": [ + {"fieldReferenceValue": "csv"}, + {"stringValue": ","} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/sqrt/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "sqrt", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/sqrt/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "sqrt", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/sqrt/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "sqrt", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/startsWith/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "starts_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "prefix"} + ] + } + } + ] + } + ] + } + } + }, + "functions/startsWith/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "starts_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "The"} + ] + } + } + ] + } + ] + } + } + }, + "functions/startsWith/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "starts_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "prefix"} + ] + } + } + ] + } + ] + } + } + }, + "functions/startsWith/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "starts_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "The"} + ] + } + } + ] + } + ] + } + } + }, + "functions/startsWith/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "starts_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "prefix"} + ] + } + } + ] + } + ] + } + } + }, + "functions/startsWith/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "starts_with", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "The"} + ] + } + } + ] + } + ] + } + } + }, + "functions/stringConcat/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_concat", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": " by "} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringConcat/method-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_concat", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": " by "}, + {"fieldReferenceValue": "author"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringConcat/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_concat", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "author"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringConcat/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_concat", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": " by "} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringConcat/static-field-name-variadic": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_concat", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": " by "}, + {"fieldReferenceValue": "author"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringContains/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "string_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "needle"} + ] + } + } + ] + } + ] + } + } + }, + "functions/stringContains/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "string_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Ring"} + ] + } + } + ] + } + ] + } + } + }, + "functions/stringContains/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "string_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "needle"} + ] + } + } + ] + } + ] + } + } + }, + "functions/stringContains/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "string_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Ring"} + ] + } + } + ] + } + ] + } + } + }, + "functions/stringContains/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "string_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "needle"} + ] + } + } + ] + } + ] + } + } + }, + "functions/stringContains/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "string_contains", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Ring"} + ] + } + } + ] + } + ] + } + } + }, + "functions/stringIndexOf/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_index_of", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "needle"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringIndexOf/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_index_of", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Ring"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringIndexOf/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_index_of", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "needle"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringIndexOf/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_index_of", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Ring"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringIndexOf/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_index_of", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "needle"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringIndexOf/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_index_of", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "Ring"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringRepeat/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_repeat", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "times"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringRepeat/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_repeat", + "args": [ + {"fieldReferenceValue": "title"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringRepeat/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_repeat", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "times"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringRepeat/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_repeat", + "args": [ + {"fieldReferenceValue": "title"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringRepeat/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_repeat", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "times"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringRepeat/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_repeat", + "args": [ + {"fieldReferenceValue": "title"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringReplaceAll/method-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_replace_all", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "find"}, + {"fieldReferenceValue": "replacement"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringReplaceAll/method-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_replace_all", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "a"}, + {"stringValue": "b"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringReplaceAll/static-expression-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_replace_all", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "find"}, + {"fieldReferenceValue": "replacement"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringReplaceAll/static-field-name-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_replace_all", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "a"}, + {"stringValue": "b"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringReplaceOne/method-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_replace_one", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "find"}, + {"fieldReferenceValue": "replacement"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringReplaceOne/method-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_replace_one", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "a"}, + {"stringValue": "b"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringReplaceOne/static-expression-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_replace_one", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "find"}, + {"fieldReferenceValue": "replacement"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringReplaceOne/static-field-name-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_replace_one", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "a"}, + {"stringValue": "b"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringReverse/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_reverse", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringReverse/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_reverse", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/stringReverse/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "string_reverse", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/substring/method-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "substring", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "start"}, + {"fieldReferenceValue": "count"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/substring/method-position": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "substring", + "args": [ + {"fieldReferenceValue": "title"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/substring/method-position-length": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "substring", + "args": [ + {"fieldReferenceValue": "title"}, + {"integerValue": "2"}, + {"integerValue": "3"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/substring/static-expression-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "substring", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "start"}, + {"fieldReferenceValue": "count"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/substring/static-field-name-position": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "substring", + "args": [ + {"fieldReferenceValue": "title"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/substring/static-field-name-position-length": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "substring", + "args": [ + {"fieldReferenceValue": "title"}, + {"integerValue": "2"}, + {"integerValue": "3"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/subtract/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "subtract", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "penalty"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/subtract/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "subtract", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/subtract/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "subtract", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "penalty"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/subtract/static-expression-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "subtract", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/subtract/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "subtract", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "penalty"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/subtract/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "subtract", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/switchOn/static-pair": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "switch_on", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + {"stringValue": "good"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/switchOn/static-pairs-and-default": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "switch_on", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + {"stringValue": "good"}, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "published"}, + {"integerValue": "1950"} + ] + } + }, + {"stringValue": "old"}, + {"stringValue": "other"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampAdd/method-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_add", + "args": [ + {"fieldReferenceValue": "published"}, + {"fieldReferenceValue": "unit"}, + {"fieldReferenceValue": "amount"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampAdd/method-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_add", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "minute"}, + {"integerValue": "30"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampAdd/static-expression-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_add", + "args": [ + {"fieldReferenceValue": "published"}, + {"fieldReferenceValue": "unit"}, + {"fieldReferenceValue": "amount"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampAdd/static-expression-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_add", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "hour"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampAdd/static-field-name-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_add", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "day"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampDiff/method-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_diff", + "args": [ + {"fieldReferenceValue": "end"}, + {"fieldReferenceValue": "start"}, + {"fieldReferenceValue": "unit"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampDiff/method-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_diff", + "args": [ + {"fieldReferenceValue": "end"}, + {"fieldReferenceValue": "start"}, + {"stringValue": "day"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampDiff/static-expression-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_diff", + "args": [ + {"fieldReferenceValue": "end"}, + {"fieldReferenceValue": "start"}, + {"stringValue": "hour"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampDiff/static-expressions-unit-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_diff", + "args": [ + {"fieldReferenceValue": "end"}, + {"fieldReferenceValue": "start"}, + {"fieldReferenceValue": "unit"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampDiff/static-field-name-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_diff", + "args": [ + {"fieldReferenceValue": "end"}, + {"fieldReferenceValue": "start"}, + {"stringValue": "second"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampDiff/static-field-names": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_diff", + "args": [ + {"fieldReferenceValue": "end"}, + {"fieldReferenceValue": "start"}, + {"stringValue": "day"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampExtract/method-expression-timezone-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_extract", + "args": [ + {"fieldReferenceValue": "published"}, + {"fieldReferenceValue": "part"}, + {"fieldReferenceValue": "tz"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampExtract/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_extract", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "dayofyear"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampExtract/method-literal-timezone": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_extract", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "hour"}, + {"stringValue": "UTC"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampExtract/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_extract", + "args": [ + {"fieldReferenceValue": "published"}, + {"fieldReferenceValue": "part"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampExtract/static-expression-timezone-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_extract", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "month"}, + {"fieldReferenceValue": "tz"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampExtract/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_extract", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "year"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampExtract/static-field-name-literal-timezone": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_extract", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "dayofweek"}, + {"stringValue": "America/New_York"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampSubtract/method-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_subtract", + "args": [ + {"fieldReferenceValue": "published"}, + {"fieldReferenceValue": "unit"}, + {"fieldReferenceValue": "amount"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampSubtract/method-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_subtract", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "minute"}, + {"integerValue": "30"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampSubtract/static-expression-expressions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_subtract", + "args": [ + {"fieldReferenceValue": "published"}, + {"fieldReferenceValue": "unit"}, + {"fieldReferenceValue": "amount"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampSubtract/static-expression-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_subtract", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "hour"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampSubtract/static-field-name-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_subtract", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "day"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampToUnixMicros/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_to_unix_micros", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampToUnixMicros/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_to_unix_micros", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampToUnixMicros/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_to_unix_micros", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampToUnixMillis/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_to_unix_millis", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampToUnixMillis/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_to_unix_millis", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampToUnixMillis/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_to_unix_millis", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampToUnixSeconds/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_to_unix_seconds", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampToUnixSeconds/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_to_unix_seconds", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampToUnixSeconds/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_to_unix_seconds", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampTruncate/method-expression-timezone-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_trunc", + "args": [ + {"fieldReferenceValue": "published"}, + {"fieldReferenceValue": "granularity"}, + {"fieldReferenceValue": "tz"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampTruncate/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_trunc", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "year"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampTruncate/method-literal-timezone": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_trunc", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "day"}, + {"stringValue": "UTC"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampTruncate/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_trunc", + "args": [ + {"fieldReferenceValue": "published"}, + {"fieldReferenceValue": "granularity"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampTruncate/static-expression-timezone-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_trunc", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "month"}, + {"fieldReferenceValue": "tz"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampTruncate/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_trunc", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "day"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/timestampTruncate/static-field-name-literal-timezone": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "timestamp_trunc", + "args": [ + {"fieldReferenceValue": "published"}, + {"stringValue": "week(monday)"}, + {"stringValue": "America/New_York"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/toLower/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "to_lower", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/toLower/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "to_lower", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/toLower/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "to_lower", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/toUpper/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "to_upper", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/toUpper/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "to_upper", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/toUpper/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "to_upper", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trim/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trim", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trim/method-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trim", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "chars"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trim/method-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trim", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "-"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trim/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trim", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trim/static-expression-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trim", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "chars"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trim/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trim", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trim/static-field-name-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trim", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "-"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trunc/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trunc", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trunc/method-places-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trunc", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "places"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trunc/method-places-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trunc", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trunc/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trunc", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trunc/static-expression-places-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trunc", + "args": [ + {"fieldReferenceValue": "rating"}, + {"fieldReferenceValue": "places"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trunc/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trunc", + "args": [{"fieldReferenceValue": "rating"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/trunc/static-field-name-places-literal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "trunc", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/type/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "type", + "args": [{"fieldReferenceValue": "value"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/type/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "type", + "args": [{"fieldReferenceValue": "value"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/type/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "type", + "args": [{"fieldReferenceValue": "value"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/unixMicrosToTimestamp/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "unix_micros_to_timestamp", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/unixMicrosToTimestamp/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "unix_micros_to_timestamp", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/unixMicrosToTimestamp/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "unix_micros_to_timestamp", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/unixMillisToTimestamp/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "unix_millis_to_timestamp", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/unixMillisToTimestamp/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "unix_millis_to_timestamp", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/unixMillisToTimestamp/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "unix_millis_to_timestamp", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/unixSecondsToTimestamp/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "unix_seconds_to_timestamp", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/unixSecondsToTimestamp/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "unix_seconds_to_timestamp", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/unixSecondsToTimestamp/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "unix_seconds_to_timestamp", + "args": [{"fieldReferenceValue": "published"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/vectorLength/method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "vector_length", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/vectorLength/static-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "vector_length", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/vectorLength/static-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "functionValue": { + "name": "vector_length", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "functions/xor/static-three": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "xor", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "published"}, + {"integerValue": "1950"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "functions/xor/static-two": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "xor", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + } + ] + } + } + } + } +} diff --git a/packages/google_cloud_firestore/test/fixtures/pipeline_golden/options.json b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/options.json new file mode 100644 index 00000000..52f5d5a5 --- /dev/null +++ b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/options.json @@ -0,0 +1,313 @@ +{ + "generator": "tool/pipeline_golden (npm run generate)", + "sdk": "@google-cloud/firestore@9.3.1", + "cases": { + "options/combined/index-mode-and-explain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "explain_options": { + "mapValue": { + "fields": { + "mode": {"stringValue": "analyze"}, + "output_format": {"stringValue": "text"} + } + } + }, + "index_mode": {"stringValue": "recommended"} + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/explain/empty": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": {"explain_options": {"mapValue": {}}}, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/explain/mode-analyze": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "explain_options": { + "mapValue": {"fields": {"mode": {"stringValue": "analyze"}}} + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/explain/mode-and-output-format": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "explain_options": { + "mapValue": { + "fields": { + "mode": {"stringValue": "analyze"}, + "output_format": {"stringValue": "text"} + } + } + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/explain/mode-execute": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "explain_options": { + "mapValue": {"fields": {"mode": {"stringValue": "execute"}}} + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/explain/output-format-text": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "explain_options": { + "mapValue": {"fields": {"output_format": {"stringValue": "text"}}} + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/index-mode/recommended": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": {"index_mode": {"stringValue": "recommended"}}, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/none/empty-object": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/none/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/raw-options/dot-notation": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "explain_options": { + "mapValue": {"fields": {"mode": {"stringValue": "analyze"}}} + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/raw-options/dot-notation-deep": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "a": { + "mapValue": { + "fields": { + "b": {"mapValue": {"fields": {"c": {"booleanValue": true}}}} + } + } + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/raw-options/dot-notation-into-empty-explain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "explain_options": { + "mapValue": {"fields": {"mode": {"stringValue": "analyze"}}} + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/raw-options/dot-notation-overrides-known": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "explain_options": { + "mapValue": { + "fields": { + "mode": {"stringValue": "execute"}, + "output_format": {"stringValue": "text"} + } + } + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/raw-options/dot-notation-replaces-known-scalar": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "index_mode": { + "mapValue": {"fields": {"mode": {"stringValue": "custom"}}} + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/raw-options/dot-notation-siblings": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "outer": { + "mapValue": { + "fields": { + "a": {"integerValue": "1"}, + "b": {"stringValue": "two"} + } + } + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/raw-options/flat": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "count": {"integerValue": "3"}, + "foo": {"stringValue": "bar"} + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/raw-options/merge-into-explain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "explain_options": { + "mapValue": { + "fields": { + "mode": {"stringValue": "analyze"}, + "output_format": {"stringValue": "text"} + } + } + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/raw-options/nested-object": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "explain_options": { + "mapValue": {"fields": {"mode": {"stringValue": "analyze"}}} + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/raw-options/object-replaces-known": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": { + "explain_options": { + "mapValue": {"fields": {"mode": {"stringValue": "execute"}}} + } + }, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "options/raw-options/override-known": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "options": {"index_mode": {"stringValue": "custom"}}, + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + } + } +} diff --git a/packages/google_cloud_firestore/test/fixtures/pipeline_golden/queries.json b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/queries.json new file mode 100644 index 00000000..6059e661 --- /dev/null +++ b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/queries.json @@ -0,0 +1,6143 @@ +{ + "generator": "tool/pipeline_golden (npm run generate)", + "sdk": "@google-cloud/firestore@9.3.1", + "cases": { + "queries/collection-group/not-equal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "collection_group", + "args": [{"referenceValue": ""}, {"stringValue": "reviews"}] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "3"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/collection-group/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "collection_group", + "args": [{"referenceValue": ""}, {"stringValue": "reviews"}] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/collection-group/where": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "collection_group", + "args": [{"referenceValue": ""}, {"stringValue": "reviews"}] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "3"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/collection/nested-path": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "collection", + "args": [{"referenceValue": "/authors/author1/books"}] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/collection/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/cursor/descending-limit-to-last": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + } + ] + } + } + ] + }, + {"name": "limit", "args": [{"integerValue": "2"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/cursor/end-at": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "queries/cursor/end-before": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "queries/cursor/end-before-descending": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + ] + } + ] + } + } + }, + "queries/cursor/limit-to-last": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "5"} + ] + } + } + ] + }, + {"name": "limit", "args": [{"integerValue": "3"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/cursor/mixed-directions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "title"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "title"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "M"} + ] + } + } + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "queries/cursor/multiple-fields": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "title"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "title"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "M"} + ] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "title"}, + {"stringValue": "M"} + ] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "queries/cursor/start-after": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "queries/cursor/start-and-end": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "queries/cursor/start-at": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "queries/cursor/start-at-descending": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "queries/cursor/with-inequality": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Horror"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "queries/cursor/with-inequality-and-limit-to-last": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + {"arrayValue": {"values": [{"stringValue": "Horror"}]}} + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + }, + {"name": "limit", "args": [{"integerValue": "3"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/limit-to-last/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + {"name": "limit", "args": [{"integerValue": "5"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/limit-to-last/with-inequality": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Horror"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + {"name": "limit", "args": [{"integerValue": "2"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/limit/with-order-by": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + {"name": "limit", "args": [{"integerValue": "5"}]} + ] + } + } + }, + "queries/limit/without-order-by": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + {"name": "limit", "args": [{"integerValue": "5"}]} + ] + } + } + }, + "queries/offset/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + {"name": "offset", "args": [{"integerValue": "10"}]} + ] + } + } + }, + "queries/offset/with-limit": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + {"name": "limit", "args": [{"integerValue": "5"}]}, + {"name": "offset", "args": [{"integerValue": "10"}]} + ] + } + } + }, + "queries/order-by/ascending": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/descending": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/document-id": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/document-id-and-inequality": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "__name__"}, + { + "referenceValue": "projects/test-project/databases/(default)/documents/books/book1" + } + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "5"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/explicit-and-inequality": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "title"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "title"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/field-path-object": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "`a.b`"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "`a.b`"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/implicit-field-path-order": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "`price-tier`"}] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "`price-tier`"}, + {"integerValue": "3"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "price.amount"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "price.amount"}, + {"integerValue": "1"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "price.amount"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "`price-tier`"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/implicit-from-composite-or": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + }, + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Horror"}, + {"stringValue": "Romance"} + ] + } + } + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/implicit-from-inequalities": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "published"}] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "published"}, + {"integerValue": "2000"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "published"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/implicit-from-inequality": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/implicit-from-mixed-inequalities": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Horror"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/implicit-from-repeated-field": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "5"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/implicit-quoted-segments": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "field"}] + } + }, + { + "functionValue": { + "name": "greater_than_or_equal", + "args": [ + {"fieldReferenceValue": "field"}, + {"stringValue": "field 100"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "`field.dot`"}] + } + }, + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "`field.dot`"}, + {"integerValue": "300"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "`field\\\\slash`"}] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "`field\\\\slash`"}, + {"integerValue": "400"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "name"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "name"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "field"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "`field.dot`"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "`field\\\\slash`"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/multiple": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "title"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "title"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/not-equal-explicitly-ordered": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Horror"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/not-in-and-descending": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Horror"}, + {"stringValue": "Romance"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/order-by/special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "`first-name`"}] + } + }, + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "`first-name`"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/select/empty": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"__name__": {"fieldReferenceValue": "__name__"}} + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/select/fields": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "author": {"fieldReferenceValue": "author"}, + "title": {"fieldReferenceValue": "title"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/select/special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "`\\`first-name\\``": { + "fieldReferenceValue": "`\\`first-name\\``" + }, + "`\\`last name\\``": { + "fieldReferenceValue": "`\\`last name\\``" + } + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/vector/cosine": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "cosine"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/distance-result-field": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/distance-threshold": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/distance-threshold-and-result-field": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/distance-threshold-cosine": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "cosine"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/distance-threshold-dot-product": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "dot_product"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/dot-product": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "dot_product"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/euclidean": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/field-path-object": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "`a.b`"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "`a.b`"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "`my embedding`"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "`my embedding`"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/vector-value": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/with-inequality-prefilter": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Horror"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/vector/with-prefilter": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "embedding"}] + } + } + ] + }, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": {"limit": {"integerValue": "5"}} + } + ] + } + } + }, + "queries/where/array-contains": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "tags"}] + } + }, + { + "functionValue": { + "name": "array_contains", + "args": [ + {"fieldReferenceValue": "tags"}, + {"stringValue": "magic"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/array-contains-any": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "tags"}] + } + }, + { + "functionValue": { + "name": "array_contains_any", + "args": [ + {"fieldReferenceValue": "tags"}, + { + "arrayValue": { + "values": [ + {"stringValue": "magic"}, + {"stringValue": "epic"} + ] + } + } + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/composite-and": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + }, + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/composite-nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + }, + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [ + {"fieldReferenceValue": "rating"} + ] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + } + }, + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "published"}] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "published"}, + {"integerValue": "1950"} + ] + } + } + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "published"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/composite-or": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "or", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + }, + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/document-id": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "__name__"}, + { + "referenceValue": "projects/test-project/databases/(default)/documents/books/book1" + } + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/double": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"doubleValue": 4.5} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/equal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/equal-nan": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"doubleValue": "NaN"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/equal-null": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"nullValue": null} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/field-path-object": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "`a.b`"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "`a.b`"}, + {"integerValue": "1"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/greater-than": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/greater-than-or-equal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/in": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"stringValue": "Sci-Fi"} + ] + } + } + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/less-than": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/less-than-or-equal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "less_than_or_equal", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/multiple": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/nested-field": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "metadata.lang"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "metadata.lang"}, + {"stringValue": "en"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/not-equal": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/not-equal-null": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "genre"}] + } + }, + { + "functionValue": { + "name": "not_equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"nullValue": null} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/not-in": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "not_equal_any", + "args": [ + {"fieldReferenceValue": "genre"}, + { + "arrayValue": { + "values": [ + {"stringValue": "Fantasy"}, + {"stringValue": "Sci-Fi"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "genre"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "`first-name`"}] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "`first-name`"}, + {"stringValue": "Ada"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + }, + "queries/where/timestamp": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "published"}] + } + }, + { + "functionValue": { + "name": "less_than", + "args": [ + {"fieldReferenceValue": "published"}, + {"timestampValue": "2023-11-14T22:13:20.123456789Z"} + ] + } + } + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "exists", + "args": [{"fieldReferenceValue": "__name__"}] + } + } + ] + }, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "published"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "__name__"} + } + } + } + ] + } + ] + } + } + } + } +} diff --git a/packages/google_cloud_firestore/test/fixtures/pipeline_golden/sources.json b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/sources.json new file mode 100644 index 00000000..1ae3ae87 --- /dev/null +++ b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/sources.json @@ -0,0 +1,291 @@ +{ + "generator": "tool/pipeline_golden (npm run generate)", + "sdk": "@google-cloud/firestore@9.3.1", + "cases": { + "sources/collection-group/force-index": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "collection_group", + "args": [{"referenceValue": ""}, {"stringValue": "reviews"}], + "options": {"force_index": {"stringValue": "my_index"}} + } + ] + } + } + }, + "sources/collection-group/id": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "collection_group", + "args": [{"referenceValue": ""}, {"stringValue": "reviews"}] + } + ] + } + } + }, + "sources/collection-group/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "collection_group", + "args": [{"referenceValue": ""}, {"stringValue": "reviews"}], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "sources/collection/force-index": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "collection", + "args": [{"referenceValue": "/books"}], + "options": {"force_index": {"stringValue": "my_index"}} + } + ] + } + } + }, + "sources/collection/nested-path": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "collection", + "args": [{"referenceValue": "/authors/author1/books"}] + } + ] + } + } + }, + "sources/collection/path": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "sources/collection/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "collection", + "args": [{"referenceValue": "/books"}], + "options": { + "foo": {"stringValue": "bar"}, + "outer": { + "mapValue": {"fields": {"inner": {"integerValue": "1"}}} + } + } + } + ] + } + } + }, + "sources/collection/reference": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]} + ] + } + } + }, + "sources/collection/reference-raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "collection", + "args": [{"referenceValue": "/books"}], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "sources/database/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": {"pipeline": {"stages": [{"name": "database"}]}} + }, + "sources/database/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "database", "options": {"foo": {"stringValue": "bar"}}} + ] + } + } + }, + "sources/documents/leading-slash-path": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "documents", "args": [{"referenceValue": "/books/book1"}]} + ] + } + } + }, + "sources/documents/nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "documents", + "args": [{"referenceValue": "/authors/author1/books/book1"}] + } + ] + } + } + }, + "sources/documents/nested-path": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "documents", + "args": [{"referenceValue": "/authors/author1/books/book1"}] + } + ] + } + } + }, + "sources/documents/paths": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "documents", + "args": [ + {"referenceValue": "/books/book1"}, + {"referenceValue": "/books/book2"} + ] + } + ] + } + } + }, + "sources/documents/paths-and-references": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "documents", + "args": [ + {"referenceValue": "/books/book1"}, + {"referenceValue": "/books/book2"} + ] + } + ] + } + } + }, + "sources/documents/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "documents", + "args": [{"referenceValue": "/books/book1"}], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "sources/documents/references": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + { + "name": "documents", + "args": [ + {"referenceValue": "/books/book1"}, + {"referenceValue": "/books/book2"} + ] + } + ] + } + } + }, + "sources/subcollection/as-array-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "add_fields", + "args": [ + { + "mapValue": { + "fields": { + "reviews": { + "functionValue": { + "name": "array", + "args": [ + { + "pipelineValue": { + "stages": [ + { + "name": "subcollection", + "args": [{"stringValue": "reviews"}] + }, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "rating": { + "fieldReferenceValue": "rating" + } + } + } + } + ] + } + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + } + } +} diff --git a/packages/google_cloud_firestore/test/fixtures/pipeline_golden/stages.json b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/stages.json new file mode 100644 index 00000000..6b46458a --- /dev/null +++ b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/stages.json @@ -0,0 +1,2866 @@ +{ + "generator": "tool/pipeline_golden (npm run generate)", + "sdk": "@google-cloud/firestore@9.3.1", + "cases": { + "stages/add-fields/field": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "add_fields", + "args": [ + { + "mapValue": { + "fields": {"rating": {"fieldReferenceValue": "rating"}} + } + } + ] + } + ] + } + } + }, + "stages/add-fields/multiple": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "add_fields", + "args": [ + { + "mapValue": { + "fields": { + "annotated": {"booleanValue": true}, + "copiedRating": {"fieldReferenceValue": "rating"} + } + } + } + ] + } + ] + } + } + }, + "stages/add-fields/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "add_fields", + "args": [ + { + "mapValue": { + "fields": { + "copiedRating": {"fieldReferenceValue": "rating"} + } + } + } + ], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "stages/add-fields/single": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "add_fields", + "args": [ + { + "mapValue": { + "fields": { + "copiedRating": {"fieldReferenceValue": "rating"} + } + } + } + ] + } + ] + } + } + }, + "stages/aggregate/groups-field-names": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": {"total": {"functionValue": {"name": "count"}}} + } + }, + { + "mapValue": { + "fields": {"genre": {"fieldReferenceValue": "genre"}} + } + } + ] + } + ] + } + } + }, + "stages/aggregate/groups-mixed": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "best": { + "functionValue": { + "name": "maximum", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + "total": {"functionValue": {"name": "count"}} + } + } + }, + { + "mapValue": { + "fields": { + "author": {"fieldReferenceValue": "author"}, + "decade": { + "functionValue": { + "name": "floor", + "args": [ + { + "functionValue": { + "name": "divide", + "args": [ + {"fieldReferenceValue": "published"}, + {"integerValue": "10"} + ] + } + } + ] + } + }, + "genre": {"fieldReferenceValue": "genre"} + } + } + } + ] + } + ] + } + } + }, + "stages/aggregate/groups-special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": {"total": {"functionValue": {"name": "count"}}} + } + }, + { + "mapValue": { + "fields": { + "`last name`": {"fieldReferenceValue": "`last name`"}, + "first-name": {"fieldReferenceValue": "`first-name`"} + } + } + } + ] + } + ] + } + } + }, + "stages/aggregate/multiple": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "averageRating": { + "functionValue": { + "name": "average", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + "total": {"functionValue": {"name": "count"}} + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "stages/aggregate/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": {"total": {"functionValue": {"name": "count"}}} + } + }, + { + "mapValue": { + "fields": {"genre": {"fieldReferenceValue": "genre"}} + } + } + ], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "stages/aggregate/single": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": {"total": {"functionValue": {"name": "count"}}} + } + }, + {"mapValue": {}} + ] + } + ] + } + } + }, + "stages/define/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "let", + "args": [ + { + "mapValue": { + "fields": { + "currentRating": {"fieldReferenceValue": "rating"} + } + } + } + ] + } + ] + } + } + }, + "stages/delete/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + {"name": "delete"} + ] + } + } + }, + "stages/distinct/aliased": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "distinct", + "args": [ + { + "mapValue": { + "fields": { + "lowerGenre": { + "functionValue": { + "name": "to_lower", + "args": [{"fieldReferenceValue": "genre"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "stages/distinct/field-names": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "distinct", + "args": [ + { + "mapValue": { + "fields": { + "author": {"fieldReferenceValue": "author"}, + "genre": {"fieldReferenceValue": "genre"} + } + } + } + ] + } + ] + } + } + }, + "stages/distinct/fields": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "distinct", + "args": [ + { + "mapValue": { + "fields": {"genre": {"fieldReferenceValue": "genre"}} + } + } + ] + } + ] + } + } + }, + "stages/distinct/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "distinct", + "args": [ + { + "mapValue": { + "fields": {"genre": {"fieldReferenceValue": "genre"}} + } + } + ], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "stages/distinct/special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "distinct", + "args": [ + { + "mapValue": { + "fields": { + "`last name`": {"fieldReferenceValue": "`last name`"}, + "first-name": {"fieldReferenceValue": "`first-name`"} + } + } + } + ] + } + ] + } + } + }, + "stages/find-nearest/all-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": { + "distance_field": {"fieldReferenceValue": "distance"}, + "limit": {"integerValue": "10"} + } + } + ] + } + } + }, + "stages/find-nearest/cosine": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "cosine"} + ] + } + ] + } + } + }, + "stages/find-nearest/distance-field": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": {"distance_field": {"fieldReferenceValue": "distance"}} + } + ] + } + } + }, + "stages/find-nearest/dot-product": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "dot_product"} + ] + } + ] + } + } + }, + "stages/find-nearest/field": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ] + } + ] + } + } + }, + "stages/find-nearest/integer-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 1}, + {"doubleValue": 2}, + {"doubleValue": 3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ] + } + ] + } + } + }, + "stages/find-nearest/limit": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": {"limit": {"integerValue": "10"}} + } + ] + } + } + }, + "stages/find-nearest/number-array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ] + } + ] + } + } + }, + "stages/find-nearest/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": { + "distance_field": {"fieldReferenceValue": "distance"}, + "extra": { + "mapValue": {"fields": {"flag": {"booleanValue": true}}} + }, + "limit": {"integerValue": "20"} + } + } + ] + } + } + }, + "stages/find-nearest/special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "`my embedding`"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": { + "distance_field": {"fieldReferenceValue": "`my distance`"} + } + } + ] + } + } + }, + "stages/find-nearest/vector-value": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ] + } + ] + } + } + }, + "stages/limit/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + {"name": "limit", "args": [{"integerValue": "10"}]} + ] + } + } + }, + "stages/limit/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "limit", + "args": [{"integerValue": "10"}], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "stages/limit/zero": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + {"name": "limit", "args": [{"integerValue": "0"}]} + ] + } + } + }, + "stages/offset/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + {"name": "offset", "args": [{"integerValue": "10"}]} + ] + } + } + }, + "stages/offset/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "offset", + "args": [{"integerValue": "10"}], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "stages/raw-stage/aggregate-params": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "best": { + "functionValue": { + "name": "maximum", + "args": [{"fieldReferenceValue": "rating"}] + } + }, + "total": {"functionValue": {"name": "count"}} + } + } + }, + { + "mapValue": { + "fields": {"genre": {"fieldReferenceValue": "genre"}} + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/array-in-object-param": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + { + "mapValue": { + "fields": { + "tags": { + "functionValue": { + "name": "array", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "classic"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/array-in-object-param-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + { + "mapValue": { + "fields": { + "tags": { + "functionValue": { + "name": "array", + "args": [{"integerValue": "1"}, {"integerValue": "2"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/array-param": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + { + "arrayValue": { + "values": [ + {"integerValue": "1"}, + {"stringValue": "a"}, + {"fieldReferenceValue": "title"} + ] + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/array-param-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + { + "arrayValue": { + "values": [ + {"integerValue": "1"}, + {"integerValue": "2"}, + {"integerValue": "3"} + ] + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/constant-in-object-param": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + { + "mapValue": { + "fields": { + "meta": { + "mapValue": {"fields": {"lang": {"stringValue": "en"}}} + } + } + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/deeply-nested-object-param": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + { + "mapValue": { + "fields": { + "a": { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "b"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "c"}, + {"fieldReferenceValue": "x"} + ] + } + }, + {"stringValue": "d"}, + { + "functionValue": { + "name": "array", + "args": [ + {"fieldReferenceValue": "y"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "e"}, + {"integerValue": "1"} + ] + } + } + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/empty-object-param": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + {"name": "custom", "args": [{"mapValue": {}}]} + ] + } + } + }, + "stages/raw-stage/expression-params": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/literal-params": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + {"name": "limit", "args": [{"integerValue": "10"}]} + ] + } + } + }, + "stages/raw-stage/nested-array-param": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + { + "arrayValue": { + "values": [ + {"integerValue": "1"}, + { + "arrayValue": { + "values": [ + {"integerValue": "2"}, + {"fieldReferenceValue": "x"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/nested-empty-object-param": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + { + "mapValue": { + "fields": {"meta": {"functionValue": {"name": "map"}}} + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/nested-plain-object-param": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "meta": { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "lang"}, + {"fieldReferenceValue": "lang"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/nested-plain-object-param-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + { + "mapValue": { + "fields": { + "meta": { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "lang"}, + {"stringValue": "en"}, + {"stringValue": "year"}, + {"integerValue": "2020"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/object-in-array-param": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + { + "arrayValue": { + "values": [ + { + "mapValue": { + "fields": { + "a": {"fieldReferenceValue": "x"}, + "b": { + "mapValue": { + "fields": {"c": {"integerValue": "1"}} + } + } + } + } + } + ] + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "find_nearest", + "args": [ + {"fieldReferenceValue": "embedding"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "euclidean"} + ], + "options": { + "distance_field": {"fieldReferenceValue": "distance"}, + "limit": {"integerValue": "10"} + } + } + ] + } + } + }, + "stages/raw-stage/options-dot-notation": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "options": { + "outer": { + "mapValue": {"fields": {"inner": {"stringValue": "value"}}} + } + } + } + ] + } + } + }, + "stages/raw-stage/options-dot-notation-backticks": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "options": { + "a": { + "mapValue": { + "fields": { + "`b": { + "mapValue": {"fields": {"c`": {"integerValue": "1"}}} + } + } + } + } + } + } + ] + } + } + }, + "stages/raw-stage/options-dot-notation-deep": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "options": { + "a": { + "mapValue": { + "fields": { + "b": { + "mapValue": {"fields": {"c": {"booleanValue": true}}} + } + } + } + } + } + } + ] + } + } + }, + "stages/raw-stage/options-dot-notation-merges-object": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "options": { + "outer": { + "mapValue": { + "fields": { + "a": {"integerValue": "1"}, + "b": {"integerValue": "2"} + } + } + } + } + } + ] + } + } + }, + "stages/raw-stage/options-dot-notation-replaces-scalar": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "options": { + "outer": {"mapValue": {"fields": {"a": {"integerValue": "1"}}}} + } + } + ] + } + } + }, + "stages/raw-stage/options-dot-notation-siblings": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "options": { + "outer": { + "mapValue": { + "fields": { + "a": {"integerValue": "1"}, + "b": {"integerValue": "2"} + } + } + } + } + } + ] + } + } + }, + "stages/raw-stage/options-expression-values": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "options": { + "nested": { + "mapValue": { + "fields": {"target": {"fieldReferenceValue": "y"}} + } + }, + "target": {"fieldReferenceValue": "x"} + } + } + ] + } + } + }, + "stages/raw-stage/options-nested-object": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "options": { + "outer": { + "mapValue": { + "fields": { + "inner": {"stringValue": "value"}, + "n": {"integerValue": "1"} + } + } + } + } + } + ] + } + } + }, + "stages/raw-stage/options-object-replaces-dot-notation": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "options": { + "outer": {"mapValue": {"fields": {"b": {"integerValue": "2"}}}} + } + } + ] + } + } + }, + "stages/raw-stage/plain-object-param": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "one": {"integerValue": "1"}, + "title": {"fieldReferenceValue": "title"} + } + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/plain-object-param-literals": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + { + "mapValue": { + "fields": { + "a": {"integerValue": "1"}, + "b": {"stringValue": "x"}, + "c": {"nullValue": null} + } + } + } + ] + } + ] + } + } + }, + "stages/raw-stage/string-params": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "custom", + "args": [ + {"stringValue": "a"}, + {"doubleValue": 1.5}, + {"booleanValue": true}, + {"nullValue": null} + ] + } + ] + } + } + }, + "stages/remove-fields/field-names": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "remove_fields", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "author"} + ] + } + ] + } + } + }, + "stages/remove-fields/fields": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "remove_fields", + "args": [ + {"fieldReferenceValue": "title"}, + {"fieldReferenceValue": "author"} + ] + } + ] + } + } + }, + "stages/remove-fields/nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "remove_fields", + "args": [{"fieldReferenceValue": "metadata.lang"}] + } + ] + } + } + }, + "stages/remove-fields/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "remove_fields", + "args": [{"fieldReferenceValue": "title"}], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "stages/remove-fields/special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "remove_fields", + "args": [ + {"fieldReferenceValue": "`first-name`"}, + {"fieldReferenceValue": "`last name`"} + ] + } + ] + } + } + }, + "stages/replace-with/field": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "replace_with", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "full_replace"} + ] + } + ] + } + } + }, + "stages/replace-with/field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "replace_with", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "full_replace"} + ] + } + ] + } + } + }, + "stages/replace-with/map-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "replace_with", + "args": [ + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "title"}, + {"fieldReferenceValue": "title"}, + {"stringValue": "rating"}, + {"fieldReferenceValue": "rating"} + ] + } + }, + {"stringValue": "full_replace"} + ] + } + ] + } + } + }, + "stages/replace-with/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "replace_with", + "args": [ + {"fieldReferenceValue": "metadata"}, + {"stringValue": "full_replace"} + ], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "stages/sample/documents": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sample", + "args": [{"integerValue": "10"}, {"stringValue": "documents"}] + } + ] + } + } + }, + "stages/sample/documents-option": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sample", + "args": [{"integerValue": "10"}, {"stringValue": "documents"}] + } + ] + } + } + }, + "stages/sample/percentage": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sample", + "args": [{"doubleValue": 0.25}, {"stringValue": "percent"}] + } + ] + } + } + }, + "stages/sample/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sample", + "args": [{"integerValue": "10"}, {"stringValue": "documents"}], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "stages/search/add-fields": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "search", + "options": { + "add_fields": { + "mapValue": { + "fields": { + "searchScore": {"functionValue": {"name": "score"}} + } + } + }, + "query": { + "functionValue": { + "name": "document_matches", + "args": [{"stringValue": "breakfast"}] + } + } + } + } + ] + } + } + }, + "stages/search/limit-offset": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "search", + "options": { + "limit": {"integerValue": "10"}, + "offset": {"integerValue": "5"}, + "query": { + "functionValue": { + "name": "document_matches", + "args": [{"stringValue": "breakfast"}] + } + } + } + } + ] + } + } + }, + "stages/search/query-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "search", + "options": { + "query": { + "functionValue": { + "name": "document_matches", + "args": [{"stringValue": "breakfast"}] + } + } + } + } + ] + } + } + }, + "stages/search/query-string": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "search", + "options": { + "query": { + "functionValue": { + "name": "document_matches", + "args": [{"stringValue": "breakfast"}] + } + } + } + } + ] + } + } + }, + "stages/search/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "search", + "options": { + "extra": { + "mapValue": {"fields": {"flag": {"booleanValue": true}}} + }, + "limit": {"integerValue": "20"}, + "query": { + "functionValue": { + "name": "document_matches", + "args": [{"stringValue": "breakfast"}] + } + } + } + } + ] + } + } + }, + "stages/search/retrieval-depth-language": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "search", + "options": { + "language_code": {"stringValue": "en"}, + "query": { + "functionValue": { + "name": "document_matches", + "args": [{"stringValue": "breakfast"}] + } + }, + "retrieval_depth": {"integerValue": "100"} + } + } + ] + } + } + }, + "stages/search/sort-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "search", + "options": { + "query": { + "functionValue": { + "name": "document_matches", + "args": [{"stringValue": "breakfast"}] + } + }, + "sort": { + "arrayValue": { + "values": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"functionValue": {"name": "score"}} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "title"} + } + } + } + ] + } + } + } + } + ] + } + } + }, + "stages/search/sort-single": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "search", + "options": { + "query": { + "functionValue": { + "name": "document_matches", + "args": [{"stringValue": "breakfast"}] + } + }, + "sort": { + "arrayValue": { + "values": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"functionValue": {"name": "score"}} + } + } + } + ] + } + } + } + } + ] + } + } + }, + "stages/select/aliased": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "name": {"fieldReferenceValue": "title"}, + "doubled": { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "2"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "stages/select/field-names": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "author": {"fieldReferenceValue": "author"}, + "title": {"fieldReferenceValue": "title"} + } + } + } + ] + } + ] + } + } + }, + "stages/select/fields": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "author": {"fieldReferenceValue": "author"}, + "title": {"fieldReferenceValue": "title"} + } + } + } + ] + } + ] + } + } + }, + "stages/select/mixed": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "author": {"fieldReferenceValue": "author"}, + "score": {"fieldReferenceValue": "rating"}, + "title": {"fieldReferenceValue": "title"} + } + } + } + ] + } + ] + } + } + }, + "stages/select/nested-field-path": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "awards.hugo": {"fieldReferenceValue": "awards.hugo"}, + "metadata.lang": {"fieldReferenceValue": "metadata.lang"} + } + } + } + ] + } + ] + } + } + }, + "stages/select/options-object": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "score": {"fieldReferenceValue": "rating"}, + "title": {"fieldReferenceValue": "title"} + } + } + } + ] + } + ] + } + } + }, + "stages/select/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"title": {"fieldReferenceValue": "title"}} + } + } + ], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "stages/select/special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "`last name`": {"fieldReferenceValue": "`last name`"}, + "first-name": {"fieldReferenceValue": "`first-name`"} + } + } + } + ] + } + ] + } + } + }, + "stages/sort/ascending-method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + } + ] + } + ] + } + } + }, + "stages/sort/descending-method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + } + ] + } + ] + } + } + }, + "stages/sort/expression-method": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": { + "functionValue": { + "name": "char_length", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "stages/sort/multiple": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "title"} + } + } + } + ] + } + ] + } + } + }, + "stages/sort/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + } + ], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "stages/sort/special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "`first-name`"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "`last name`"} + } + } + } + ] + } + ] + } + } + }, + "stages/sort/top-level-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": { + "functionValue": { + "name": "char_length", + "args": [{"fieldReferenceValue": "title"}] + } + } + } + } + } + ] + } + ] + } + } + }, + "stages/sort/top-level-field-name": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "ascending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + }, + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "title"} + } + } + } + ] + } + ] + } + } + }, + "stages/to-scalar-expression/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "add_fields", + "args": [ + { + "mapValue": { + "fields": { + "averageRating": { + "functionValue": { + "name": "scalar", + "args": [ + { + "pipelineValue": { + "stages": [ + { + "name": "collection", + "args": [{"referenceValue": "/reviews"}] + }, + { + "name": "aggregate", + "args": [ + { + "mapValue": { + "fields": { + "avg": { + "functionValue": { + "name": "average", + "args": [ + { + "fieldReferenceValue": "rating" + } + ] + } + } + } + } + }, + {"mapValue": {}} + ] + } + ] + } + } + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "stages/union/complex": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"title": {"fieldReferenceValue": "title"}} + } + } + ] + }, + { + "name": "union", + "args": [ + { + "pipelineValue": { + "stages": [ + { + "name": "collection_group", + "args": [ + {"referenceValue": ""}, + {"stringValue": "reviews"} + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "3"} + ] + } + } + ] + }, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "title": {"fieldReferenceValue": "title"} + } + } + } + ] + } + ] + } + } + ] + } + ] + } + } + }, + "stages/union/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "union", + "args": [ + { + "pipelineValue": { + "stages": [ + { + "name": "collection", + "args": [{"referenceValue": "/magazines"}] + } + ] + } + } + ] + } + ] + } + } + }, + "stages/union/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "union", + "args": [ + { + "pipelineValue": { + "stages": [ + { + "name": "collection", + "args": [{"referenceValue": "/magazines"}] + } + ] + } + } + ], + "options": {"foo": {"stringValue": "bar"}} + } + ] + } + } + }, + "stages/unnest/aliased-field": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "unnest", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "tag"} + ] + } + ] + } + } + }, + "stages/unnest/expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "unnest", + "args": [ + { + "functionValue": { + "name": "array", + "args": [ + {"integerValue": "1"}, + {"integerValue": "2"}, + {"fieldReferenceValue": "rating"} + ] + } + }, + {"fieldReferenceValue": "item"} + ] + } + ] + } + } + }, + "stages/unnest/field": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "unnest", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "tags"} + ] + } + ] + } + } + }, + "stages/unnest/field-special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "unnest", + "args": [ + {"fieldReferenceValue": "`my tags`"}, + {"fieldReferenceValue": "`\\`my tags\\``"} + ] + } + ] + } + } + }, + "stages/unnest/index-field": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "unnest", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "tag"} + ], + "options": {"index_field": {"fieldReferenceValue": "index"}} + } + ] + } + } + }, + "stages/unnest/options-object": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "unnest", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "tag"} + ], + "options": {"index_field": {"fieldReferenceValue": "idx"}} + } + ] + } + } + }, + "stages/unnest/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "unnest", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "tag"} + ], + "options": { + "foo": {"stringValue": "bar"}, + "index_field": {"fieldReferenceValue": "position"} + } + } + ] + } + } + }, + "stages/unnest/special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "unnest", + "args": [ + {"fieldReferenceValue": "tags"}, + {"fieldReferenceValue": "`my tag`"} + ], + "options": {"index_field": {"fieldReferenceValue": "`tag index`"}} + } + ] + } + } + }, + "stages/update/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "update", + "args": [ + { + "mapValue": { + "fields": { + "rating": { + "functionValue": { + "name": "add", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "1"} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "stages/where/chained": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "genre"}, + {"stringValue": "Fantasy"} + ] + } + } + ] + } + ] + } + } + }, + "stages/where/condition": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ] + } + ] + } + } + }, + "stages/where/raw-options": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "greater_than", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "4"} + ] + } + } + ], + "options": { + "foo": {"stringValue": "bar"}, + "outer": { + "mapValue": {"fields": {"inner": {"booleanValue": true}}} + } + } + } + ] + } + } + } + } +} diff --git a/packages/google_cloud_firestore/test/fixtures/pipeline_golden/values.json b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/values.json new file mode 100644 index 00000000..402af131 --- /dev/null +++ b/packages/google_cloud_firestore/test/fixtures/pipeline_golden/values.json @@ -0,0 +1,1680 @@ +{ + "generator": "tool/pipeline_golden (npm run generate)", + "sdk": "@google-cloud/firestore@9.3.1", + "cases": { + "values/constant/array": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "arrayValue": { + "values": [ + {"integerValue": "1"}, + {"stringValue": "a"}, + {"booleanValue": true}, + {"nullValue": null} + ] + } + } + } + } + } + ] + } + ] + } + } + }, + "values/constant/bytes": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + {"mapValue": {"fields": {"result": {"bytesValue": "AQID/w=="}}}} + ] + } + ] + } + } + }, + "values/constant/date": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": {"timestampValue": "2024-01-02T03:04:05.678Z"} + } + } + } + ] + } + ] + } + } + }, + "values/constant/document-reference": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "referenceValue": "projects/test-project/databases/(default)/documents/books/book1" + } + } + } + } + ] + } + ] + } + } + }, + "values/constant/double": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + {"mapValue": {"fields": {"result": {"doubleValue": 3.14}}}} + ] + } + ] + } + } + }, + "values/constant/empty-string": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + {"mapValue": {"fields": {"result": {"stringValue": ""}}}} + ] + } + ] + } + } + }, + "values/constant/false": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + {"mapValue": {"fields": {"result": {"booleanValue": false}}}} + ] + } + ] + } + } + }, + "values/constant/geo-point": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "geoPointValue": { + "latitude": 37.7749, + "longitude": -122.4194 + } + } + } + } + } + ] + } + ] + } + } + }, + "values/constant/infinity": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"doubleValue": "Infinity"}} + } + } + ] + } + ] + } + } + }, + "values/constant/integer": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + {"mapValue": {"fields": {"result": {"integerValue": "42"}}}} + ] + } + ] + } + } + }, + "values/constant/map": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "mapValue": { + "fields": { + "a": {"integerValue": "1"}, + "b": {"stringValue": "x"}, + "nested": { + "mapValue": { + "fields": { + "c": { + "arrayValue": { + "values": [ + {"integerValue": "1"}, + {"integerValue": "2"} + ] + } + } + } + } + } + } + } + } + } + } + } + ] + } + ] + } + } + }, + "values/constant/nan": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + {"mapValue": {"fields": {"result": {"doubleValue": "NaN"}}}} + ] + } + ] + } + } + }, + "values/constant/negative-infinity": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"doubleValue": "-Infinity"}} + } + } + ] + } + ] + } + } + }, + "values/constant/negative-integer": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + {"mapValue": {"fields": {"result": {"integerValue": "-7"}}}} + ] + } + ] + } + } + }, + "values/constant/null": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + {"mapValue": {"fields": {"result": {"nullValue": null}}}} + ] + } + ] + } + } + }, + "values/constant/string": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + {"mapValue": {"fields": {"result": {"stringValue": "Fantasy"}}}} + ] + } + ] + } + } + }, + "values/constant/timestamp": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "timestampValue": "2023-11-14T22:13:20.123456789Z" + } + } + } + } + ] + } + ] + } + } + }, + "values/constant/timestamp-millis": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": {"timestampValue": "2023-11-14T22:13:20.123Z"} + } + } + } + ] + } + ] + } + } + }, + "values/constant/true": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + {"mapValue": {"fields": {"result": {"booleanValue": true}}}} + ] + } + ] + } + } + }, + "values/constant/vector": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + } + } + } + ] + } + ] + } + } + }, + "values/constant/zero": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + {"mapValue": {"fields": {"result": {"integerValue": "0"}}}} + ] + } + ] + } + } + }, + "values/data-types/all-constants": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + {"name": "limit", "args": [{"integerValue": "1"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "array": { + "functionValue": { + "name": "array", + "args": [ + {"integerValue": "1"}, + {"stringValue": "a string"}, + {"booleanValue": true}, + {"nullValue": null}, + { + "geoPointValue": { + "latitude": 37.7749, + "longitude": -122.4194 + } + }, + {"timestampValue": "2023-11-14T22:13:20.123456789Z"}, + {"timestampValue": "2024-01-02T03:04:05.678Z"}, + {"bytesValue": "AQID/w=="}, + { + "referenceValue": "projects/test-project/databases/(default)/documents/books/book1" + }, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "number"}, + {"integerValue": "2"}, + {"stringValue": "string"}, + {"stringValue": "b string"} + ] + } + } + ] + } + }, + "boolean": {"booleanValue": true}, + "bytes": {"bytesValue": "AQID/w=="}, + "date": {"timestampValue": "2024-01-02T03:04:05.678Z"}, + "documentReference": { + "referenceValue": "projects/test-project/databases/(default)/documents/books/book1" + }, + "geoPoint": { + "geoPointValue": { + "latitude": 37.7749, + "longitude": -122.4194 + } + }, + "map": { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "number"}, + {"integerValue": "1"}, + {"stringValue": "string"}, + {"stringValue": "a string"}, + {"stringValue": "boolean"}, + {"booleanValue": true}, + {"stringValue": "null"}, + {"nullValue": null}, + {"stringValue": "geoPoint"}, + { + "geoPointValue": { + "latitude": 37.7749, + "longitude": -122.4194 + } + }, + {"stringValue": "timestamp"}, + {"timestampValue": "2023-11-14T22:13:20.123456789Z"}, + {"stringValue": "date"}, + {"timestampValue": "2024-01-02T03:04:05.678Z"}, + {"stringValue": "uint8Array"}, + {"bytesValue": "AQID/w=="}, + {"stringValue": "documentReference"}, + { + "referenceValue": "projects/test-project/databases/(default)/documents/books/book1" + }, + {"stringValue": "vectorValue"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + }, + {"stringValue": "map"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "number"}, + {"integerValue": "2"}, + {"stringValue": "string"}, + {"stringValue": "b string"} + ] + } + }, + {"stringValue": "array"}, + { + "functionValue": { + "name": "array", + "args": [ + {"integerValue": "1"}, + {"stringValue": "c string"} + ] + } + } + ] + } + }, + "null": {"nullValue": null}, + "number": {"integerValue": "1"}, + "string": {"stringValue": "a string"}, + "timestamp": { + "timestampValue": "2023-11-14T22:13:20.123456789Z" + }, + "vectorValue": { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + } + } + } + ] + } + ] + } + } + }, + "values/data-types/array-and-map-functions": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "sort", + "args": [ + { + "mapValue": { + "fields": { + "direction": {"stringValue": "descending"}, + "expression": {"fieldReferenceValue": "rating"} + } + } + } + ] + }, + {"name": "limit", "args": [{"integerValue": "1"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "author": {"fieldReferenceValue": "author"}, + "genre": {"fieldReferenceValue": "genre"}, + "published": {"fieldReferenceValue": "published"}, + "rating": {"fieldReferenceValue": "rating"}, + "tags": {"fieldReferenceValue": "tags"}, + "title": {"fieldReferenceValue": "title"} + } + } + } + ] + }, + { + "name": "add_fields", + "args": [ + { + "mapValue": { + "fields": { + "metadata": { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "genre"}, + {"fieldReferenceValue": "genre"}, + {"stringValue": "rating"}, + { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "10"} + ] + } + }, + {"stringValue": "nestedArray"}, + { + "functionValue": { + "name": "array", + "args": [{"fieldReferenceValue": "title"}] + } + }, + {"stringValue": "nestedMap"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "published"}, + {"fieldReferenceValue": "published"} + ] + } + } + ] + } + }, + "metadataArray": { + "functionValue": { + "name": "array", + "args": [ + {"integerValue": "1"}, + {"integerValue": "2"}, + {"fieldReferenceValue": "genre"}, + { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "10"} + ] + } + }, + { + "functionValue": { + "name": "array", + "args": [{"fieldReferenceValue": "title"}] + } + }, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "published"}, + {"fieldReferenceValue": "published"} + ] + } + } + ] + } + } + } + } + } + ] + }, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "and", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "metadataArray"}, + { + "functionValue": { + "name": "array", + "args": [ + {"integerValue": "1"}, + {"integerValue": "2"}, + {"fieldReferenceValue": "genre"}, + { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "10"} + ] + } + }, + { + "functionValue": { + "name": "array", + "args": [{"fieldReferenceValue": "title"}] + } + }, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "published"}, + {"fieldReferenceValue": "published"} + ] + } + } + ] + } + } + ] + } + }, + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "metadata"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "genre"}, + {"fieldReferenceValue": "genre"}, + {"stringValue": "rating"}, + { + "functionValue": { + "name": "multiply", + "args": [ + {"fieldReferenceValue": "rating"}, + {"integerValue": "10"} + ] + } + }, + {"stringValue": "nestedArray"}, + { + "functionValue": { + "name": "array", + "args": [{"fieldReferenceValue": "title"}] + } + }, + {"stringValue": "nestedMap"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "published"}, + {"fieldReferenceValue": "published"} + ] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "values/field/backslash": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"fieldReferenceValue": "`a\\\\b`"}} + } + } + ] + } + ] + } + } + }, + "values/field/backtick": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"fieldReferenceValue": "`a\\`b`"}} + } + } + ] + } + ] + } + } + }, + "values/field/document-id": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"fieldReferenceValue": "__name__"}} + } + } + ] + } + ] + } + } + }, + "values/field/field-name-argument": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "`first-name`"}, + {"stringValue": "Ada"} + ] + } + } + ] + } + ] + } + } + }, + "values/field/field-path-object": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"fieldReferenceValue": "`a.b`.c"}} + } + } + ] + } + ] + } + } + }, + "values/field/nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": {"fieldReferenceValue": "metadata.lang"} + } + } + } + ] + } + ] + } + } + }, + "values/field/non-ascii": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"fieldReferenceValue": "`naïve`"}} + } + } + ] + } + ] + } + } + }, + "values/field/reserved-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"fieldReferenceValue": "`a/b`"}} + } + } + ] + } + ] + } + } + }, + "values/field/simple": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"fieldReferenceValue": "title"}} + } + } + ] + } + ] + } + } + }, + "values/field/space": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"fieldReferenceValue": "`last name`"}} + } + } + ] + } + ] + } + } + }, + "values/field/special-characters": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": { + "result": {"fieldReferenceValue": "`first-name`"} + } + } + } + ] + } + ] + } + } + }, + "values/value-position/boolean": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + {"booleanValue": true} + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/bytes": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + {"bytesValue": "AQID/w=="} + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/constant-list": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + { + "arrayValue": { + "values": [{"stringValue": "a"}, {"stringValue": "b"}] + } + } + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/date": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + {"timestampValue": "2024-01-02T03:04:05.678Z"} + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/document-reference": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + { + "referenceValue": "projects/test-project/databases/(default)/documents/books/book1" + } + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/double": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + {"doubleValue": 1.5} + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/geo-point": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + { + "geoPointValue": { + "latitude": 37.7749, + "longitude": -122.4194 + } + } + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/integer": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + {"integerValue": "1"} + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/list-empty": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + {"functionValue": {"name": "array"}} + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/list-nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + { + "functionValue": { + "name": "array", + "args": [ + { + "functionValue": { + "name": "array", + "args": [{"stringValue": "a"}] + } + }, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "k"}, + {"stringValue": "v"} + ] + } + } + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/list-plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + { + "functionValue": { + "name": "array", + "args": [{"stringValue": "a"}, {"stringValue": "b"}] + } + } + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/list-with-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + { + "functionValue": { + "name": "array", + "args": [ + {"stringValue": "a"}, + {"fieldReferenceValue": "b"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/map-empty": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + {"functionValue": {"name": "map"}} + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/map-nested": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "inner"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "k"}, + {"fieldReferenceValue": "y"} + ] + } + }, + {"stringValue": "list"}, + { + "functionValue": { + "name": "array", + "args": [{"fieldReferenceValue": "z"}] + } + } + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/map-plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "a"}, + {"integerValue": "1"}, + {"stringValue": "b"}, + {"stringValue": "x"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/map-with-expression": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + { + "functionValue": { + "name": "map", + "args": [ + {"stringValue": "a"}, + {"fieldReferenceValue": "x"}, + {"stringValue": "b"}, + {"integerValue": "1"} + ] + } + } + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/null": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + {"nullValue": null} + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/string": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + {"stringValue": "a"} + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/timestamp": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + {"timestampValue": "2023-11-14T22:13:20.123456789Z"} + ] + } + } + ] + } + ] + } + } + }, + "values/value-position/vector": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "where", + "args": [ + { + "functionValue": { + "name": "equal", + "args": [ + {"fieldReferenceValue": "value"}, + { + "mapValue": { + "fields": { + "__type__": {"stringValue": "__vector__"}, + "value": { + "arrayValue": { + "values": [ + {"doubleValue": 0.1}, + {"doubleValue": 0.2}, + {"doubleValue": 0.3} + ] + } + } + } + } + } + ] + } + } + ] + } + ] + } + } + }, + "values/variable/plain": { + "database": "projects/test-project/databases/(default)", + "structuredPipeline": { + "pipeline": { + "stages": [ + {"name": "collection", "args": [{"referenceValue": "/books"}]}, + { + "name": "select", + "args": [ + { + "mapValue": { + "fields": {"result": {"variableReferenceValue": "x"}} + } + } + ] + } + ] + } + } + } + } +} diff --git a/packages/google_cloud_firestore/test/pipeline_api_parity_test.dart b/packages/google_cloud_firestore/test/pipeline_api_parity_test.dart new file mode 100644 index 00000000..47cd9de6 --- /dev/null +++ b/packages/google_cloud_firestore/test/pipeline_api_parity_test.dart @@ -0,0 +1,1656 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Compares the signatures of the Dart Pipelines API with the Node.js SDK's. +/// +/// The Node API is read from `test/fixtures/node_pipeline_api.json`, which +/// `tool/pipeline_api_parity` extracts from the pinned +/// `@google-cloud/firestore` typings. The Dart API is read with +/// `package:analyzer` from what `lib/google_cloud_firestore.dart` exports. +/// +/// How Node maps onto Dart: +/// +/// * A Node top-level function `x` is `PipelineFunctions.x`, a top-level `x` +/// from `lib/src/pipeline*.dart`, or `Expression.x`. Every one of those that +/// exists is compared. +/// * A Node class maps onto the Dart type in [_classes]; its methods and +/// properties are looked up on that type, inherited members included. +/// * A Node options object (`CollectionStageOptions`, ...) maps onto Dart +/// named parameters; a stage's required keys may instead be positional. +/// * A Node string union maps onto the `value`s of a Dart enum ([_enums]). +/// +/// Signatures are compared on shape, not on exact types: argument counts, +/// which positions are optional, whether a parameter takes a single value or +/// a list, and whether Dart's type accepts each kind of value Node does (an +/// expression, a string, a number, ...). A Node rest parameter matches a +/// trailing Dart `Iterable`, either in its own position or folding the +/// arguments before it, as in `and(Iterable)` for +/// Node's `and(first, second, ...more)`. +/// +/// Every difference is reported as `id: message`, one per line. An `id` is +/// the Dart member (`PipelineFunctions.join`, `Pipeline.where`, `field` for a +/// top-level function) optionally followed by `#` and what differs: `#arity`, +/// a Dart parameter name, a Node option key, or `#returns`. Entries in +/// [_knownDifferences] and [_pendingFixes] are keyed by a full `id`, or by a +/// bare member to cover all of its differences. Stale entries fail the test. +@TestOn('vm') +@Timeout(Duration(minutes: 5)) +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/dart/element/type_provider.dart'; +import 'package:analyzer/dart/element/type_system.dart'; +import 'package:test/test.dart'; + +/// The Node SDK version the snapshot must come from. +const _nodeVersion = '9.3.1'; + +/// Node member names that Dart spells differently, keyed by +/// `NodeOwner.member` (a class, `Transaction`, or an options type). +const _renames = { + // Named after Dart's String.toLowerCase()/toUpperCase(). + 'Expression.toLower': 'toLowerCase', + 'Expression.toUpper': 'toUpperCase', + 'Field.fieldName': 'path', + 'PipelineResult.isEqual': '==', + // Dart decodes the protobuf Any into a map; Node returns it as is. + 'ExplainStats.rawData': 'raw', + 'Transaction.execute': 'executePipeline', + 'PipelineExecuteOptions.explainOptions': 'explain', + // Named like the VectorQueryOptions they are built from. + 'FindNearestStageOptions.field': 'vectorField', + 'FindNearestStageOptions.vectorValue': 'queryVector', + 'FindNearestStageOptions.distanceField': 'distanceResultField', +}; + +/// The Dart type standing for each Node class or interface. +const _classes = { + 'Expression': 'PipelineExpression', + // Node's implementation classes; Dart keeps them private and builds them + // with constant() and pipelineFunction(). Their constructors are not + // compared. + 'Constant': 'PipelineExpression', + 'FunctionExpression': 'PipelineExpression', + 'AggregateFunction': 'PipelineAggregateFunction', + 'BooleanExpression': 'PipelineBooleanExpression', + 'Field': 'PipelineField', + 'AliasedExpression': 'PipelineAliasedExpression', + // Dart aliases aggregates like any other expression. + 'AliasedAggregate': 'PipelineAliasedExpression', + 'Selectable': 'Selectable', + 'Ordering': 'PipelineOrdering', + 'Pipeline': 'Pipeline', + 'PipelineSource': 'PipelineSource', + 'PipelineResult': 'PipelineResult', + 'PipelineSnapshot': 'PipelineSnapshot', + 'ExplainStats': 'ExplainStats', +}; + +/// The Dart enum standing for each Node string union, compared by `value`. +const _enums = {'Type': 'PipelineValueType'}; + +/// Dart types that Node models as option objects or inline string unions, +/// and where. They are compared through the named parameters that use them. +const _optionTypes = { + 'PipelineExplainOptions': 'PipelineExecuteOptions.explainOptions', + 'PipelineExplainMode': 'PipelineExecuteOptions.explainOptions.mode', + 'PipelineExplainOutputFormat': + 'PipelineExecuteOptions.explainOptions.outputFormat', + 'PipelineIndexMode': 'PipelineExecuteOptions.indexMode', +}; + +/// Dart classes whose static methods stand for Node's top-level functions. +const _functionNamespaces = {'PipelineFunctions', 'Expression'}; + +/// Node properties that only exist for TypeScript runtime type tests, such as +/// `expr.expressionType === 'Field'` or `'selectable' in value`. Dart uses its +/// type system (sealed classes and `is`) instead. +const _nodeTypeTags = {'expressionType', 'selectable'}; + +/// Public Dart members with no Node counterpart, and why. +/// +/// `xLiteral` variants of an existing `x` are allowed without an entry; their +/// arity is still compared with Node's `x`. +const _dartOnly = { + 'Expression.raw': + 'Escape hatch for backend functions this SDK does not wrap.', + 'Expression.vector': 'Shorthand for constant(FieldValue.vector(...)).', + 'PipelineFunctions.raw': 'Escape hatch, same as pipelineFunction().', + 'PipelineFunctions.maximumN': + 'Named after the backend maximum_n function; same as arrayMaximumN.', + 'PipelineFunctions.minimumN': + 'Named after the backend minimum_n function; same as arrayMinimumN.', + 'pipelineFunction': + 'Escape hatch for backend functions this SDK does not wrap; Node only ' + 'offers `new FunctionExpression(name, params)`.', + 'PipelineExpression.arrayConcatMultiple': + "Dart's arrayConcat takes one array; this is Node's variadic form.", + 'PipelineExpression.arrayContainsAllFrom': + 'Node overloads arrayContainsAll(values | arrayExpression); Dart splits ' + 'off the expression form.', + 'PipelineExpression.geoDistance': + 'Node only offers geoDistance on Field; Dart allows any geo point ' + 'expression.', + 'PipelineAliasedExpression.expression': + 'Node exposes it as the underscored _expr.', + 'PipelineAliasedExpression.name': + 'Node exposes it as the underscored _alias.', + 'Pipeline.firestore': 'Node keeps the Firestore instance private.', + 'PipelineResult.name': + 'The document name as returned by the backend; Node only exposes ref.', + 'PipelineSnapshot.empty': 'Mirrors QuerySnapshot.empty.', + 'PipelineSnapshot.size': 'Mirrors QuerySnapshot.size.', + 'ExplainStats.typeName': 'Node exposes it as rawData.type_url.', +}; + +/// Intentional differences, keyed by mismatch id or bare member. +const _knownDifferences = { + 'PipelineFunctions.count#arity': + 'count() with no argument counts every input, like countAll(); Node ' + 'requires an argument.', + 'PipelineExpression.arrayConcat#arity': + 'Takes a single array; the variadic form is arrayConcatMultiple.', + 'PipelineSource.collection#collectionPath': + 'Takes a path; a CollectionReference goes to collectionReference(), ' + 'since Dart has no overloads.', + 'PipelineSource.collectionReference': + 'The CollectionReference form of Node collection().', + 'Pipeline.addFields#fields': + 'Only aliased expressions: adding a bare field to itself is a no-op.', + 'Pipeline.search': + 'The search stage is still evolving; Dart passes a raw option map ' + 'through instead of typing SearchStageOptions.', + 'Pipeline.rawStage#options': + "Node's rawStage(name, params, options) takes options at runtime, but " + 'its typings omit them.', + 'Pipeline.execute#readTime': + 'Reads at a past time (ExecutePipelineRequest.read_time); Node only ' + 'passes a read time internally.', + 'Transaction.executePipeline': + 'Takes the same execute options as Pipeline.execute; Node takes none.', + 'Pipeline.findNearest#distanceThreshold': + 'Dart-only convenience, mirroring VectorQuery: applied as a where stage ' + 'filtering the distance after find_nearest, as createFrom(vectorQuery) ' + 'does, and never sent as an option. The backend rejects one: "Stage ' + "'find_nearest(field: FieldReference, vector: Vector, distance_measure: " + "DistanceMeasure, limit: Int64?, distance_field: FieldPath?)' does not " + 'support option(s) [distance_threshold]."', + 'PipelineValueType#request_timestamp': + "The backend's is_type rejects it: its list of accepted types does not " + "include 'request_timestamp'.", + 'type:ExpressionType': 'The values of the expressionType type tag.', + 'type:TimeGranularity': 'Dart takes these as plain strings (Object?).', + 'type:TimePart': 'Dart takes these as plain strings (Object?).', + 'type:TimeUnit': 'Dart takes these as plain strings (Object?).', +}; + +/// Differences that look like Dart bugs or gaps, to be fixed in `lib/`. +/// +/// Remove an entry once its fix lands; the test fails while it is stale. +const _pendingFixes = { + // Options. + 'PipelineSource.collection#forceIndex': + 'Node collection({collection, forceIndex}) can force an index; Dart ' + 'cannot.', + 'PipelineSource.collectionGroup#forceIndex': + 'Node collectionGroup({collectionId, forceIndex}) can force an index; ' + 'Dart cannot.', + // Dart-only functions with no Node counterpart. + 'PipelineFunctions.log': + 'Node has no log function; check the backend supports `log`, or drop.', + 'PipelineExpression.log': + 'Node has no log method; check the backend supports `log`, or drop.', + 'PipelineFunctions.cmp': + 'Node has no cmp function; check the backend supports `cmp`, or drop.', + 'PipelineFunctions.referenceSlice': + 'Node has no referenceSlice; check the backend supports ' + '`reference_slice`, or drop.', + 'PipelineExpression.referenceSlice': + 'Node has no referenceSlice; check the backend supports ' + '`reference_slice`, or drop.', + // Node APIs Dart does not implement yet. + 'PipelineFunctions.subcollection': + 'Missing: Node subcollection(path | options) starts a subcollection ' + 'Pipeline for use inside another stage.', + 'Pipeline.define': 'Missing stage: Node define(...aliasedExpressions).', + 'Pipeline.delete': 'Missing stage: Node delete().', + 'Pipeline.update': 'Missing stage: Node update(transformedFields?).', + 'Pipeline.toArrayExpression': + 'Missing: Node turns a Pipeline into an array subquery expression.', + 'Pipeline.toScalarExpression': + 'Missing: Node turns a Pipeline into a scalar subquery expression.', + 'Pipeline.stream': 'Missing: Node streams results; Dart only has execute().', +}; + +void main() { + late _Report report; + + setUpAll(() async { + final node = _NodeApi.load( + File('test/fixtures/node_pipeline_api.json').readAsStringSync(), + ); + final dart = await _DartApi.load(Directory.current.path); + report = _Checker(node, dart).run(); + }); + + test('the snapshot comes from the pinned Node SDK', () { + expect(report.node.formatVersion, 1); + expect(report.node.version, _nodeVersion); + }); + + test('the Dart Pipeline API matches the Node SDK signatures', () { + final unexplained = [ + for (final mismatch in report.mismatches) + if (report.coveringKey(mismatch) == null) mismatch, + ]; + if (unexplained.isNotEmpty) { + fail( + '${unexplained.length} difference(s) from the Node SDK ' + '$_nodeVersion. Fix the Dart API, or record the difference in ' + '_knownDifferences (intentional) or _pendingFixes (to fix):\n' + '${unexplained.join('\n')}', + ); + } + }); + + test('the parity tables have no stale entries', () { + final problems = [ + ...report.problems, + for (final key in _knownDifferences.keys) + if (_pendingFixes.containsKey(key)) + '$key: in both _knownDifferences and _pendingFixes', + for (final MapEntry(key: name, value: entries) in { + '_knownDifferences': _knownDifferences, + '_pendingFixes': _pendingFixes, + }.entries) + for (final key in entries.keys) + if (!report.mismatches.any((m) => m.matches(key))) + '$key: stale $name entry, the difference is gone', + for (final key in _dartOnly.keys) + if (!report.dartOnlyUsed.contains(key)) + '$key: stale _dartOnly entry, Node has it or Dart no longer does', + for (final key in _renames.keys) + if (!report.renamesUsed.contains(key)) + '$key: stale _renames entry, Node or Dart no longer has it', + ]; + if (problems.isNotEmpty) fail(problems.join('\n')); + }); +} + +// --- Node API ---------------------------------------------------------------- + +final class _NodeType { + _NodeType.fromJson(Map json) + : kinds = _strings(json['kinds']), + literals = _strings(json['literals']), + aliases = _strings(json['aliases']), + options = _strings(json['options']), + elements = json['elements'] == null + ? null + : _NodeType.fromJson(json['elements']! as Map), + properties = { + for (final MapEntry(:key, :value) + in ((json['properties'] as Map?) ?? {}).entries) + key: _NodeProperty.fromJson(value! as Map), + }; + + _NodeType.union(Iterable<_NodeType> types) + : kinds = {for (final type in types) ...type.kinds}, + literals = {for (final type in types) ...type.literals}, + aliases = {for (final type in types) ...type.aliases}, + options = {for (final type in types) ...type.options}, + elements = _unionOrNull([for (final type in types) ?type.elements]), + properties = const {}; + + static _NodeType? _unionOrNull(List<_NodeType> types) { + return types.isEmpty ? null : _NodeType.union(types); + } + + final Set kinds; + final Set literals; + final Set aliases; + final Set options; + final _NodeType? elements; + final Map properties; + + String get label { + final sorted = kinds.toList()..sort(); + return sorted.join('|'); + } +} + +final class _NodeProperty { + _NodeProperty.fromJson(Map json) + : optional = json['optional'] == true, + type = _NodeType.fromJson(json); + + final bool optional; + final _NodeType type; +} + +final class _NodeParam { + _NodeParam.fromJson(Map json) + : name = json['name']! as String, + optional = json['optional'] == true, + rest = json['rest'] == true, + // Runtime-only members have no types: anything goes. + type = json.containsKey('kinds') + ? _NodeType.fromJson(json) + : _NodeType.fromJson(const { + 'kinds': ['any'], + }); + + final String name; + final bool optional; + final bool rest; + final _NodeType type; + + bool get required => !optional && !rest; + + bool get isOptions => + type.kinds.length == 1 && type.kinds.single == 'options'; +} + +final class _NodeSignature { + _NodeSignature.fromJson(Map json) + : params = [ + for (final param in json['params']! as List) + _NodeParam.fromJson(param as Map), + ], + returns = json['returns'] == null + ? null + : _NodeType.fromJson(json['returns']! as Map); + + final List<_NodeParam> params; + final _NodeType? returns; +} + +final class _NodeMember { + _NodeMember({ + required this.owner, + required this.name, + required this.isMethod, + required this.overloads, + }) : runtimeOnly = false; + + /// A member only the JavaScript implementation has, without types. + _NodeMember.runtimeOnly({ + required this.owner, + required this.name, + required Map json, + }) : isMethod = json['kind'] != 'getter', + overloads = [ + if (json['kind'] != 'getter') _NodeSignature.fromJson(json), + ], + runtimeOnly = true; + + final String owner; + final String name; + final bool isMethod; + final List<_NodeSignature> overloads; + + /// Whether only the JavaScript implementation has this member: the typings + /// omit it, so it is not part of the contract and has no types. + final bool runtimeOnly; +} + +final class _NodeClass { + _NodeClass(this.name, Map json, Map? js) + : superclass = json['extends'] as String?, + interfaces = _strings(json['implements']), + members = { + for (final MapEntry(:key, :value) + in (json['methods']! as Map).entries) + key: _NodeMember( + owner: name, + name: key, + isMethod: true, + overloads: _signatures(value), + ), + for (final key in (json['properties']! as Map).keys) + key: _NodeMember( + owner: name, + name: key, + isMethod: false, + overloads: const [], + ), + for (final MapEntry(:key, :value) in (js ?? {}).entries) + key: _NodeMember.runtimeOnly( + owner: name, + name: key, + json: value! as Map, + ), + }; + + final String name; + final String? superclass; + final Set interfaces; + final Map members; +} + +final class _NodeApi { + _NodeApi._(this._json); + + factory _NodeApi.load(String source) { + return _NodeApi._(jsonDecode(source) as Map); + } + + final Map _json; + + int get formatVersion => _json['formatVersion']! as int; + + String get version => (_json['source']! as Map)['version']! as String; + + late final Map functions = { + for (final MapEntry(:key, :value) + in (_json['functions']! as Map).entries) + key: _NodeMember( + owner: '', + name: key, + isMethod: true, + overloads: _signatures(value), + ), + for (final MapEntry(:key, :value) + in (_json['runtimeOnlyFunctions']! as Map).entries) + key: _NodeMember.runtimeOnly( + owner: '', + name: key, + json: value! as Map, + ), + }; + + late final Map classes = () { + final js = _json['runtimeOnlyMembers']! as Map; + return { + for (final section in ['classes', 'interfaces']) + for (final MapEntry(:key, :value) + in (_json[section]! as Map).entries) + key: _NodeClass( + key, + value! as Map, + js[key] as Map?, + ), + }; + }(); + + late final Map> types = { + for (final MapEntry(:key, :value) + in (_json['types']! as Map).entries) + key: value! as Map, + }; + + /// The pipeline entry points declared outside the namespace, by owner. + late final Map> entryPoints = () { + final result = >{}; + for (final MapEntry(:key, :value) + in (_json['entryPoints']! as Map).entries) { + final [owner, name] = key.split('.'); + (result[owner] ??= {})[name] = _NodeMember( + owner: owner, + name: name, + isMethod: true, + overloads: _signatures(value), + ); + } + return result; + }(); + + /// A member of [className] or of the classes it extends or implements. + _NodeMember? lookUp(String className, String member) { + final cls = classes[className]; + if (cls == null) return null; + return cls.members[member] ?? + [ + ?cls.superclass, + ...cls.interfaces, + ].map((name) => lookUp(name, member)).nonNulls.firstOrNull; + } + + /// The string values of a literal [type], resolving named unions. + Set literalValues(_NodeType type) { + return { + ...type.literals, + for (final alias in type.aliases) ..._strings(types[alias]?['values']), + }; + } + + /// The properties of the options object [name], without its base types. + Map optionProperties(String name) { + return _NodeType.fromJson({ + 'kinds': const ['options'], + 'properties': types[name]!['properties'], + }).properties; + } + + Set optionBases(String name) => _strings(types[name]!['extends']); +} + +Set _strings(Object? json) => {...?(json as List?)?.cast()}; + +List<_NodeSignature> _signatures(Object? json) { + return [ + for (final overload in (json! as Map)['overloads']! as List) + _NodeSignature.fromJson(overload as Map), + ]; +} + +// --- Dart API ---------------------------------------------------------------- + +final class _DartApi { + _DartApi._(this.barrel, this.typedData, this.core); + + static Future<_DartApi> load(String packageRoot) async { + final collection = AnalysisContextCollection(includedPaths: [packageRoot]); + final session = collection.contextFor(packageRoot).currentSession; + Future library(String uri) async { + final result = await session.getLibraryByUri(uri); + if (result is! LibraryElementResult) { + throw StateError('Could not resolve $uri: $result'); + } + return result.element; + } + + return _DartApi._( + await library( + 'package:google_cloud_firestore/google_cloud_firestore.dart', + ), + await library('dart:typed_data'), + await library('dart:core'), + ); + } + + final LibraryElement barrel; + final LibraryElement typedData; + final LibraryElement core; + + TypeSystem get typeSystem => barrel.typeSystem; + + TypeProvider get typeProvider => barrel.typeProvider; + + late final Map exports = + barrel.exportNamespace.definedNames2; + + /// The class an exported name stands for, looking through typedefs. + InterfaceElement? interfaceNamed(String name) { + return switch (exports[name]) { + final InterfaceElement element => element, + final TypeAliasElement alias => switch (alias.aliasedType) { + final InterfaceType type => type.element, + _ => null, + }, + _ => null, + }; + } + + /// [element]'s type with its type parameters at their bounds. + InterfaceType instantiate(InterfaceElement element) { + return typeSystem.instantiateInterfaceToBounds( + element: element, + nullabilitySuffix: NullabilitySuffix.none, + ); + } + + InterfaceType exportedType(String name) { + final element = interfaceNamed(name); + if (element == null) throw StateError('$name is not exported.'); + return instantiate(element); + } + + InterfaceType libraryType(LibraryElement library, String name) { + return instantiate( + library.exportNamespace.definedNames2[name]! as InterfaceElement, + ); + } + + /// The library declaring the Pipeline API (`lib/src/firestore.dart`). + late final LibraryElement implementation = interfaceNamed( + 'PipelineFunctions', + )!.library; + + /// Whether [element] is declared in a `lib/src/pipeline*.dart` file. + bool isPipelineDeclaration(Element element) { + final path = element.firstFragment.libraryFragment?.source.fullName ?? ''; + return RegExp( + r'[/\\]lib[/\\]src[/\\]pipeline[^/\\]*\.dart$', + ).hasMatch(path) || + RegExp(r'[/\\]lib[/\\]src[/\\]pipeline[/\\]').hasMatch(path); + } + + /// Every public top-level declaration of the Pipeline API files. + late final List pipelineDeclarations = [ + for (final element in [ + ...implementation.classes, + ...implementation.enums, + ...implementation.mixins, + ...implementation.extensions, + ...implementation.extensionTypes, + ...implementation.typeAliases, + ...implementation.topLevelFunctions, + ...implementation.topLevelVariables, + ]) + if (element.isPublic && isPipelineDeclaration(element)) element, + ]; + + late final Map topLevelFunctions = { + for (final element + in pipelineDeclarations.whereType()) + element.name!: element, + }; + + /// Public instance members of [element], inherited ones included unless + /// [declaredOnly]. + Map members( + InterfaceElement element, { + bool declaredOnly = false, + }) { + final result = {}; + final chain = [ + element, + if (!declaredOnly) + for (final type in element.allSupertypes) + if (!type.isDartCoreObject) type.element, + ]; + for (final cls in chain) { + for (final member in [ + ...cls.methods, + ...cls.getters, + ]) { + final name = member.name; + if (name == null || member.isStatic || name.startsWith('_')) continue; + result.putIfAbsent(name, () => member); + } + } + return result; + } + + Map statics(InterfaceElement element) { + return { + for (final method in element.methods) + if (method.isStatic && method.isPublic) method.name!: method, + }; + } + + /// The `value` of each constant of [element], an enum. + Set enumValues(EnumElement element) { + return { + for (final constant in element.constants) + ?constant.computeConstantValue()?.getField('value')?.toStringValue(), + }; + } +} + +final class _DartCallable { + _DartCallable(this.id, this.element); + + final String id; + final ExecutableElement element; + + bool get isGetter => element is GetterElement; + + List get positional => [ + for (final param in element.formalParameters) + if (!param.isNamed) param, + ]; + + Map get named => { + for (final param in element.formalParameters) + if (param.isNamed) param.name!: param, + }; + + int get requiredPositional => + positional.where((param) => param.isRequiredPositional).length; + + String render() { + final positional = this.positional; + final required = [ + for (final param in positional) + if (param.isRequiredPositional) _renderParam(param), + ]; + final optional = [ + for (final param in positional) + if (param.isOptionalPositional) _renderParam(param), + ]; + final named = [ + for (final param in this.named.values) + '${param.isRequiredNamed ? 'required ' : ''}${_renderParam(param)}', + ]; + final parts = [ + ...required, + if (optional.isNotEmpty) '[${optional.join(', ')}]', + if (named.isNotEmpty) '{${named.join(', ')}}', + ]; + return '${element.name}(${parts.join(', ')})'; + } + + static String _renderParam(FormalParameterElement param) { + return '${param.type.getDisplayString()} ${param.name}'; + } +} + +// --- Comparison -------------------------------------------------------------- + +final class _Mismatch { + _Mismatch(this.id, this.message); + + final String id; + final String message; + + String get member => id.split('#').first; + + bool matches(String key) => key == id || key == member; + + @override + String toString() => '$id: $message'; +} + +final class _Report { + _Report(this.node); + + final _NodeApi node; + final List<_Mismatch> mismatches = []; + + /// Problems with the tables themselves, such as unknown names. + final List problems = []; + final Set dartOnlyUsed = {}; + final Set renamesUsed = {}; + + String? coveringKey(_Mismatch mismatch) { + for (final table in [_knownDifferences, _pendingFixes]) { + if (table.containsKey(mismatch.id)) return mismatch.id; + if (table.containsKey(mismatch.member)) return mismatch.member; + } + return null; + } +} + +/// Signature style of a callable, which decides what may differ. +enum _Style { + /// Functions and expression methods. + plain, + + /// Pipeline stages: options objects map onto named parameters. + stage, + + /// `xLiteral` conveniences: only the arity is compared. + literal, +} + +final class _Checker { + _Checker(this.node, this.dart) : report = _Report(node); + + final _NodeApi node; + final _DartApi dart; + final _Report report; + + /// For each base option key (`StageOptions.rawOptions`), the stages that + /// lack it in Dart, and how many stages Node offers it on. + final Map> _missingBaseKeys = {}; + final Map _baseKeyStages = {}; + + void _mismatch(String id, String message) { + report.mismatches.add(_Mismatch(id, message)); + } + + String _rename(String nodeOwner, String name) { + final key = '$nodeOwner.$name'; + final renamed = _renames[key]; + if (renamed == null) return name; + report.renamesUsed.add(key); + return renamed; + } + + /// The Dart-only check: allowed when listed in [_dartOnly]. + void _dartOnlyMember(String id, String message) { + if (_dartOnly.containsKey(id)) { + report.dartOnlyUsed.add(id); + return; + } + _mismatch(id, message); + } + + _Report run() { + _checkFunctions(); + _checkClasses(); + _checkEntryPoints(); + _checkEnums(); + _checkTypes(); + _checkExports(); + for (final MapEntry(key: key, value: missing) in _missingBaseKeys.entries) { + if (missing.isEmpty) continue; + _mismatch( + 'StageOptions#$key', + 'Node accepts `$key` on every stage; Dart lacks it on ' + '${missing.length} of the ${_baseKeyStages[key]} stages it has: ' + '${missing.join(', ')}', + ); + } + return report; + } + + // Top-level functions. + + void _checkFunctions() { + final namespaces = >{}; + for (final name in _functionNamespaces) { + final element = dart.interfaceNamed(name); + if (element == null) { + report.problems.add('_functionNamespaces: Dart exports no $name'); + } else { + namespaces[name] = dart.statics(element); + } + } + + for (final function in node.functions.values) { + final candidates = [ + if (dart.topLevelFunctions[function.name] case final element?) + _DartCallable(function.name, element), + for (final MapEntry(key: owner, value: statics) in namespaces.entries) + if (statics[function.name] case final element?) + _DartCallable('$owner.${function.name}', element), + ]; + if (candidates.isEmpty) { + if (!function.runtimeOnly) { + _mismatch( + 'PipelineFunctions.${function.name}', + 'missing in Dart: Node exports ' + '${_renderNode(function.name, function.overloads)}', + ); + } + continue; + } + for (final candidate in candidates) { + _compare(function, candidate, _Style.plain); + } + } + + for (final MapEntry(key: owner, value: statics) in namespaces.entries) { + for (final name in statics.keys) { + if (!node.functions.containsKey(name)) { + _dartOnlyMember( + '$owner.$name', + 'Dart-only: Node has no top-level function `$name`', + ); + } + } + } + for (final name in dart.topLevelFunctions.keys) { + if (!node.functions.containsKey(name)) { + _dartOnlyMember(name, 'Dart-only: Node has no function `$name`'); + } + } + } + + // Classes. + + void _checkClasses() { + final nodeClassesByDart = >{}; + for (final MapEntry(key: nodeName, value: dartName) in _classes.entries) { + final nodeClass = node.classes[nodeName]; + final dartClass = dart.interfaceNamed(dartName); + if (nodeClass == null) { + report.problems.add('_classes: Node has no $nodeName'); + continue; + } + if (dartClass == null) { + _mismatch( + 'export:$dartName', + 'Node exports $nodeName, but its Dart counterpart $dartName is not ' + 'exported from lib/google_cloud_firestore.dart', + ); + continue; + } + final dartLabel = dartClass.name!; + (nodeClassesByDart[dartLabel] ??= []).add(nodeName); + final dartMembers = dart.members(dartClass); + + for (final member in nodeClass.members.values) { + // Runtime-only members are not part of the typed contract; they only + // count as Node having the Dart member. + if (member.runtimeOnly) continue; + if (_nodeTypeTags.contains(member.name)) continue; + final dartName = _rename(nodeName, member.name); + final id = '$dartLabel.$dartName'; + final element = dartMembers[dartName]; + if (element == null) { + _mismatch( + id, + 'missing in Dart: Node $nodeName.' + '${member.isMethod ? _renderNode(member.name, member.overloads) : member.name}', + ); + continue; + } + _compareMember(member, _DartCallable(id, element)); + } + } + + // Dart members must exist on one of the Node classes they stand for. + for (final MapEntry(key: dartName, value: nodeNames) + in nodeClassesByDart.entries) { + final dartClass = dart.interfaceNamed(dartName)!; + final reverseRenames = { + for (final nodeName in nodeNames) + for (final MapEntry(:key, :value) in _renames.entries) + if (key.startsWith('$nodeName.')) value: key.split('.').last, + }; + final declared = dart.members(dartClass, declaredOnly: true); + for (final MapEntry(key: name, value: element) in declared.entries) { + if (const { + 'hashCode', + 'toString', + 'noSuchMethod', + 'runtimeType', + }.contains(name)) { + continue; + } + final nodeName = reverseRenames[name] ?? name; + if (nodeName == '==' && !reverseRenames.containsKey('==')) continue; + if (nodeNames.any((cls) => node.lookUp(cls, nodeName) != null)) { + continue; + } + final id = '$dartName.$name'; + if (_isLiteralVariant(name, declared)) { + final base = name.substring(0, name.length - 'Literal'.length); + final nodeBase = nodeNames + .map((cls) => node.lookUp(cls, _unrename(cls, base))) + .nonNulls + .firstOrNull; + if (nodeBase != null) { + _compare(nodeBase, _DartCallable(id, element), _Style.literal); + } + continue; + } + _dartOnlyMember( + id, + 'Dart-only: Node ${nodeNames.join('/')} has no `$name`', + ); + } + } + } + + String _unrename(String nodeClass, String dartName) { + for (final MapEntry(:key, :value) in _renames.entries) { + if (value == dartName && key.startsWith('$nodeClass.')) { + return key.split('.').last; + } + } + return dartName; + } + + bool _isLiteralVariant(String name, Map members) { + if (!name.endsWith('Literal') || name == 'Literal') return false; + return members.containsKey( + name.substring(0, name.length - 'Literal'.length), + ); + } + + void _compareMember(_NodeMember member, _DartCallable callable) { + if (member.isMethod == callable.isGetter) { + _mismatch( + '${callable.id}#kind', + 'Node ${member.owner}.${member.name} is a ' + '${member.isMethod ? 'method' : 'property'}, Dart has a ' + '${callable.isGetter ? 'getter' : 'method'}', + ); + return; + } + if (!member.isMethod) return; + final stage = const {'Pipeline', 'PipelineSource'}.contains(member.owner); + _compare(member, callable, stage ? _Style.stage : _Style.plain); + } + + void _checkEntryPoints() { + for (final MapEntry(key: owner, value: members) + in node.entryPoints.entries) { + final dartClass = dart.interfaceNamed(owner); + if (dartClass == null) { + _mismatch(owner, 'missing in Dart: Node entry point owner $owner'); + continue; + } + final dartMembers = dart.members(dartClass); + for (final member in members.values) { + final dartName = _rename(owner, member.name); + final id = '$owner.$dartName'; + final element = dartMembers[dartName]; + if (element == null) { + _mismatch( + id, + 'missing in Dart: Node ${_renderNode(member.name, member.overloads)}', + ); + continue; + } + _compare(member, _DartCallable(id, element), _Style.plain); + } + } + } + + // Signatures. + + void _compare(_NodeMember member, _DartCallable dart, _Style style) { + final optionOverloads = [ + for (final overload in member.overloads) + if (overload.params.length == 1 && overload.params.single.isOptions) + overload, + ]; + var positional = [ + for (final overload in member.overloads) + if (!optionOverloads.contains(overload)) overload.params, + ]; + final named = dart.named; + + if (style == _Style.stage) { + // A stage may take a Node positional parameter as a named one, as + // Dart's unnest(selectable, {indexField}) does. + positional = [ + for (final params in positional) _dropTrailingNamed(params, named), + ]; + } + + final coveredNamed = {}; + if (optionOverloads.isNotEmpty) { + coveredNamed.addAll( + _compareOptions( + dart, + optionOverloads, + hasPositionalForm: positional.isNotEmpty, + ), + ); + } + if (style == _Style.stage) { + for (final params in member.overloads.map((o) => o.params)) { + coveredNamed.addAll(params.map((param) => param.name)); + } + } + if (style != _Style.literal) { + for (final name in named.keys) { + if (coveredNamed.contains(name)) continue; + _mismatch( + '${dart.id}#$name', + 'Dart-only named parameter `$name`: Node ' + '${_renderNode(member.name, member.overloads)} has no such option', + ); + } + } + + if (positional.isEmpty) { + if (dart.positional.isNotEmpty) { + _mismatch( + '${dart.id}#arity', + 'Node takes only an options object, Dart takes ' + '${dart.positional.length} positional argument(s) — Node ' + '${_renderNode(member.name, member.overloads)}; Dart ' + '${dart.render()}', + ); + } + } else { + _comparePositional(member, positional, dart, style); + } + + if (style != _Style.literal && !member.runtimeOnly) { + _compareReturns(member, dart); + } + } + + List<_NodeParam> _dropTrailingNamed( + List<_NodeParam> params, + Map named, + ) { + var end = params.length; + while (end > 0 && named.containsKey(params[end - 1].name)) { + end--; + } + return params.sublist(0, end); + } + + void _comparePositional( + _NodeMember member, + List> overloads, + _DartCallable dart, + _Style style, + ) { + final nodeMin = overloads + .map((params) => params.where((param) => param.required).length) + .reduce((a, b) => a < b ? a : b); + final restIndexes = [ + for (final params in overloads) + for (final (index, param) in params.indexed) + if (param.rest) index, + ]; + final restIndex = restIndexes.isEmpty + ? null + : restIndexes.reduce((a, b) => a < b ? a : b); + final nodeMax = restIndex != null + ? null + : overloads + .map((params) => params.length) + .reduce((a, b) => a > b ? a : b); + + final positional = dart.positional; + final dartRequired = dart.requiredPositional; + final dartTotal = positional.length; + + // A trailing Dart Iterable takes Node's rest parameter, possibly folding + // the parameters before it too. + final collapsedAt = + restIndex != null && + dartTotal > 0 && + dartTotal - 1 <= restIndex && + _iterableElement(positional.last.type) != null + ? dartTotal - 1 + : null; + + final problems = []; + if (member.runtimeOnly) { + // JavaScript cannot tell which parameters are optional; only check that + // Dart takes as many. + if (collapsedAt == null && (nodeMax == null || nodeMax > dartTotal)) { + problems.add( + 'Node takes ${nodeMax ?? 'any number of'} argument(s), Dart at most ' + '$dartTotal', + ); + } + } else if (collapsedAt != null) { + final expected = nodeMin < collapsedAt ? nodeMin : collapsedAt; + final actual = dartRequired < collapsedAt ? dartRequired : collapsedAt; + if (expected != actual) { + problems.add( + 'Node requires $expected argument(s) before ' + '`${positional[collapsedAt].name}`, Dart requires $actual', + ); + } + if (collapsedAt >= nodeMin && + positional[collapsedAt].isRequiredPositional) { + problems.add( + 'Node lets the trailing arguments be omitted, Dart requires the ' + 'Iterable `${positional[collapsedAt].name}`', + ); + } + } else { + if (dartRequired != nodeMin) { + problems.add( + 'Node requires $nodeMin argument(s), Dart requires $dartRequired', + ); + } + if (nodeMax == null) { + problems.add( + 'Node accepts any number of arguments, Dart at most $dartTotal', + ); + } else if (nodeMax > dartTotal) { + problems.add('Node accepts up to $nodeMax, Dart at most $dartTotal'); + } else if (dartTotal > nodeMax) { + problems.add('Dart accepts up to $dartTotal, Node at most $nodeMax'); + } + } + if (problems.isNotEmpty) { + _mismatch( + '${dart.id}#arity', + '${problems.join('; ')} — Node ' + '${_renderNode(member.name, member.overloads)}; Dart ' + '${dart.render()}', + ); + } + + if (style == _Style.literal || member.runtimeOnly) return; + + _NodeType? typeAt(int index) { + final types = [ + for (final params in overloads) + for (final (i, param) in params.indexed) + if (i == index && !param.rest || i <= index && param.rest) + param.type, + ]; + return types.isEmpty ? null : _NodeType.union(types); + } + + final compared = collapsedAt ?? dartTotal; + for (var i = 0; i < compared; i++) { + final type = typeAt(i); + if (type == null) continue; + _compareType( + '${dart.id}#${positional[i].name}', + 'parameter ${i + 1} (`${positional[i].name}`)', + type, + positional[i].type, + ); + } + if (collapsedAt != null) { + final folded = _NodeType.union([ + for (var i = collapsedAt; i <= restIndex!; i++) ?typeAt(i), + ]); + final param = positional[collapsedAt]; + _compareType( + '${dart.id}#${param.name}', + 'the elements of `${param.name}`, which take Node\'s arguments from ' + 'position ${collapsedAt + 1} on', + folded, + _iterableElement(param.type)!, + ); + } + } + + /// Compares the keys of Node's options objects with Dart's named + /// parameters, returning the named parameters they account for. + Set _compareOptions( + _DartCallable dart, + List<_NodeSignature> overloads, { + required bool hasPositionalForm, + }) { + final covered = {}; + final named = dart.named; + for (final optionsName in { + for (final overload in overloads) ...overload.params.single.type.options, + }) { + for (final base in node.optionBases(optionsName)) { + for (final key in node.optionProperties(base).keys) { + _baseKeyStages[key] = (_baseKeyStages[key] ?? 0) + 1; + final missing = _missingBaseKeys[key] ??= []; + if (named.containsKey(key)) { + covered.add(key); + } else { + missing.add(dart.id); + } + } + } + for (final MapEntry(key: key, value: property) + in node.optionProperties(optionsName).entries) { + final dartName = _rename(optionsName, key); + final param = named[dartName]; + if (param == null) { + // A stage's required option is its positional argument. + if (property.optional || !hasPositionalForm) { + _mismatch( + '${dart.id}#$key', + 'Node option `$key` (${property.type.label}) of $optionsName ' + 'has no Dart named parameter — Dart ${dart.render()}', + ); + } + continue; + } + covered.add(dartName); + if (property.optional == param.isRequiredNamed) { + _mismatch( + '${dart.id}#$dartName', + 'Node option `$key` of $optionsName is ' + '${property.optional ? 'optional' : 'required'}, Dart\'s ' + '`$dartName` is ${param.isRequiredNamed ? 'required' : 'optional'}', + ); + } + _compareOption( + '${dart.id}#$dartName', + '$optionsName.$key', + property, + param.type, + ); + } + } + return covered; + } + + /// Compares an option with the Dart type of its named parameter, recursing + /// into inline option objects such as `explainOptions`. + void _compareOption( + String id, + String nodeLabel, + _NodeProperty property, + DartType dartType, + ) { + // An index signature (`{[name: string]: unknown}`) is a plain map. + final nested = { + for (final MapEntry(:key, :value) in property.type.properties.entries) + if (key != '[key]') key: value, + }; + if (nested.isEmpty || property.type.kinds.length != 1) { + _compareType(id, 'option `$nodeLabel`', property.type, dartType); + return; + } + final element = dartType is InterfaceType ? dartType.element : null; + final constructor = element?.unnamedConstructor; + if (constructor == null) { + _compareType(id, 'option `$nodeLabel`', property.type, dartType); + return; + } + final params = { + for (final param in constructor.formalParameters) + if (param.isNamed) param.name!: param, + }; + for (final MapEntry(key: key, value: nestedProperty) in nested.entries) { + final param = params[key]; + if (param == null) { + _mismatch( + '$id.$key', + 'Node option `$nodeLabel.$key` has no named parameter on ' + '${element!.name}()', + ); + continue; + } + _compareOption('$id.$key', '$nodeLabel.$key', nestedProperty, param.type); + } + for (final name in params.keys) { + if (!nested.containsKey(name)) { + _mismatch( + '$id.$name', + 'Dart-only parameter `$name` of ${element!.name}(): Node ' + '`$nodeLabel` has no such key', + ); + } + } + } + + void _compareReturns(_NodeMember member, _DartCallable callable) { + final dartTypes = []; + for (final overload in member.overloads) { + final returns = overload.returns; + if (returns == null) return; + for (final kind in returns.kinds) { + final types = _dartTypesFor(kind, returns); + // Unmapped kinds (`any`, `documentData`, ...) cannot be compared. + if (types == null || types.length != 1) return; + dartTypes.add(types.single); + } + } + if (dartTypes.isEmpty) return; + final expected = dartTypes.reduce(dart.typeSystem.leastUpperBound); + final actual = callable.element.returnType; + if (!dart.typeSystem.isSubtypeOf(actual, expected)) { + _mismatch( + '${callable.id}#returns', + 'Node returns ${expected.getDisplayString()}, Dart returns ' + '${actual.getDisplayString()} — Node ' + '${_renderNode(member.name, member.overloads)}', + ); + } + } + + // Types. + + DartType? _iterableElement(DartType type) { + if (type is! InterfaceType) return null; + return type + .asInstanceOf(dart.typeProvider.iterableElement) + ?.typeArguments + .single; + } + + bool _isObject(DartType type) => type is DynamicType || type.isDartCoreObject; + + /// The Dart types a Node [kind] stands for, all of which a parameter must + /// accept; null when Dart has no equivalent. + List? _dartTypesFor(String kind, _NodeType type) { + final provider = dart.typeProvider; + InterfaceType exported(String name) => dart.exportedType(name); + return switch (kind) { + 'string' || 'literal' => [provider.stringType], + 'number' => [provider.numType], + 'boolean' => [provider.boolType], + 'map' => [ + provider.mapType(provider.stringType, provider.objectQuestionType), + ], + 'array' => [provider.iterableType(provider.objectQuestionType)], + 'expression' || 'aggregateFunction' => [exported('PipelineExpression')], + 'booleanExpression' => [exported('PipelineBooleanExpression')], + 'field' => [exported('PipelineField')], + 'aliasedExpression' || + 'aliasedAggregate' => [exported('PipelineAliasedExpression')], + 'selectable' => [ + exported('PipelineField'), + exported('PipelineAliasedExpression'), + ], + 'ordering' => [exported('PipelineOrdering')], + 'pipeline' => [exported('Pipeline')], + 'pipelineSnapshot' => [exported('PipelineSnapshot')], + 'pipelineResult' => [exported('PipelineResult')], + 'pipelineSource' => [exported('PipelineSource')], + 'explainStats' => [exported('ExplainStats')], + 'vectorValue' => [exported('VectorValue')], + 'geoPoint' => [exported('GeoPoint')], + 'timestamp' => [exported('Timestamp')], + 'documentReference' => [exported('DocumentReference')], + 'collectionReference' => [exported('CollectionReference')], + 'fieldPath' => [exported('FieldPath')], + 'query' => [exported('Query')], + 'bytes' => [dart.libraryType(dart.typedData, 'Uint8List')], + 'date' => [dart.libraryType(dart.core, 'DateTime')], + 'promise' => switch (type.elements) { + final elements? when elements.kinds.length == 1 => + switch (_dartTypesFor(elements.kinds.single, elements)) { + [final value] => [provider.futureType(value)], + _ => null, + }, + _ => null, + }, + _ => null, + }; + } + + /// Whether a Dart parameter of type [dartType] accepts every value of + /// Node [kind]. + bool _accepts(DartType dartType, String kind, _NodeType type) { + if (kind == 'null' || kind == 'undefined') { + return dart.typeSystem.isNullable(dartType); + } + if (_isObject(dartType)) return true; + if (kind == 'any') return false; + if (kind == 'number') { + return dartType.isDartCoreNum || + dartType.isDartCoreInt || + dartType.isDartCoreDouble; + } + if (kind == 'array') return _iterableElement(dartType) != null; + if (kind == 'literal' && dartType.element is EnumElement) { + final values = { + for (final value in dart.enumValues(dartType.element! as EnumElement)) + value.toLowerCase(), + }; + return node + .literalValues(type) + .every((value) => values.contains(value.toLowerCase())); + } + final types = _dartTypesFor(kind, type); + if (types == null) return false; + return types.every((t) => dart.typeSystem.isSubtypeOf(t, dartType)); + } + + void _compareType( + String id, + String where, + _NodeType nodeType, + DartType dartType, + ) { + final iterable = _iterableElement(dartType) != null; + if (iterable && + !nodeType.kinds.contains('array') && + !nodeType.kinds.contains('any')) { + _mismatch( + id, + 'Node takes a single value (${nodeType.label}) for $where, Dart takes ' + 'an Iterable (${dartType.getDisplayString()})', + ); + return; + } + final rejected = [ + for (final kind in nodeType.kinds) + if (!_accepts(dartType, kind, nodeType)) kind, + ]..sort(); + if (rejected.isNotEmpty) { + _mismatch( + id, + 'Node accepts ${nodeType.label} for $where, Dart\'s ' + '${dartType.getDisplayString()} rejects ${rejected.join('|')}', + ); + return; + } + // Compare the elements of a list with Node's array elements. + final nodeElements = nodeType.elements; + final dartElement = _iterableElement(dartType); + if (nodeElements != null && dartElement != null) { + _compareType(id, 'the elements of $where', nodeElements, dartElement); + } + } + + // String unions and exports. + + void _checkEnums() { + for (final MapEntry(key: nodeName, value: dartName) in _enums.entries) { + final element = dart.exports[dartName]; + final union = node.types[nodeName]; + if (element is! EnumElement || union?['kind'] != 'stringUnion') { + report.problems.add( + '_enums: $nodeName -> $dartName is not a union/enum', + ); + continue; + } + final nodeValues = _strings(union!['values']); + final dartValues = dart.enumValues(element); + for (final value in nodeValues.difference(dartValues)) { + _mismatch( + '$dartName#$value', + "Node's $nodeName includes '$value'; $dartName has no member with " + 'that value', + ); + } + for (final value in dartValues.difference(nodeValues)) { + _mismatch( + '$dartName#$value', + "Dart-only value '$value': Node's $nodeName does not include it", + ); + } + } + } + + void _checkTypes() { + // Options objects are compared through the parameters that take them. + final options = {}; + void addOptions(_NodeType type) { + for (final name in type.options) { + options + ..add(name) + ..addAll(node.optionBases(name)); + } + } + + final members = [ + ...node.functions.values, + for (final cls in node.classes.values) ...cls.members.values, + for (final owner in node.entryPoints.values) ...owner.values, + ]; + for (final member in members) { + for (final overload in member.overloads) { + overload.params.map((param) => param.type).forEach(addOptions); + } + } + + for (final MapEntry(key: name, value: type) in node.types.entries) { + switch (type['kind']) { + case 'stringUnion' when !_enums.containsKey(name): + _mismatch( + 'type:$name', + 'Node string union $name has no Dart enum in _enums', + ); + case 'object' when !options.contains(name): + _mismatch( + 'type:$name', + 'Node object type $name is not an options parameter of any ' + 'member; teach this test how it maps onto Dart', + ); + } + } + + for (final tag in _nodeTypeTags) { + if (!node.classes.values.any((cls) => cls.members.containsKey(tag))) { + report.problems.add('_nodeTypeTags: no Node class has `$tag`'); + } + } + } + + void _checkExports() { + final accounted = { + ..._classes.values, + ..._enums.values, + ..._optionTypes.keys, + ..._functionNamespaces, + ...dart.topLevelFunctions.keys, + }; + // Typedefs of a mapped class, such as BooleanExpression, are accounted + // for by that class. + final accountedClasses = { + for (final name in accounted) ?dart.interfaceNamed(name), + }; + for (final name in {..._optionTypes.keys, ..._enums.values}) { + if (!dart.exports.containsKey(name)) { + report.problems.add('$name is listed but not exported'); + } + } + for (final element in dart.pipelineDeclarations) { + final name = element.name!; + if (dart.exports[name] != element) { + _mismatch( + 'export:$name', + '$name is declared in ${element.firstFragment.libraryFragment?.source.shortName} ' + 'but not exported from lib/google_cloud_firestore.dart', + ); + } + if (!accounted.contains(name) && + !(element is TypeAliasElement && + accountedClasses.contains(dart.interfaceNamed(name)))) { + _dartOnlyMember( + name, + 'Dart-only type: map it onto Node in _classes, _enums or ' + '_optionTypes, or list it in _dartOnly', + ); + } + } + } +} + +/// Renders a Node overload set as one merged signature, such as +/// `join(arrayExpression: expression|string, delimiter: expression|string)`. +String _renderNode(String name, List<_NodeSignature> overloads) { + if (overloads.isEmpty) return name; + final length = overloads + .map((overload) => overload.params.length) + .reduce((a, b) => a > b ? a : b); + final parts = []; + for (var i = 0; i < length; i++) { + final params = [ + for (final overload in overloads) + if (i < overload.params.length) overload.params[i], + ]; + final names = {}; + for (final param in params) { + names[param.name] = (names[param.name] ?? 0) + 1; + } + final paramName = + (names.entries.toList()..sort((a, b) { + final byCount = b.value.compareTo(a.value); + return byCount != 0 ? byCount : a.key.compareTo(b.key); + })) + .first + .key; + final optional = + params.length < overloads.length || params.any((p) => p.optional); + final rest = params.any((p) => p.rest); + final type = _NodeType.union(params.map((p) => p.type)); + parts.add( + '${rest ? '...' : ''}$paramName${optional && !rest ? '?' : ''}: ' + '${type.label}', + ); + } + return '$name(${parts.join(', ')})'; +} diff --git a/packages/google_cloud_firestore/test/pipeline_e2e_coverage_test.dart b/packages/google_cloud_firestore/test/pipeline_e2e_coverage_test.dart new file mode 100644 index 00000000..0b94aa9f --- /dev/null +++ b/packages/google_cloud_firestore/test/pipeline_e2e_coverage_test.dart @@ -0,0 +1,1363 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Static guard over the live Pipeline E2E suite in `test/e2e/pipeline/`. +/// +/// The live suite needs credentials, so it only runs in its own workflow; this +/// guard runs in the ordinary credential-free `dart test`. It resolves the E2E +/// sources with `package:analyzer` and checks them against the Pipeline API +/// derived from the resolved library, so a newly added public function fails +/// here until it gets a live case: +/// +/// 1. **Member coverage.** Every public member declared in +/// `lib/src/pipeline.dart` — top-level functions, static and instance +/// methods, getters and constructors of its classes, and every enum value — +/// plus `Firestore.pipeline` and `Transaction.executePipeline`, is +/// referenced somewhere under `test/e2e/pipeline/`. References match by +/// resolved element, so the static `PipelineFunctions.round` and the fluent +/// `PipelineExpression.round` are distinct members, as are an override such +/// as `PipelineField.ascending` and `PipelineExpression.ascending`. Public +/// declarations of `pipeline.dart` missing from the barrel are reported +/// too. +/// 2. **Optional parameters.** Each optional parameter of those members is +/// supplied by at least one invocation and omitted by another. An explicit +/// `null` argument counts as omitted. +/// 3. **Value positions.** A parameter typed `Object`, `Object?` or `dynamic` +/// receives an argument whose static type is a `PipelineExpression` in at +/// least one invocation, and a plain value (a literal, a field-name +/// `String`, a number, ...) in another. For a parameter typed +/// `Iterable` or `Iterable` the same applies to the +/// elements of list literals passed to it. +/// 4. **Required shapes.** Each parameter listed in [_requiredShapes] +/// receives every listed argument shape at least once, and every +/// enum-typed parameter (such as `findNearest`'s `distanceMeasure`) +/// receives every value of its enum. +/// +/// [_notExecutable] and [_notValuePositions] exempt the few members and +/// parameters these rules cannot apply to. Entries that name nothing, or that +/// no longer prevent a gap, fail the guard, as do stale entries in the shape +/// and family tables. +/// +/// The E2E files must also import only the public barrel, `package:test`, +/// the harness and `dart:` libraries, be tagged `prod`, and declare their tests +/// through `pipelineE2E`. +/// +/// **Adding a Pipeline API member:** add a live case for it — and for each of +/// its optional and value-position parameters — to the file of its family +/// (see [_families] and [_familyOf]), or this guard fails. A member whose name +/// matches no family table fails the guard until it is assigned one. +@Timeout(Duration(minutes: 5)) +library; + +import 'dart:io'; +import 'dart:isolate'; + +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/dart/element/type_system.dart'; +import 'package:analyzer/diagnostic/diagnostic.dart'; +import 'package:test/test.dart'; + +/// The E2E sources this guard scans, relative to the package root. +const _e2eDirectory = 'test/e2e/pipeline'; + +const _barrelUri = 'package:google_cloud_firestore/google_cloud_firestore.dart'; + +/// The library whose `pipeline.dart` part declares the Pipeline API. +const _implementationUri = 'package:google_cloud_firestore/src/firestore.dart'; + +const _pipelinePart = 'package:google_cloud_firestore/src/pipeline.dart'; + +/// Pipeline API members declared outside `pipeline.dart`. +const _entryPoints = { + 'Firestore': ['pipeline'], + 'Transaction': ['executePipeline'], +}; + +/// Members of `Object`, which are never part of the universe. +const _objectMembers = { + '==', + 'hashCode', + 'noSuchMethod', + 'runtimeType', + 'toString', +}; + +/// The families the API is split into, with the E2E file owning each. +/// +/// The function families follow the README's "Function reference" table: +/// Generic (`length`, `reverse`, `concat`) belongs to `string`, Reference to +/// `map_reference`, and Type, Debug and Search to `type_debug`. +const _families = { + 'sources': 'sources_test.dart', + 'stages': 'stages_test.dart', + 'results': 'results_test.dart', + 'execute': 'execute_test.dart', + 'aggregates': 'aggregates_test.dart', + 'comparison_logical': 'functions_comparison_logical_test.dart', + 'arithmetic': 'functions_arithmetic_test.dart', + 'string': 'functions_string_test.dart', + 'array': 'functions_array_test.dart', + 'map_reference': 'functions_map_reference_test.dart', + 'timestamp_vector': 'functions_timestamp_vector_test.dart', + 'type_debug': 'functions_type_debug_test.dart', +}; + +/// Family overrides for single members, checked first by [_familyOf]. +const _familyByMember = { + 'Pipeline.aggregate': 'aggregates', + 'Pipeline.execute': 'execute', + 'Pipeline.firestore': 'execute', + 'PipelineField.path': 'type_debug', +}; + +/// Families by declaring class, checked second by [_familyOf]. +const _familyByClass = { + 'Firestore': 'sources', + 'Pipeline': 'stages', + 'PipelineAliasedExpression': 'stages', + 'PipelineExplainMode': 'execute', + 'PipelineExplainOptions': 'execute', + 'PipelineExplainOutputFormat': 'execute', + 'PipelineIndexMode': 'execute', + 'PipelineOrdering': 'stages', + 'PipelineResult': 'results', + 'PipelineSnapshot': 'results', + 'PipelineSource': 'sources', + 'PipelineValueType': 'type_debug', + 'Transaction': 'execute', +}; + +/// Families by member name, checked last by [_familyOf]. +/// +/// Shared by every form of a function: static (`PipelineFunctions`, +/// `Expression`), fluent (`PipelineExpression`, `PipelineBooleanExpression`, +/// `PipelineField`) and top-level. +const _familyByName = { + // Comparison and logical. + 'and': 'comparison_logical', + 'cmp': 'comparison_logical', + 'coalesce': 'comparison_logical', + 'conditional': 'comparison_logical', + 'equal': 'comparison_logical', + 'equalAny': 'comparison_logical', + 'greaterThan': 'comparison_logical', + 'greaterThanOrEqual': 'comparison_logical', + 'ifNull': 'comparison_logical', + 'lessThan': 'comparison_logical', + 'lessThanOrEqual': 'comparison_logical', + 'nor': 'comparison_logical', + 'not': 'comparison_logical', + 'notEqual': 'comparison_logical', + 'notEqualAny': 'comparison_logical', + 'or': 'comparison_logical', + 'switchOn': 'comparison_logical', + 'xor': 'comparison_logical', + // Aggregates. + 'arrayAgg': 'aggregates', + 'arrayAggDistinct': 'aggregates', + 'average': 'aggregates', + 'count': 'aggregates', + 'countAll': 'aggregates', + 'countDistinct': 'aggregates', + 'countIf': 'aggregates', + 'first': 'aggregates', + 'last': 'aggregates', + 'maximum': 'aggregates', + 'minimum': 'aggregates', + 'sum': 'aggregates', + // Arithmetic. + 'abs': 'arithmetic', + 'add': 'arithmetic', + 'ceil': 'arithmetic', + 'divide': 'arithmetic', + 'exp': 'arithmetic', + 'floor': 'arithmetic', + 'ln': 'arithmetic', + 'log': 'arithmetic', + 'log10': 'arithmetic', + 'logicalMaximum': 'arithmetic', + 'logicalMinimum': 'arithmetic', + 'mod': 'arithmetic', + 'multiply': 'arithmetic', + 'pow': 'arithmetic', + 'rand': 'arithmetic', + 'round': 'arithmetic', + 'sqrt': 'arithmetic', + 'subtract': 'arithmetic', + 'trunc': 'arithmetic', + // Array. + 'array': 'array', + 'arrayConcat': 'array', + 'arrayConcatMultiple': 'array', + 'arrayContains': 'array', + 'arrayContainsAll': 'array', + 'arrayContainsAllFrom': 'array', + 'arrayContainsAny': 'array', + 'arrayFilter': 'array', + 'arrayFirst': 'array', + 'arrayFirstN': 'array', + 'arrayGet': 'array', + 'arrayIndexOf': 'array', + 'arrayIndexOfAll': 'array', + 'arrayLast': 'array', + 'arrayLastIndexOf': 'array', + 'arrayLastN': 'array', + 'arrayLength': 'array', + 'arrayMaximum': 'array', + 'arrayMaximumN': 'array', + 'arrayMinimum': 'array', + 'arrayMinimumN': 'array', + 'arrayReverse': 'array', + 'arraySlice': 'array', + 'arraySum': 'array', + 'arrayTransform': 'array', + 'arrayTransformWithIndex': 'array', + 'join': 'array', + 'joinLiteral': 'array', + 'maximumN': 'array', + 'minimumN': 'array', + 'variable': 'array', + // String, including the generic length / reverse / concat. + 'byteLength': 'string', + 'charLength': 'string', + 'concat': 'string', + 'endsWith': 'string', + 'length': 'string', + 'like': 'string', + 'ltrim': 'string', + 'regexContains': 'string', + 'regexFind': 'string', + 'regexFindAll': 'string', + 'regexMatch': 'string', + 'reverse': 'string', + 'rtrim': 'string', + 'split': 'string', + 'splitLiteral': 'string', + 'startsWith': 'string', + 'stringConcat': 'string', + 'stringContains': 'string', + 'stringIndexOf': 'string', + 'stringRepeat': 'string', + 'stringReplaceAll': 'string', + 'stringReplaceAllLiteral': 'string', + 'stringReplaceOne': 'string', + 'stringReplaceOneLiteral': 'string', + 'stringReverse': 'string', + 'substring': 'string', + 'substringLiteral': 'string', + 'toLower': 'string', + 'toLowerCase': 'string', + 'toUpper': 'string', + 'toUpperCase': 'string', + 'trim': 'string', + // Map and reference. + 'collectionId': 'map_reference', + 'currentDocument': 'map_reference', + 'documentId': 'map_reference', + 'getField': 'map_reference', + 'map': 'map_reference', + 'mapEntries': 'map_reference', + 'mapGet': 'map_reference', + 'mapGetLiteral': 'map_reference', + 'mapKeys': 'map_reference', + 'mapMerge': 'map_reference', + 'mapRemove': 'map_reference', + 'mapSet': 'map_reference', + 'mapValues': 'map_reference', + 'parent': 'map_reference', + 'referenceSlice': 'map_reference', + // Timestamp and vector. + 'cosineDistance': 'timestamp_vector', + 'currentTimestamp': 'timestamp_vector', + 'dotProduct': 'timestamp_vector', + 'euclideanDistance': 'timestamp_vector', + 'geoDistance': 'timestamp_vector', + 'timestampAdd': 'timestamp_vector', + 'timestampDiff': 'timestamp_vector', + 'timestampExtract': 'timestamp_vector', + 'timestampSubtract': 'timestamp_vector', + 'timestampToUnixMicros': 'timestamp_vector', + 'timestampToUnixMillis': 'timestamp_vector', + 'timestampToUnixSeconds': 'timestamp_vector', + 'timestampTruncate': 'timestamp_vector', + 'unixMicrosToTimestamp': 'timestamp_vector', + 'unixMillisToTimestamp': 'timestamp_vector', + 'unixSecondsToTimestamp': 'timestamp_vector', + 'vector': 'timestamp_vector', + 'vectorLength': 'timestamp_vector', + // Type, debugging, search and expression primitives. + 'asBoolean': 'type_debug', + 'constant': 'type_debug', + 'documentMatches': 'type_debug', + 'exists': 'type_debug', + 'field': 'type_debug', + 'ifAbsent': 'type_debug', + 'ifError': 'type_debug', + 'isAbsent': 'type_debug', + 'isError': 'type_debug', + 'isType': 'type_debug', + 'pipelineFunction': 'type_debug', + 'raw': 'type_debug', + 'score': 'type_debug', + 'type': 'type_debug', + // Orderings and aliases, used by the sort and projection stages. + 'as': 'stages', + 'ascending': 'stages', + 'descending': 'stages', +}; + +/// The argument shapes [_requiredShapes] can demand. +enum _Shape { + /// The argument is a list, set or map literal holding a `PipelineExpression` + /// at any depth. + collectionWithExpression( + 'a list/set/map literal holding a PipelineExpression', + ), + + /// The argument is a list literal with an element that is itself a list, + /// set or map literal holding a `PipelineExpression`. + nestedCollectionWithExpression( + 'a list literal whose element is a list/set/map literal holding a ' + 'PipelineExpression', + ), + + /// The argument is a non-empty list literal of numbers. + numericList('a list literal of numbers'), + + /// The argument is a `String` literal containing a dot. + dottedPath("a dotted String literal such as 'a.b'"), + + /// The argument's static type is `FieldPath`, or it is a list or set + /// literal holding one. + fieldPath('a FieldPath, or a list literal holding one'); + + const _Shape(this.description); + + final String description; +} + +/// Argument shapes that must each be exercised at least once (rule 4). +/// +/// Collections holding expressions: wherever the Node SDK types a parameter +/// `Array`, `unknown[]` or `Record` — +/// and wherever it takes `unknown`, which it converts with +/// `valueToDefaultExpr` — a Dart collection literal may hold expressions and +/// must reach the backend as an `array(...)` / `map(...)` function rather +/// than a literal value. Each distinct encoding path appears here at least +/// once: the search-space functions, the generic value path, the variadic +/// first-argument path and nested construction. +/// +/// Numeric lists: vector positions accept a plain list of numbers, which must +/// be sent as a vector rather than an array. +const _requiredShapes = >{ + // Search spaces. + 'PipelineFunctions.equalAny(searchSpace)': [_Shape.collectionWithExpression], + 'PipelineFunctions.notEqualAny(searchSpace)': [ + _Shape.collectionWithExpression, + ], + 'PipelineFunctions.arrayContainsAll(searchValues)': [ + _Shape.collectionWithExpression, + ], + 'PipelineFunctions.arrayContainsAny(searchValues)': [ + _Shape.collectionWithExpression, + ], + 'PipelineExpression.equalAny(searchSpace)': [_Shape.collectionWithExpression], + 'PipelineExpression.notEqualAny(searchSpace)': [ + _Shape.collectionWithExpression, + ], + 'PipelineExpression.arrayContainsAll(values)': [ + _Shape.collectionWithExpression, + ], + 'PipelineExpression.arrayContainsAny(values)': [ + _Shape.collectionWithExpression, + ], + // Generic value positions. + 'equal(right)': [_Shape.collectionWithExpression], + 'PipelineFunctions.equal(right)': [_Shape.collectionWithExpression], + 'PipelineExpression.equal(other)': [_Shape.collectionWithExpression], + 'PipelineFunctions.conditional(trueCase)': [_Shape.collectionWithExpression], + 'PipelineBooleanExpression.conditional(thenExpression)': [ + _Shape.collectionWithExpression, + ], + 'PipelineExpression.mapSet(value)': [_Shape.collectionWithExpression], + 'PipelineExpression.arrayConcat(secondArray)': [ + _Shape.collectionWithExpression, + ], + // Variadic arguments and nested construction. + 'PipelineFunctions.arrayConcat(arrays)': [ + _Shape.nestedCollectionWithExpression, + ], + 'PipelineExpression.arrayConcatMultiple(otherArrays)': [ + _Shape.nestedCollectionWithExpression, + ], + 'PipelineFunctions.mapMerge(maps)': [_Shape.nestedCollectionWithExpression], + 'PipelineExpression.mapMerge(maps)': [_Shape.nestedCollectionWithExpression], + 'PipelineFunctions.mapSet(keyValues)': [ + _Shape.nestedCollectionWithExpression, + ], + 'PipelineFunctions.array(values)': [_Shape.nestedCollectionWithExpression], + 'Expression.array(values)': [_Shape.nestedCollectionWithExpression], + 'PipelineFunctions.map(keyValues)': [_Shape.nestedCollectionWithExpression], + // Vector positions. + 'PipelineFunctions.cosineDistance(right)': [_Shape.numericList], + 'PipelineFunctions.dotProduct(right)': [_Shape.numericList], + 'PipelineFunctions.euclideanDistance(right)': [_Shape.numericList], + 'PipelineExpression.cosineDistance(other)': [_Shape.numericList], + 'PipelineExpression.dotProduct(other)': [_Shape.numericList], + 'PipelineExpression.euclideanDistance(other)': [_Shape.numericList], + 'Pipeline.findNearest(queryVector)': [_Shape.numericList], + // Field paths. + 'field(fieldPath)': [_Shape.dottedPath, _Shape.fieldPath], + 'Expression.field(fieldPath)': [_Shape.fieldPath], + // Field names, which take a FieldPath wherever they take a String. + 'ascending(expression)': [_Shape.fieldPath], + 'descending(expression)': [_Shape.fieldPath], + 'Pipeline.select(selections)': [_Shape.fieldPath], + 'Pipeline.distinct(groups)': [_Shape.fieldPath], + 'Pipeline.aggregate(groups)': [_Shape.fieldPath], + 'Pipeline.removeFields(fields)': [_Shape.fieldPath], + 'Pipeline.unnest(selectable)': [_Shape.fieldPath], + 'Pipeline.replaceWith(expression)': [_Shape.fieldPath], + 'Pipeline.findNearest(vectorField)': [_Shape.fieldPath], + 'Pipeline.findNearest(distanceResultField)': [_Shape.fieldPath], + // Result paths. + 'PipelineResult.get(field)': [_Shape.dottedPath, _Shape.fieldPath], +}; + +/// Why a stage with no backend options has no live `rawOptions` case. +const _noStageOptions = + 'the stage has no backend options (the Node SDK knows none, and no SDK ' + 'system test sends raw ones), so any key would be an unknown option the ' + 'backend may reject, and an empty map equals omitting it; the ' + 'findNearest and unnest cases send real raw options through the same ' + 'encoding'; + +/// Why a collection source has no live `rawOptions` case. +const _forceIndexOnly = + "the stage's only backend option, force_index, needs a named index the " + 'E2E database does not declare (the Node SDK system tests skip forceIndex ' + 'for the same reason), any other key would be an unknown option the ' + 'backend may reject, and an empty map equals omitting it'; + +/// Members (`Member`) and parameters (`Member(param)`) that cannot run +/// against the E2E database, exempt from every rule. +/// +/// Keep this minimal: anything the backend can execute gets a live case. +const _notExecutable = { + // Raw stage options. Pipeline.findNearest and Pipeline.unnest pass theirs + // live, overriding their typed options. + 'PipelineSource.collection(rawOptions)': _forceIndexOnly, + 'PipelineSource.collectionGroup(rawOptions)': _forceIndexOnly, + 'PipelineSource.collectionReference(rawOptions)': _forceIndexOnly, + 'PipelineSource.database(rawOptions)': _noStageOptions, + 'PipelineSource.documents(rawOptions)': _noStageOptions, + 'Pipeline.addFields(rawOptions)': _noStageOptions, + 'Pipeline.aggregate(rawOptions)': _noStageOptions, + 'Pipeline.distinct(rawOptions)': _noStageOptions, + 'Pipeline.limit(rawOptions)': _noStageOptions, + 'Pipeline.offset(rawOptions)': _noStageOptions, + 'Pipeline.removeFields(rawOptions)': _noStageOptions, + 'Pipeline.replaceWith(rawOptions)': _noStageOptions, + 'Pipeline.sample(rawOptions)': _noStageOptions, + 'Pipeline.select(rawOptions)': _noStageOptions, + 'Pipeline.sort(rawOptions)': _noStageOptions, + 'Pipeline.union(rawOptions)': _noStageOptions, + 'Pipeline.where(rawOptions)': _noStageOptions, + 'Pipeline.search': + 'needs a full-text search index, which pipeline_e2e_books does not ' + 'have; the Node SDK system tests do not cover search either ' + '(TODO(search))', + 'PipelineExpression.geoDistance': 'only valid inside a search stage', + 'PipelineFunctions.documentMatches': 'only valid inside a search stage', + 'PipelineFunctions.geoDistance': 'only valid inside a search stage', + 'PipelineFunctions.score': 'only valid inside a search stage', + 'documentMatches': 'only valid inside a search stage', + 'score': 'only valid inside a search stage', + 'Expression.raw(options)': + 'no backend function takes options yet (the Node SDK only uses them for ' + 'the disabled snippet function); an empty map equals omitting it', + 'PipelineFunctions.raw(options)': + 'no backend function takes options yet (the Node SDK only uses them for ' + 'the disabled snippet function); an empty map equals omitting it', + 'pipelineFunction(options)': + 'no backend function takes options yet (the Node SDK only uses them for ' + 'the disabled snippet function); an empty map equals omitting it', +}; + +/// `Object`-typed parameters (`Member(param)`) that are not Pipeline value +/// positions, exempt from rule 3. +const _notValuePositions = { + 'Expression.constant(value)': + 'wraps a literal value; an expression is not a constant', + 'Expression.field(fieldPath)': + 'takes a String or a FieldPath; see the fieldPath shape', + 'PipelineFunctions.map(keyValues)': + 'takes the Map or alternating key/value Iterable itself; expressions go ' + 'in its values (see the nestedCollectionWithExpression shape)', + 'PipelineResult.get(field)': + 'takes a String or a FieldPath; see the dottedPath and fieldPath shapes', + 'Pipeline.findNearest(distanceResultField)': + 'takes a String or a FieldPath naming the field the distance is written ' + 'to; see the fieldPath shape', + 'PipelineSource.createFrom(query)': 'takes a Query or a VectorQuery', + 'PipelineSource.documents(documents)': + 'takes DocumentReferences and document path Strings', + 'constant(value)': 'wraps a literal value; an expression is not a constant', + 'field(fieldPath)': + 'takes a String or a FieldPath; see the dottedPath and fieldPath shapes', +}; + +/// The imports an E2E file may use besides `dart:` libraries. +const _allowedImports = {_barrelUri, 'package:test/test.dart', 'harness.dart'}; + +void main() { + late final _Analysis analysis; + + setUpAll(() async { + analysis = await _Analysis.run(); + }); + + test('the E2E sources resolve without errors', () { + expect(analysis.diagnostics, isEmpty); + }); + + test('the E2E sources follow the suite conventions', () { + expect(analysis.conventionErrors, isEmpty); + }); + + test('the guard tables match the Pipeline API', () { + expect(analysis.configurationErrors, isEmpty); + }); + + test('every Pipeline API member has live E2E coverage', () { + final report = analysis.coverageReport(); + if (report != null) fail(report); + }); +} + +/// A member of the Pipeline API universe. +final class _Member { + _Member(this.key, this.element); + + /// `Class.member`, `Class.new` for an unnamed constructor, or the bare name + /// of a top-level function. + final String key; + final Element element; + + late final String? family = _familyOf(this); + + late final List parameters = switch (element) { + final ExecutableElement executable => executable.formalParameters, + _ => const [], + }; + + String get name => element.name!; + + String? get className => switch (element.enclosingElement) { + final InterfaceElement owner => owner.name, + _ => null, + }; +} + +/// Returns the family that owns [member], or `null` when no table assigns +/// one. +String? _familyOf(_Member member) { + return _familyByMember[member.key] ?? + _familyByClass[member.className] ?? + _familyByName[member.name]; +} + +/// How rule 3 treats a parameter. +enum _ValueKind { + /// Not a value position. + none, + + /// Typed `Object`, `Object?` or `dynamic`. + value, + + /// Typed `Iterable` (or `List`) of `Object`, `Object?` or `dynamic`. + iterable, +} + +/// What the E2E sources do with one parameter of one member. +final class _ParameterUsage { + _ParameterUsage(this.parameter, this.kind); + + final FormalParameterElement parameter; + final _ValueKind kind; + + bool supplied = false; + bool omitted = false; + bool expression = false; + bool plain = false; + final shapes = <_Shape>{}; + + /// The enum whose values this parameter takes, if it is enum-typed. + late final EnumElement? enumElement = switch (parameter.type) { + InterfaceType(element: final EnumElement element) => element, + _ => null, + }; + + /// The names of the [enumElement] values passed to this parameter. + final enumValues = {}; +} + +/// A coverage gap, attributed to a member's family. +final class _Gap { + _Gap( + this.member, + this.rule, + this.subject, + this.message, { + this.waivable = true, + }); + + final _Member member; + final int rule; + + /// Whether an exemption can waive this gap. A missing export never is. + final bool waivable; + + /// The exemption key this gap would be waived by: the member key, or + /// `Member(param)`. + final String subject; + final String message; + + String get line => '$subject: $message'; +} + +const _ruleTitles = { + 1: 'Rule 1: member coverage', + 2: 'Rule 2: optional parameters', + 3: 'Rule 3: value positions', + 4: 'Rule 4: required shapes', +}; + +final class _Analysis { + _Analysis._(this._packageRoot); + + final String _packageRoot; + + final diagnostics = []; + final conventionErrors = []; + final configurationErrors = []; + + /// The universe, keyed by [_Member.key]. + final members = {}; + + /// Universe members that are not exported from the barrel. + final unexported = <_Member>[]; + + final referenced = {}; + + /// `Member(param)` to its usage. + final usages = {}; + + late final TypeSystem _typeSystem; + late final InterfaceType _expressionType; + late final InterfaceType _fieldPathType; + late final InterfaceType _numType; + + static Future<_Analysis> run() async { + final barrel = await Isolate.resolvePackageUri(Uri.parse(_barrelUri)); + final packageRoot = File.fromUri(barrel!).parent.parent.path; + final analysis = _Analysis._(packageRoot); + await analysis._analyze(barrel.toFilePath()); + return analysis; + } + + String get _e2ePath => '$_packageRoot/$_e2eDirectory'; + + Future _analyze(String barrelPath) async { + final sources = + Directory(_e2ePath) + .listSync(recursive: true) + .whereType() + .map((file) => file.absolute.path) + .where((path) => path.endsWith('.dart')) + .toList() + ..sort(); + + final collection = AnalysisContextCollection( + includedPaths: [barrelPath, ...sources], + ); + final session = collection.contextFor(barrelPath).currentSession; + + final barrel = + await session.getLibraryByUri(_barrelUri) as LibraryElementResult; + final implementation = + await session.getLibraryByUri(_implementationUri) + as LibraryElementResult; + _buildUniverse(barrel.element, implementation.element); + + for (final path in sources) { + final result = await session.getResolvedUnit(path); + if (result is! ResolvedUnitResult) { + diagnostics.add('${_relative(path)}: could not be resolved ($result)'); + continue; + } + _checkUnit(result); + } + + _checkConfiguration(); + } + + // --------------------------------------------------------------------------- + // Universe. + + void _buildUniverse(LibraryElement barrel, LibraryElement implementation) { + _typeSystem = implementation.typeSystem; + _expressionType = implementation.getClass('PipelineExpression')!.thisType; + _fieldPathType = implementation.getClass('FieldPath')!.thisType; + _numType = implementation.typeProvider.numType; + + final exported = barrel.exportNamespace.definedNames2.values.toSet(); + bool isDeclaredInPipeline(Element element) { + return element.firstFragment.libraryFragment?.source.uri.toString() == + _pipelinePart; + } + + void add(Element element, {required bool exportedOwner}) { + final key = _keyOf(element)!; + final member = _Member(key, element); + members[key] = member; + if (!exportedOwner) unexported.add(member); + for (final parameter in member.parameters) { + usages['$key(${parameter.name})'] = _ParameterUsage( + parameter, + _valueKind(parameter.type), + ); + } + } + + for (final function in implementation.topLevelFunctions) { + if (!function.isPublic || !isDeclaredInPipeline(function)) continue; + add(function, exportedOwner: exported.contains(function)); + } + for (final alias in implementation.typeAliases) { + if (alias.isPublic && + isDeclaredInPipeline(alias) && + !exported.contains(alias)) { + unexported.add(_Member(alias.name!, alias)); + } + } + for (final owner in [ + ...implementation.classes, + ...implementation.enums, + ]) { + if (!owner.isPublic || !isDeclaredInPipeline(owner)) continue; + final isExported = exported.contains(owner); + for (final element in _publicMembers(owner)) { + add(element, exportedOwner: isExported); + } + } + for (final MapEntry(key: className, value: names) in _entryPoints.entries) { + final owner = implementation.getClass(className)!; + for (final name in names) { + add(owner.getMethod(name)!, exportedOwner: exported.contains(owner)); + } + } + } + + /// The public members [owner] declares: constructors of instantiable + /// classes, methods and getters (fields included) — or, for an enum, only + /// its values. + Iterable _publicMembers(InterfaceElement owner) sync* { + if (owner is EnumElement) { + yield* owner.fields.where((field) => field.isEnumConstant); + return; + } + if (owner is ClassElement && !owner.isAbstract && !owner.isSealed) { + yield* owner.constructors.where( + (constructor) => + constructor.isPublic && constructor.isOriginDeclaration, + ); + } + yield* owner.methods.where( + (method) => method.isPublic && !_objectMembers.contains(method.name), + ); + yield* owner.fields.where( + (field) => field.isPublic && !_objectMembers.contains(field.name), + ); + } + + _ValueKind _valueKind(DartType type) { + bool isTop(DartType type) => type is DynamicType || type.isDartCoreObject; + if (isTop(type)) return _ValueKind.value; + if (type is InterfaceType && + (type.isDartCoreIterable || type.isDartCoreList) && + isTop(type.typeArguments.single)) { + return _ValueKind.iterable; + } + return _ValueKind.none; + } + + /// The universe key of [element], or `null` when it is not declared in the + /// implementation library. + String? _keyOf(Element element) { + var base = element.baseElement; + if (base is PropertyAccessorElement) base = base.variable; + if (base.library?.uri.toString() != _implementationUri) return null; + return switch (base.enclosingElement) { + final InterfaceElement owner => '${owner.name}.${base.name}', + _ => base.name, + }; + } + + // --------------------------------------------------------------------------- + // E2E sources. + + String _relative(String path) => path.substring(_packageRoot.length + 1); + + void _checkUnit(ResolvedUnitResult result) { + final file = _relative(result.path); + for (final diagnostic in result.diagnostics) { + if (diagnostic.severity != Severity.error) continue; + final location = result.lineInfo.getLocation(diagnostic.offset); + diagnostics.add('$file:${location.lineNumber}: ${diagnostic.message}'); + } + + final unit = result.unit; + final isTest = file.endsWith('_test.dart'); + for (final directive in unit.directives) { + if (directive is! ImportDirective) continue; + final uri = directive.uri.stringValue ?? ''; + if (!uri.startsWith('dart:') && !_allowedImports.contains(uri)) { + conventionErrors.add( + '$file: imports $uri; E2E files may import only the barrel, ' + 'package:test, harness.dart and dart: libraries', + ); + } + } + if (isTest) { + final tagged = unit.directives.whereType().any( + (library) => library.metadata.any( + (annotation) => + annotation.name.name == 'Tags' && + (annotation.arguments?.toSource().contains("'prod'") ?? false), + ), + ); + if (!tagged) { + conventionErrors.add("$file: missing @Tags(['prod']) on `library;`"); + } + } + + final visitor = _ReferenceVisitor(this); + unit.accept(visitor); + if (isTest && !visitor.declaresSuite) { + conventionErrors.add('$file: declares no tests through pipelineE2E'); + } + } + + void _recordReference(Element? element) { + if (element == null) return; + final key = _keyOf(element); + if (key != null && members.containsKey(key)) referenced.add(key); + } + + void _recordInvocation(Element? element, ArgumentList arguments) { + if (element == null) return; + final key = _keyOf(element); + final member = key == null ? null : members[key]; + if (member == null) return; + + final supplied = {}; + for (final argument in arguments.arguments) { + final parameter = argument.correspondingParameter; + final value = _argumentValue(argument, parameter); + final name = parameter?.name; + if (name == null || value == null || value is NullLiteral) continue; + supplied.add(name); + + final subject = '$key($name)'; + final usage = usages[subject]; + if (usage == null) continue; + switch (usage.kind) { + case _ValueKind.none: + break; + case _ValueKind.value: + _classify(value, usage); + case _ValueKind.iterable: + if (_isListOrSetLiteral(value)) { + for (final element in _leaves(value)) { + _classify(element, usage); + } + } + } + for (final shape in _requiredShapes[subject] ?? const <_Shape>[]) { + if (_hasShape(value, shape)) usage.shapes.add(shape); + } + if (usage.enumElement != null) { + if (_enumConstantName(value) case final constant?) { + usage.enumValues.add(constant); + } + } + } + + for (final parameter in member.parameters) { + if (!parameter.isOptional) continue; + final usage = usages['$key(${parameter.name})']!; + if (supplied.contains(parameter.name)) { + usage.supplied = true; + } else { + usage.omitted = true; + } + } + } + + /// The name of the enum value [value] reads, as in `DistanceMeasure.cosine`. + String? _enumConstantName(Expression value) { + var element = switch (value) { + PrefixedIdentifier() => value.identifier.element, + PropertyAccess() => value.propertyName.element, + SimpleIdentifier() => value.element, + _ => null, + }; + if (element is PropertyAccessorElement) element = element.variable; + return element is FieldElement && element.isEnumConstant + ? element.name + : null; + } + + /// The expression passed by [argument]. + /// + /// A named argument is a `NamedExpression` up to analyzer 12 and a + /// `NamedArgument` from 13; neither type is named here so that the guard + /// builds against both. In both, the value is the last expression child. + Expression? _argumentValue( + AstNode argument, + FormalParameterElement? parameter, + ) { + if (parameter != null && parameter.isNamed) { + return argument.childEntities.whereType().lastOrNull; + } + return argument is Expression ? argument : null; + } + + /// Records whether [value] is an expression or a plain value. Arguments + /// whose static type could be either, such as `Object`, count as neither. + void _classify(Expression value, _ParameterUsage usage) { + switch (_expressionness(value)) { + case true: + usage.expression = true; + case false: + usage.plain = true; + case null: + break; + } + } + + /// Whether [value]'s static type is a `PipelineExpression` (`true`), cannot + /// be one (`false`), or might be (`null`). + bool? _expressionness(Expression value) { + final type = value.staticType; + if (type == null || type is InvalidType || type is NeverType) return null; + if (type.isDartCoreNull) return false; + final nonNull = _typeSystem.promoteToNonNull(type); + if (_typeSystem.isSubtypeOf(nonNull, _expressionType)) return true; + if (_typeSystem.isSubtypeOf(_expressionType, nonNull)) return null; + return false; + } + + bool _isSubtypeOf(Expression value, InterfaceType type) { + final staticType = value.staticType; + if (staticType == null || staticType is InvalidType) return false; + if (staticType is NeverType || staticType.isDartCoreNull) return false; + return _typeSystem.isSubtypeOf( + _typeSystem.promoteToNonNull(staticType), + type, + ); + } + + bool _isListOrSetLiteral(Expression value) { + return value is ListLiteral || (value is SetOrMapLiteral && value.isSet); + } + + /// The element and value expressions of a collection literal, looking + /// through `if`, `for`, null-aware elements and spreads of literals. + Iterable _leaves(Expression literal) { + final elements = switch (literal) { + ListLiteral() => literal.elements, + SetOrMapLiteral() => literal.elements, + _ => const [], + }; + return _collectionLeaves(elements); + } + + Iterable _collectionLeaves( + Iterable elements, + ) sync* { + // An if-chain rather than a switch: the set of CollectionElement + // subtypes differs between analyzer versions. + for (final element in elements) { + if (element is Expression) { + yield element; + } else if (element is NullAwareElement) { + yield element.value; + } else if (element is MapLiteralEntry) { + yield element.value; + } else if (element is IfElement) { + yield* _collectionLeaves([element.thenElement, ?element.elseElement]); + } else if (element is ForElement) { + yield* _collectionLeaves([element.body]); + } else if (element is SpreadElement) { + yield* _leaves(element.expression); + } + } + } + + /// Whether [value] is a collection literal holding an expression at any + /// depth. + bool _holdsExpression(Expression value) { + if (value is! ListLiteral && value is! SetOrMapLiteral) return false; + return _leaves( + value, + ).any((leaf) => (_expressionness(leaf) ?? false) || _holdsExpression(leaf)); + } + + bool _hasShape(Expression value, _Shape shape) { + switch (shape) { + case _Shape.collectionWithExpression: + return _holdsExpression(value); + case _Shape.nestedCollectionWithExpression: + return _isListOrSetLiteral(value) && + _leaves(value).any(_holdsExpression); + case _Shape.numericList: + final leaves = value is ListLiteral ? _leaves(value).toList() : null; + return leaves != null && + leaves.isNotEmpty && + leaves.every((leaf) => _isSubtypeOf(leaf, _numType)); + case _Shape.dottedPath: + return value is StringLiteral && + (value.stringValue?.contains('.') ?? false); + case _Shape.fieldPath: + return _isSubtypeOf(value, _fieldPathType) || + (_isListOrSetLiteral(value) && + _leaves( + value, + ).any((leaf) => _isSubtypeOf(leaf, _fieldPathType))); + } + } + + // --------------------------------------------------------------------------- + // Configuration. + + void _checkConfiguration() { + for (final MapEntry(:key, :value) in _families.entries) { + if (!File('$_e2ePath/$value').existsSync()) { + configurationErrors.add( + 'family $key: $_e2eDirectory/$value does not exist', + ); + } + } + for (final member in members.values) { + if (member.family == null) { + configurationErrors.add( + '${member.key}: no family; add it to _familyByName, ' + '_familyByClass or _familyByMember', + ); + } + } + for (final MapEntry(:key, :value) in [ + ..._familyByMember.entries, + ..._familyByClass.entries, + ..._familyByName.entries, + ]) { + if (!_families.containsKey(value)) { + configurationErrors.add( + 'family table entry $key: unknown family $value', + ); + } + } + for (final key in _familyByMember.keys) { + if (!members.containsKey(key)) { + configurationErrors.add('_familyByMember: $key is not an API member'); + } + } + final classes = {for (final member in members.values) member.className}; + for (final key in _familyByClass.keys) { + if (!classes.contains(key)) { + configurationErrors.add('_familyByClass: $key declares no API member'); + } + } + final namesResolvedByName = { + for (final member in members.values) + if (_familyByMember[member.key] == null && + _familyByClass[member.className] == null) + member.name, + }; + for (final key in _familyByName.keys) { + if (!namesResolvedByName.contains(key)) { + configurationErrors.add('_familyByName: no API member named $key'); + } + } + + for (final MapEntry(:key, :value) in _requiredShapes.entries) { + final usage = usages[key]; + if (usage == null) { + configurationErrors.add('_requiredShapes: $key is not a parameter'); + } else if (value.isEmpty) { + configurationErrors.add('_requiredShapes: $key lists no shape'); + } else if (_notExecutable.containsKey(key) || + _notExecutable.containsKey(key.substring(0, key.indexOf('(')))) { + configurationErrors.add( + '_requiredShapes: $key is exempted by _notExecutable', + ); + } + } + + // An exemption is stale when it names nothing, or when the E2E sources + // already cover what it waives. + final unexempted = _gaps( + applyExemptions: false, + ).where((gap) => gap.waivable).toList(); + final waived = {for (final gap in unexempted) gap.subject}; + final waivedMembers = {for (final gap in unexempted) gap.member.key}; + for (final key in _notExecutable.keys) { + if (!members.containsKey(key) && !usages.containsKey(key)) { + configurationErrors.add( + '_notExecutable: $key is not an API member or parameter', + ); + } else if (!waived.contains(key) && + !(members.containsKey(key) && waivedMembers.contains(key))) { + configurationErrors.add( + '_notExecutable: $key has full coverage; remove the exemption', + ); + } + } + final valueGaps = { + for (final gap in unexempted) + if (gap.rule == 3) gap.subject, + }; + for (final key in _notValuePositions.keys) { + final usage = usages[key]; + if (usage == null || usage.kind == _ValueKind.none) { + configurationErrors.add( + '_notValuePositions: $key is not an Object-typed parameter', + ); + } else if (!valueGaps.contains(key)) { + configurationErrors.add( + '_notValuePositions: $key is covered; remove the exemption', + ); + } + } + } + + // --------------------------------------------------------------------------- + // Gaps. + + List<_Gap> _gaps({required bool applyExemptions}) { + final gaps = <_Gap>[]; + bool exempt(String subject, {bool valuePosition = false}) { + if (!applyExemptions) return false; + return _notExecutable.containsKey(subject) || + (valuePosition && _notValuePositions.containsKey(subject)); + } + + for (final member in unexported) { + gaps.add( + _Gap( + member, + 1, + member.key, + 'public in lib/src/pipeline.dart but not exported from ' + 'lib/google_cloud_firestore.dart', + waivable: false, + ), + ); + } + + for (final member in members.values) { + final key = member.key; + if (exempt(key)) continue; + if (!referenced.contains(key)) { + gaps.add(_Gap(member, 1, key, 'not referenced')); + } + for (final parameter in member.parameters) { + final subject = '$key(${parameter.name})'; + final usage = usages[subject]!; + if (exempt(subject)) continue; + if (parameter.isOptional) { + if (!usage.supplied) { + gaps.add(_Gap(member, 2, subject, 'never supplied')); + } + if (!usage.omitted) { + gaps.add(_Gap(member, 2, subject, 'never omitted')); + } + } + if (!exempt(subject, valuePosition: true)) { + switch (usage.kind) { + case _ValueKind.none: + break; + case _ValueKind.value: + if (!usage.expression) { + gaps.add( + _Gap(member, 3, subject, 'never passed a PipelineExpression'), + ); + } + if (!usage.plain) { + gaps.add( + _Gap( + member, + 3, + subject, + 'never passed a plain value (literal, field name, ...)', + ), + ); + } + case _ValueKind.iterable: + if (!usage.expression) { + gaps.add( + _Gap( + member, + 3, + subject, + 'no list-literal element is a PipelineExpression', + ), + ); + } + if (!usage.plain) { + gaps.add( + _Gap( + member, + 3, + subject, + 'no list-literal element is a plain value', + ), + ); + } + } + } + if (usage.enumElement case final enumElement?) { + for (final constant in enumElement.fields) { + if (constant.isEnumConstant && + !usage.enumValues.contains(constant.name)) { + gaps.add( + _Gap( + member, + 4, + subject, + 'never passed ${enumElement.name}.${constant.name}', + ), + ); + } + } + } + for (final shape in _requiredShapes[subject] ?? const <_Shape>[]) { + if (!usage.shapes.contains(shape)) { + gaps.add( + _Gap( + member, + 4, + subject, + 'never passed ${shape.description} (${shape.name})', + ), + ); + } + } + } + } + return gaps; + } + + /// The grouped gap report, or `null` when there are no gaps. + String? coverageReport() { + final gaps = _gaps(applyExemptions: true); + if (gaps.isEmpty) return null; + + final byFamily = >{}; + for (final gap in gaps) { + (byFamily[gap.member.family ?? '(no family)'] ??= []).add(gap); + } + final families = [ + ..._families.keys.where(byFamily.containsKey), + ...byFamily.keys.where((family) => !_families.containsKey(family)), + ]; + + final buffer = StringBuffer() + ..writeln( + 'Pipeline E2E coverage: ${gaps.length} gaps. Add live cases under ' + '$_e2eDirectory/ to the file named for each family; the rules are ' + 'documented at the top of test/pipeline_e2e_coverage_test.dart.', + ) + ..writeln() + ..writeln('Gaps by family:'); + for (final family in families) { + buffer.writeln( + ' ${family.padRight(20)} ${'${byFamily[family]!.length}'.padLeft(4)}' + ' $_e2eDirectory/${_families[family] ?? '?'}', + ); + } + + for (final family in families) { + final familyGaps = byFamily[family]!; + buffer + ..writeln() + ..writeln( + '== $family: $_e2eDirectory/${_families[family] ?? '?'} ' + '(${familyGaps.length}) ==', + ); + for (final rule in _ruleTitles.keys) { + final lines = [ + for (final gap in familyGaps) + if (gap.rule == rule) gap.line, + ]; + if (lines.isEmpty) continue; + buffer.writeln('-- ${_ruleTitles[rule]} --'); + lines.forEach(buffer.writeln); + } + } + return buffer.toString(); + } +} + +/// Records every API reference and invocation in an E2E unit. +final class _ReferenceVisitor extends RecursiveAstVisitor { + _ReferenceVisitor(this._analysis); + + final _Analysis _analysis; + + /// Whether the unit invokes the harness's `pipelineE2E`. + bool declaresSuite = false; + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + _analysis._recordReference(node.element); + super.visitSimpleIdentifier(node); + } + + @override + void visitMethodInvocation(MethodInvocation node) { + final element = node.methodName.element; + if (element is TopLevelFunctionElement && element.name == 'pipelineE2E') { + declaresSuite = true; + } + _analysis._recordInvocation(element, node.argumentList); + super.visitMethodInvocation(node); + } + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) { + final element = node.constructorName.element; + _analysis + .._recordReference(element) + .._recordInvocation(element, node.argumentList); + super.visitInstanceCreationExpression(node); + } +} diff --git a/packages/google_cloud_firestore/test/pipeline_golden_test.dart b/packages/google_cloud_firestore/test/pipeline_golden_test.dart new file mode 100644 index 00000000..73e52443 --- /dev/null +++ b/packages/google_cloud_firestore/test/pipeline_golden_test.dart @@ -0,0 +1,3759 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Compares the Pipeline requests this SDK sends with the ones the Node.js +/// SDK sends for the same Pipelines. +/// +/// The expected requests in `test/fixtures/pipeline_golden/*.json` are +/// generated by the Node.js SDK (see `tool/pipeline_golden/README.md`), never +/// by this package. Every fixture id has a Dart case below that builds the +/// equivalent Pipeline; the request it sends must equal the fixture. +/// +/// When this SDK deliberately sends something else, the id is listed in +/// [_knownDivergences]. Differences that look like bugs, still to be fixed, +/// are listed in [_pendingFixes]. Node APIs with no Dart equivalent are +/// listed in [_dartApiGaps]. All three lists are checked: an entry fails as +/// soon as it no longer describes reality. +library; + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:google_cloud_firestore/src/firestore_http_client.dart'; +import 'package:google_cloud_firestore_v1/firestore.dart' as firestore_v1; +import 'package:google_cloud_firestore_v1/testing.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart' hide greaterThan, lessThan; + +const _projectId = 'test-project'; +const _databaseId = '(default)'; +const _fixtureDir = 'test/fixtures/pipeline_golden'; + +class _MockFirestoreHttpClient extends Mock implements FirestoreHttpClient {} + +/// Where this SDK deliberately sends something other than the Node SDK. +/// +/// [_Divergence.check] asserts the Dart request still has the intended shape. +final _knownDivergences = { + for (final name in [ + 'equalAny', + 'notEqualAny', + 'arrayContainsAll', + 'arrayContainsAny', + ]) + for (final variant in [ + 'static-field-name-list-with-expression', + 'method-list-with-expression', + ]) + 'functions/$name/$variant': const _Divergence( + 'A search-space list holding expressions is sent as an array(...) ' + 'function. Node sends a literal arrayValue holding a field ' + 'reference, which the backend rejects ("Value type is not ' + 'supported: FIELD_REFERENCE_VALUE").', + _searchSpaceIsArrayFunction, + ), + 'stages/unnest/field-special-characters': const _Divergence( + "unnest(field('my tags')) writes each element back to `my tags`. Node " + 'rebuilds the target with field(Field._alias), quoting the already ' + r'quoted name a second time ("`\`my tags\``").', + _unnestTargetIsQuotedOnce, + ), + 'queries/select/special-characters': const _Divergence( + "createFrom(query.select(['first-name', 'last name'])) selects " + "`first-name` and `last name`. Node passes the projection's quoted " + 'paths back through field(), which quotes them again ' + r'("`\`first-name\``").', + _projectionIsQuotedOnce, + ), + for (final (id, stage, argument) in [ + ('select/special-characters', 'select', 0), + ('distinct/special-characters', 'distinct', 0), + ('aggregate/groups-special-characters', 'aggregate', 1), + ]) + 'stages/$id': _Divergence( + "A String selection such as 'first-name' is keyed by its quoted path, " + '`first-name`, like a PipelineField. The backend reads the keys as ' + 'field paths and rejects an unquoted one that is not an identifier: ' + "for select(['last name']) it answers " + r'"Invalid property path \"last name\". Unquoted property paths must ' + r'match regex ([a-zA-Z_][a-zA-Z_0-9]*), and quoted property paths must ' + r'match regex (`(?:[^`\\]|(?:\\.))+`)". Node keys a string by the ' + 'string itself.', + (request) => + _projectionIsQuotedOnce(request, stage: stage, argument: argument), + ), + for (final (name, operand) in [('add', 1), ('multiply', 2)]) + for (final form in ['static', 'method']) + 'functions/$name/$form-variadic': _Divergence( + 'add and multiply send further operands as nested two-operand calls, ' + '$name($name(rating, $operand), x). The backend takes exactly two ' + 'operands: for add(a, b, c, d) it answers "The function ' + "'add[T <: number](x: T, y: T): T' takes [2..2] argument(s), but 4 " + 'were provided." Node sends a single call with every operand from its ' + 'method form, and its top-level functions drop `...others` at runtime, ' + 'only forwarding `second`.', + (request) => _selectsNestedBinary(request, name, operand), + ), + for (final (id, comparisons) in [ + ('start-at-descending', ['less_than']), + ('end-before-descending', ['greater_than']), + ('mixed-directions', ['less_than', 'greater_than']), + ('descending-limit-to-last', ['less_than', 'greater_than']), + ]) + 'queries/cursor/$id': _Divergence( + 'A cursor on a descending ordering keeps the values on the descending ' + 'side of the bound, as the Query does (startAt(4) keeps ratings up to ' + "4). Node's whereConditionsFromCursor ignores the direction and keeps " + 'the ascending side.', + (request) => + _cursorComparisons(request).join(',') == comparisons.join(','), + ), + 'options/index-mode/recommended': const _Divergence( + _indexModeReason, + _sendsNoIndexMode, + ), + 'options/combined/index-mode-and-explain': _Divergence( + _indexModeReason, + (request) => + _sendsNoIndexMode(request) && + _path(request, ['structuredPipeline', 'options', 'explain_options']) != + null, + ), + 'queries/vector/distance-result-field': _Divergence( + _distanceResultFieldReason, + (request) => _findNearestDistanceField(request) == 'distance', + ), + 'queries/vector/distance-threshold': _Divergence( + _distanceThresholdReason, + (request) => _filtersDistance( + request, + comparison: 'less_than_or_equal', + distanceFunction: 'euclidean_distance', + ), + ), + 'queries/vector/distance-threshold-cosine': _Divergence( + _distanceThresholdReason, + (request) => _filtersDistance( + request, + comparison: 'less_than_or_equal', + distanceFunction: 'cosine_distance', + ), + ), + 'queries/vector/distance-threshold-dot-product': _Divergence( + _distanceThresholdReason, + (request) => _filtersDistance( + request, + comparison: 'greater_than_or_equal', + distanceFunction: 'dot_product', + ), + ), + 'queries/vector/distance-threshold-and-result-field': _Divergence( + '$_distanceResultFieldReason $_distanceThresholdReason With a distance ' + 'result field, the filter reads that field.', + (request) => + _findNearestDistanceField(request) == 'distance' && + _filtersDistance( + request, + comparison: 'less_than_or_equal', + distanceField: 'distance', + ), + ), +}; + +const _indexModeReason = + 'The deprecated indexMode is not sent: the backend rejects the ' + 'index_mode option Node sends ("Unsupported option: index_mode"), and ' + "recommended, its only value, is the backend's default."; + +const _distanceResultFieldReason = + "createFrom(vectorQuery) keeps distanceResultField as find_nearest's " + 'distance_field option, so each result carries its distance as it does ' + 'for the VectorQuery. Node drops it.'; + +const _distanceThresholdReason = + 'createFrom(vectorQuery) applies distanceThreshold as a where stage on ' + 'the distance after find_nearest (<= for euclidean and cosine, >= for ' + 'dot product), keeping the VectorQuery results. Node drops the threshold ' + 'and returns documents beyond it. find_nearest takes no threshold option: ' + 'the backend rejects one ("does not support option(s) ' + '[distance_threshold]").'; + +/// Differences from the Node SDK that are not intended, awaiting a fix. +/// +/// Each entry must still differ from its fixture; remove it once fixed. +const _pendingFixes = { + // Ordering helpers. + // Nested plain collections. Node turns a nested list or map into an + // array(...) / map(...) function; Dart sends a nested literal. + 'functions/equalAny/static-field-name-list-nested': + "A nested list in the search space (['Fantasy', ['Sci-Fi']]) is sent " + 'as a literal arrayValue; Node sends an array(...) function inside the ' + 'literal arrayValue (the known-divergence family: prod likely rejects ' + "Node's form).", + 'functions/notEqualAny/static-field-name-list-nested': + 'As equalAny: nested literal arrayValue vs Node array(...) function.', + 'functions/arrayContainsAll/static-field-name-list-nested': + 'As equalAny: nested literal arrayValue vs Node array(...) function.', + 'functions/arrayContainsAny/static-field-name-list-nested': + 'As equalAny: nested literal arrayValue vs Node array(...) function.', +}; + +/// Node APIs this SDK has no way to express; there is no Dart case for them. +const _dartApiGaps = { + 'sources/subcollection/as-array-expression': + 'No subcollection() source and no Pipeline.toArrayExpression().', + 'stages/define/plain': 'No Pipeline.define (let) stage.', + 'stages/delete/plain': 'No Pipeline.delete stage.', + 'stages/update/plain': 'No Pipeline.update stage.', + 'stages/to-scalar-expression/plain': 'No Pipeline.toScalarExpression().', + 'functions/arraySlice/static-field-name-offset': + 'PipelineFunctions.arraySlice requires a length; only the ' + 'PipelineExpression.arraySlice method makes it optional.', +}; + +void main() { + final fixtures = _loadFixtures(); + final registry = _Registry(); + _registerCases(registry); + + group('Node golden corpus', () { + test('every fixture has a Dart case or a documented API gap', () { + final missing = [ + for (final id in fixtures.keys) + if (!registry.cases.containsKey(id) && !_dartApiGaps.containsKey(id)) + id, + ]; + expect( + missing, + isEmpty, + reason: + 'Add a Dart case to _registerCases for each of these fixture ' + 'ids, or list it in _dartApiGaps.', + ); + }); + + test('every Dart case has a fixture', () { + final orphans = [ + for (final id in registry.cases.keys) + if (!fixtures.containsKey(id)) id, + ]; + expect( + orphans, + isEmpty, + reason: + 'Add these cases to tool/pipeline_golden/cases.js and regenerate ' + 'the fixtures (see tool/pipeline_golden/README.md).', + ); + }); + + test('documented API gaps have a fixture and no Dart case', () { + for (final id in _dartApiGaps.keys) { + expect(fixtures, contains(id), reason: '_dartApiGaps lists $id'); + expect( + registry.cases, + isNot(contains(id)), + reason: '$id has a Dart case: remove it from _dartApiGaps.', + ); + } + }); + + test('divergences and pending fixes have a fixture and a Dart case', () { + for (final id in [..._knownDivergences.keys, ..._pendingFixes.keys]) { + expect(fixtures, contains(id)); + expect(registry.cases, contains(id)); + } + expect( + _knownDivergences.keys.toSet().intersection(_pendingFixes.keys.toSet()), + isEmpty, + ); + }); + }); + + group('matches the Node SDK request', () { + late _MockFirestoreHttpClient client; + late Firestore db; + firestore_v1.ExecutePipelineRequest? captured; + + setUp(() { + client = _MockFirestoreHttpClient(); + when(() => client.cachedProjectId).thenReturn(_projectId); + when( + () => client.v1>(any()), + ).thenAnswer((invocation) async { + final callback = + invocation.positionalArguments.single + as Future> + Function(firestore_v1.Firestore api, String projectId); + final api = FakeFirestore( + executePipeline: (request) { + captured = request; + return const Stream.empty(); + }, + ); + return callback(api, _projectId); + }); + db = Firestore.internal( + settings: const Settings( + projectId: _projectId, + databaseId: _databaseId, + ), + client: client, + ); + }); + + Future capture(_Run run) async { + captured = null; + await run(db); + final request = captured; + if (request == null) fail('The case sent no request.'); + return _canonicalize(jsonDecode(jsonEncode(request.toJson()))); + } + + for (final id in fixtures.keys.toList()..sort()) { + final runs = registry.cases[id]; + if (runs == null) continue; + final expected = fixtures[id]; + + test(id, () async { + for (final (index, run) in runs.indexed) { + final actual = await capture(run); + final form = runs.length == 1 ? '' : ' (Dart form #${index + 1})'; + final differences = []; + _diff(expected, actual, r'$', differences); + + final divergence = _knownDivergences[id]; + final pending = _pendingFixes[id]; + if (divergence != null) { + expect( + differences, + isNotEmpty, + reason: + '$id$form now matches Node: remove it from ' + '_knownDivergences.', + ); + expect( + divergence.check(actual), + isTrue, + reason: + '$id$form no longer has the intended shape: ' + '${divergence.reason}\n' + 'Dart sent:\n${_pretty(actual)}', + ); + } else if (pending != null) { + expect( + differences, + isNotEmpty, + reason: + '$id$form now matches Node: remove it from ' + '_pendingFixes.', + ); + } else if (differences.isNotEmpty) { + fail( + '$id$form differs from the Node SDK request:\n' + ' ${differences.join('\n ')}\n\n' + 'Dart sent:\n${_pretty(actual)}\n\n' + 'Node sent:\n${_pretty(expected)}', + ); + } + } + }); + } + }); +} + +// ----------------------------------------------------------------------------- +// Fixtures and comparison +// ----------------------------------------------------------------------------- + +/// Loads every case of every fixture file, keyed by id. +Map _loadFixtures() { + final files = + Directory(_fixtureDir) + .listSync() + .whereType() + .where((file) => file.path.endsWith('.json')) + .toList() + ..sort((a, b) => a.path.compareTo(b.path)); + if (files.isEmpty) { + throw StateError('No fixtures in $_fixtureDir; run from the package root.'); + } + + final result = {}; + for (final file in files) { + final json = jsonDecode(file.readAsStringSync()) as Map; + final cases = json['cases']! as Map; + for (final MapEntry(:key, :value) in cases.entries) { + if (result.containsKey(key)) { + throw StateError('Duplicate fixture id $key in ${file.path}.'); + } + result[key] = _canonicalize(value); + } + } + return result; +} + +/// Repeated and map fields that proto3 JSON omits when empty. +/// +/// The same normalization `tool/pipeline_golden/generate.js` applies. +const _omitWhenEmpty = {'args', 'options', 'fields', 'values'}; + +/// Sorts keys (`name` first, as in the fixtures) and drops [_omitWhenEmpty] +/// fields that are empty. +Object? _canonicalize(Object? value) { + return switch (value) { + List() => [for (final item in value) _canonicalize(item)], + Map() => { + for (final key in value.keys.cast().toList()..sort(_compareKeys)) + if (_canonicalize(value[key]) case final child + when !(_omitWhenEmpty.contains(key) && _isEmpty(child))) + key: child, + }, + _ => value, + }; +} + +int _compareKeys(String a, String b) { + if (a == b) return 0; + if (a == 'name') return -1; + if (b == 'name') return 1; + return a.compareTo(b); +} + +bool _isEmpty(Object? value) { + return switch (value) { + List() => value.isEmpty, + Map() => value.isEmpty, + _ => false, + }; +} + +/// Lists the paths at which [expected] and [actual] differ. +/// +/// Numbers compare by value, so `1` and `1.0` (a `doubleValue` encoded by +/// JavaScript and by Dart) are equal. +void _diff(Object? expected, Object? actual, String path, List out) { + if (expected is Map && actual is Map) { + for (final key in {...expected.keys, ...actual.keys}) { + final child = '$path.$key'; + if (!expected.containsKey(key)) { + out.add('$child: missing in Node, Dart has ${_short(actual[key])}'); + } else if (!actual.containsKey(key)) { + out.add('$child: Node has ${_short(expected[key])}, missing in Dart'); + } else { + _diff(expected[key], actual[key], child, out); + } + } + } else if (expected is List && actual is List) { + if (expected.length != actual.length) { + out.add( + '$path: Node has ${expected.length} items, Dart has ' + '${actual.length}: Node ${_short(expected)}, Dart ${_short(actual)}', + ); + return; + } + for (var i = 0; i < expected.length; i++) { + _diff(expected[i], actual[i], '$path[$i]', out); + } + } else if (expected != actual) { + out.add('$path: Node ${_short(expected)}, Dart ${_short(actual)}'); + } +} + +String _short(Object? value) { + final json = jsonEncode(value); + return json.length <= 200 ? json : '${json.substring(0, 200)}...'; +} + +String _pretty(Object? value) => + const JsonEncoder.withIndent(' ').convert(value); + +/// A deliberate difference from the Node SDK. +final class _Divergence { + const _Divergence(this.reason, this.check); + + /// Why this SDK sends something else. + final String reason; + + /// Whether the canonical Dart request has the intended shape. + final bool Function(Object? request) check; +} + +/// Whether the request sends no `index_mode` execute option. +bool _sendsNoIndexMode(Object? request) { + final options = _path(request, ['structuredPipeline', 'options']); + return options == null || + (options is Map && !options.containsKey('index_mode')); +} + +/// Whether the `unnest` stage writes each element back to the quoted +/// `` `my tags` `` field it reads. +bool _unnestTargetIsQuotedOnce(Object? request) { + final stages = _path(request, ['structuredPipeline', 'pipeline', 'stages']); + if (stages is! List || stages.length != 2) return false; + const target = {'fieldReferenceValue': '`my tags`'}; + return _path(stages[1], ['name']) == 'unnest' && + _jsonEquals(_path(stages[1], ['args']), [target, target]); +} + +/// Whether the [stage] stage's [argument] keys `` `first-name` `` and +/// `` `last name` `` by their singly quoted paths. +bool _projectionIsQuotedOnce( + Object? request, { + String stage = 'select', + int argument = 0, +}) { + final stages = _path(request, ['structuredPipeline', 'pipeline', 'stages']); + if (stages is! List) return false; + final projection = stages.cast().firstWhere( + (candidate) => _path(candidate, ['name']) == stage, + orElse: () => null, + ); + return _jsonEquals( + _path(projection, ['args', argument, 'mapValue', 'fields']), + { + for (final path in ['`first-name`', '`last name`']) + path: {'fieldReferenceValue': path}, + }, + ); +} + +bool _jsonEquals(Object? a, Object? b) => equals(b).matches(a, {}); + +/// Whether the selected expression is the variadic call of `functions/$name` +/// sent as two nested two-operand calls: `name(name(rating, operand), x)`, +/// where `x` is the third operand's field. +bool _selectsNestedBinary(Object? request, String name, int operand) { + final stages = _path(request, ['structuredPipeline', 'pipeline', 'stages']); + if (stages is! List || stages.length != 2) return false; + final outer = _path(stages[1], [ + 'args', + 0, + 'mapValue', + 'fields', + 'result', + 'functionValue', + ]); + final inner = _path(outer, ['args', 0, 'functionValue']); + return _path(outer, ['name']) == name && + _path(outer, ['args']) is List && + (_path(outer, ['args'])! as List).length == 2 && + _path(outer, ['args', 1, 'fieldReferenceValue']) != null && + _jsonEquals(inner, { + 'name': name, + 'args': [ + {'fieldReferenceValue': 'rating'}, + {'integerValue': '$operand'}, + ], + }); +} + +/// Whether the `where` stage's function receives its search space as an +/// `array(...)` function. +bool _searchSpaceIsArrayFunction(Object? request) { + final stages = _path(request, ['structuredPipeline', 'pipeline', 'stages']); + if (stages is! List || stages.length != 2) return false; + final searchSpace = _path(stages[1], ['args', 0, 'functionValue', 'args', 1]); + return _path(searchSpace, ['functionValue', 'name']) == 'array'; +} + +/// The `less_than` and `greater_than` comparisons of the cursor filters, the +/// `where` stages after the first `sort`, in order. +List _cursorComparisons(Object? request) { + final stages = _path(request, ['structuredPipeline', 'pipeline', 'stages']); + if (stages is! List) return const []; + final sort = stages.indexWhere((stage) => _path(stage, ['name']) == 'sort'); + if (sort < 0) return const []; + + final comparisons = []; + void visit(Object? value) { + final function = _path(value, ['functionValue']); + final name = _path(function, ['name']); + if (name == 'less_than' || name == 'greater_than') { + comparisons.add(name! as String); + } + final args = _path(function, ['args']); + if (args is List) args.forEach(visit); + } + + for (final stage in stages.skip(sort + 1)) { + if (_path(stage, ['name']) != 'where') continue; + final args = _path(stage, ['args']); + if (args is List) args.forEach(visit); + } + return comparisons; +} + +/// The field the find_nearest stage writes the distance to, if any. +Object? _findNearestDistanceField(Object? request) { + final stages = _path(request, ['structuredPipeline', 'pipeline', 'stages']); + if (stages is! List) return null; + final findNearest = stages.lastWhere( + (stage) => _path(stage, ['name']) == 'find_nearest', + orElse: () => null, + ); + return _path(findNearest, [ + 'options', + 'distance_field', + 'fieldReferenceValue', + ]); +} + +/// Whether find_nearest, sent without a threshold option, is followed by a +/// `where` stage comparing the distance with the 0.5 threshold. +/// +/// The distance is either [distanceFunction] of the `embedding` field, or +/// [distanceField]. +bool _filtersDistance( + Object? request, { + required String comparison, + String? distanceFunction, + String? distanceField, +}) { + final stages = _path(request, ['structuredPipeline', 'pipeline', 'stages']); + if (stages is! List || stages.length < 2) return false; + final findNearest = stages[stages.length - 2]; + if (_path(findNearest, ['name']) != 'find_nearest' || + _path(findNearest, ['options', 'distance_threshold']) != null) { + return false; + } + + final where = stages.last; + final condition = _path(where, ['args', 0, 'functionValue']); + if (_path(where, ['name']) != 'where' || + _path(condition, ['name']) != comparison || + _path(condition, ['args', 1, 'doubleValue']) != 0.5) { + return false; + } + + final distance = _path(condition, ['args', 0]); + if (distanceField != null) { + return _path(distance, ['fieldReferenceValue']) == distanceField; + } + return _path(distance, ['functionValue', 'name']) == distanceFunction && + _path(distance, ['functionValue', 'args', 0, 'fieldReferenceValue']) == + 'embedding'; +} + +Object? _path(Object? value, List path) { + var current = value; + for (final segment in path) { + current = switch ((current, segment)) { + (final Map map, final String key) => map[key], + (final List list, final int index) when index < list.length => + list[index], + _ => null, + }; + } + return current; +} + +// ----------------------------------------------------------------------------- +// Case registry +// ----------------------------------------------------------------------------- + +/// Executes a Pipeline against [db]; the request it sends is captured. +typedef _Run = Future Function(Firestore db); + +typedef _Build = Pipeline Function(Firestore db); +typedef _Expr = PipelineExpression Function(); +typedef _DbExpr = PipelineExpression Function(Firestore db); +typedef _Static1 = PipelineExpression Function(Object? target); +typedef _Method0 = PipelineExpression Function(PipelineExpression target); +typedef _Static2 = PipelineExpression Function(Object? target, Object? operand); +typedef _Method1 = + PipelineExpression Function(PipelineExpression target, Object? operand); + +/// The Dart cases, keyed by fixture id. +/// +/// A case may have several Dart forms (for example a top-level helper and +/// the matching [PipelineFunctions] member); each one must match. +final class _Registry { + final cases = >{}; + + void run(String id, List<_Run> runs) { + if (cases.containsKey(id)) throw StateError('Duplicate case id $id.'); + cases[id] = runs; + } + + /// A Pipeline executed without options. + void pipeline( + String id, + _Build build, [ + List<_Build> alternatives = const [], + ]) { + run(id, [ + for (final b in [build, ...alternatives]) (db) => b(db).execute(), + ]); + } + + /// A function wrapped as in `cases.js`: boolean expressions in `where`, + /// everything else in `select(.as('result'))`. + void expr(String id, _Expr build, [List<_Expr> alternatives = const []]) { + pipeline(id, (db) => _wrap(db, build()), [ + for (final b in alternatives) (db) => _wrap(db, b()), + ]); + } + + /// A function that needs the Firestore instance, such as for a reference. + void dbExpr(String id, _DbExpr build) { + pipeline(id, (db) => _wrap(db, build(db))); + } + + /// An aggregate function, in `aggregate(.as('result'))`. + void aggregate( + String id, + _Expr build, [ + List<_Expr> alternatives = const [], + ]) { + pipeline(id, (db) => _books(db).aggregate([build().as('result')]), [ + for (final b in alternatives) + (db) => _books(db).aggregate([b().as('result')]), + ]); + } + + /// Mirrors `unary` in `cases.js`. + void unary( + String name, + String target, + _Static1 static, + _Method0 method, { + String area = 'functions', + }) { + final register = area == 'aggregates' ? aggregate : expr; + register('$area/$name/static-field-name', () => static(target), [ + () => static(_fieldPathOf(target)), + ]); + register('$area/$name/static-expression', () => static(field(target))); + register('$area/$name/method', () => method(field(target))); + } + + /// Mirrors `binary` in `cases.js`. + void binary( + String name, + String target, + Object? literal, + _Expr operand, + _Static2 static, + _Method1 method, { + List<_Static2> staticAlternatives = const [], + List<_Method1> methodAlternatives = const [], + }) { + final statics = [static, ...staticAlternatives]; + final methods = [method, ...methodAlternatives]; + void staticCase( + String variant, + Object? Function() target, + Object? Function() op, + ) { + expr('functions/$name/static-$variant', () => static(target(), op()), [ + for (final s in statics.skip(1)) () => s(target(), op()), + // A FieldPath names the target field as its String does. + if (target() case final String name) + for (final s in statics) () => s(_fieldPathOf(name), op()), + ]); + } + + staticCase('field-name-literal', () => target, () => literal); + staticCase('field-name-expression', () => target, operand); + staticCase('expression-literal', () => field(target), () => literal); + staticCase('expression-expression', () => field(target), operand); + expr( + 'functions/$name/method-literal', + () => method(field(target), literal), + [for (final m in methods.skip(1)) () => m(field(target), literal)], + ); + expr( + 'functions/$name/method-expression', + () => method(field(target), operand()), + ); + } +} + +Pipeline _books(Firestore db) => db.pipeline().collection('books'); + +/// The [FieldPath] naming the same field as the dotted String [name]. +FieldPath _fieldPathOf(String name) => FieldPath(name.split('.')); + +Pipeline _wrap(Firestore db, PipelineExpression expression) { + return switch (expression) { + PipelineBooleanExpression() => _books(db).where(expression), + _ => _books(db).select([expression.as('result')]), + }; +} + +// Fixed values, identical to the ones in `cases.js`. +final _timestamp = Timestamp(seconds: 1700000000, nanoseconds: 123456789); +final _date = DateTime.utc(2024, 1, 2, 3, 4, 5, 678); +final _geoPoint = GeoPoint(latitude: 37.7749, longitude: -122.4194); +final _bytes = Uint8List.fromList([1, 2, 3, 255]); +final _vector = FieldValue.vector([0.1, 0.2, 0.3]); +DocumentReference _docRef(Firestore db) => db.doc('books/book1'); + +PipelineBooleanExpression _isFantasy() => field('genre').equal('Fantasy'); +PipelineBooleanExpression _isGood() => field('rating').greaterThan(4); +PipelineBooleanExpression _isOld() => field('published').lessThan(1950); + +void _registerCases(_Registry r) { + _registerSources(r); + _registerQueries(r); + _registerStages(r); + _registerOptions(r); + _registerValues(r); + _registerArithmetic(r); + _registerComparisonAndLogic(r); + _registerStrings(r); + _registerArrays(r); + _registerMaps(r); + _registerTimestamps(r); + _registerTypesVectorsAndReferences(r); + _registerAggregates(r); +} + +// ----------------------------------------------------------------------------- +// Sources and queries +// ----------------------------------------------------------------------------- + +void _registerSources(_Registry r) { + r.pipeline( + 'sources/collection/path', + (db) => db.pipeline().collection('books'), + ); + r.pipeline( + 'sources/collection/nested-path', + (db) => db.pipeline().collection('authors/author1/books'), + ); + r.pipeline( + 'sources/collection/reference', + (db) => db.pipeline().collectionReference(db.collection('books')), + ); + r.pipeline( + 'sources/collection-group/id', + (db) => db.pipeline().collectionGroup('reviews'), + ); + // Dart has no typed forceIndex (a pending fix in the API parity test), but + // rawOptions sends the same option. + r.pipeline( + 'sources/collection/force-index', + (db) => db.pipeline().collection( + 'books', + rawOptions: const {'force_index': 'my_index'}, + ), + ); + r.pipeline( + 'sources/collection/raw-options', + (db) => db.pipeline().collection( + 'books', + rawOptions: const {'foo': 'bar', 'outer.inner': 1}, + ), + ); + r.pipeline( + 'sources/collection/reference-raw-options', + (db) => db.pipeline().collectionReference( + db.collection('books'), + rawOptions: const {'foo': 'bar'}, + ), + ); + r.pipeline( + 'sources/collection-group/force-index', + (db) => db.pipeline().collectionGroup( + 'reviews', + rawOptions: const {'force_index': 'my_index'}, + ), + ); + r.pipeline( + 'sources/collection-group/raw-options', + (db) => db.pipeline().collectionGroup( + 'reviews', + rawOptions: const {'foo': 'bar'}, + ), + ); + r.pipeline('sources/database/plain', (db) => db.pipeline().database()); + r.pipeline( + 'sources/database/raw-options', + (db) => db.pipeline().database(rawOptions: const {'foo': 'bar'}), + ); + r.pipeline( + 'sources/documents/paths', + (db) => db.pipeline().documents(['books/book1', 'books/book2']), + ); + r.pipeline( + 'sources/documents/references', + (db) => + db.pipeline().documents([db.doc('books/book1'), db.doc('books/book2')]), + ); + r.pipeline( + 'sources/documents/nested', + (db) => db.pipeline().documents([db.doc('authors/author1/books/book1')]), + ); + r.pipeline( + 'sources/documents/raw-options', + (db) => db.pipeline().documents( + [db.doc('books/book1')], + rawOptions: const {'foo': 'bar'}, + ), + ); + r.pipeline( + 'sources/documents/nested-path', + (db) => db.pipeline().documents(['authors/author1/books/book1']), + ); + r.pipeline( + 'sources/documents/leading-slash-path', + (db) => db.pipeline().documents(['/books/book1']), + ); + r.pipeline( + 'sources/documents/paths-and-references', + (db) => db.pipeline().documents(['books/book1', db.doc('books/book2')]), + ); +} + +void _registerQueries(_Registry r) { + void query(String id, Object Function(Firestore db) build) { + r.pipeline('queries/$id', (db) => db.pipeline().createFrom(build(db))); + } + + CollectionReference col(Firestore db) => db.collection('books'); + + query('collection/plain', col); + query( + 'collection/nested-path', + (db) => db.collection('authors/author1/books'), + ); + query('collection-group/plain', (db) => db.collectionGroup('reviews')); + query( + 'collection-group/where', + (db) => db + .collectionGroup('reviews') + .where('rating', WhereFilter.greaterThan, 3), + ); + query( + 'collection-group/not-equal', + (db) => + db.collectionGroup('reviews').where('rating', WhereFilter.notEqual, 3), + ); + + query( + 'where/less-than', + (db) => col(db).where('rating', WhereFilter.lessThan, 4), + ); + query( + 'where/less-than-or-equal', + (db) => col(db).where('rating', WhereFilter.lessThanOrEqual, 4), + ); + query( + 'where/equal', + (db) => col(db).where('genre', WhereFilter.equal, 'Fantasy'), + ); + query( + 'where/not-equal', + (db) => col(db).where('genre', WhereFilter.notEqual, 'Fantasy'), + ); + query( + 'where/greater-than-or-equal', + (db) => col(db).where('rating', WhereFilter.greaterThanOrEqual, 4), + ); + query( + 'where/greater-than', + (db) => col(db).where('rating', WhereFilter.greaterThan, 4), + ); + query( + 'where/array-contains', + (db) => col(db).where('tags', WhereFilter.arrayContains, 'magic'), + ); + query( + 'where/in', + (db) => col(db).where('genre', WhereFilter.isIn, ['Fantasy', 'Sci-Fi']), + ); + query( + 'where/not-in', + (db) => col(db).where('genre', WhereFilter.notIn, ['Fantasy', 'Sci-Fi']), + ); + query( + 'where/array-contains-any', + (db) => + col(db).where('tags', WhereFilter.arrayContainsAny, ['magic', 'epic']), + ); + query( + 'where/equal-null', + (db) => col(db).where('genre', WhereFilter.equal, null), + ); + query( + 'where/not-equal-null', + (db) => col(db).where('genre', WhereFilter.notEqual, null), + ); + query( + 'where/equal-nan', + (db) => col(db).where('rating', WhereFilter.equal, double.nan), + ); + query( + 'where/double', + (db) => col(db).where('rating', WhereFilter.greaterThan, 4.5), + ); + query( + 'where/timestamp', + (db) => col(db).where('published', WhereFilter.lessThan, _timestamp), + ); + query( + 'where/nested-field', + (db) => col(db).where('metadata.lang', WhereFilter.equal, 'en'), + ); + query( + 'where/special-characters', + (db) => col(db).where('first-name', WhereFilter.equal, 'Ada'), + ); + query( + 'where/field-path-object', + (db) => col(db).where(FieldPath(const ['a.b']), WhereFilter.equal, 1), + ); + query( + 'where/document-id', + // Node also accepts the bare ID 'book1'; Dart's Query.where requires the + // reference it resolves to. + (db) => col( + db, + ).where(FieldPath.documentId, WhereFilter.equal, col(db).doc('book1')), + ); + query( + 'where/multiple', + (db) => col(db) + .where('genre', WhereFilter.equal, 'Fantasy') + .where('rating', WhereFilter.greaterThan, 4), + ); + query( + 'where/composite-and', + (db) => col(db).whereFilter( + Filter.and([ + Filter.where('genre', WhereFilter.equal, 'Fantasy'), + Filter.where('rating', WhereFilter.greaterThan, 4), + ]), + ), + ); + query( + 'where/composite-or', + (db) => col(db).whereFilter( + Filter.or([ + Filter.where('genre', WhereFilter.equal, 'Fantasy'), + Filter.where('rating', WhereFilter.greaterThan, 4), + ]), + ), + ); + query( + 'where/composite-nested', + (db) => col(db).whereFilter( + Filter.or([ + Filter.and([ + Filter.where('genre', WhereFilter.equal, 'Fantasy'), + Filter.where('rating', WhereFilter.greaterThan, 4), + ]), + Filter.where('published', WhereFilter.lessThan, 1950), + ]), + ), + ); + + query('order-by/ascending', (db) => col(db).orderBy('rating')); + query( + 'order-by/descending', + (db) => col(db).orderBy('rating', descending: true), + ); + query( + 'order-by/multiple', + (db) => col(db).orderBy('rating', descending: true).orderBy('title'), + ); + query('order-by/document-id', (db) => col(db).orderBy(FieldPath.documentId)); + query('order-by/special-characters', (db) => col(db).orderBy('first-name')); + query( + 'order-by/field-path-object', + (db) => col(db).orderBy(FieldPath(const ['a.b'])), + ); + query( + 'order-by/implicit-from-inequality', + (db) => col(db).where('rating', WhereFilter.greaterThan, 4), + ); + query( + 'order-by/implicit-from-inequalities', + (db) => col(db) + .where('rating', WhereFilter.greaterThan, 4) + .where('published', WhereFilter.lessThan, 2000), + ); + query( + 'order-by/explicit-and-inequality', + (db) => col(db) + .where('rating', WhereFilter.greaterThan, 4) + .orderBy('title', descending: true), + ); + query( + 'order-by/implicit-from-mixed-inequalities', + (db) => col(db) + .where('rating', WhereFilter.greaterThan, 4) + .where('genre', WhereFilter.notEqual, 'Horror'), + ); + query( + 'order-by/implicit-from-repeated-field', + (db) => col(db) + .where('rating', WhereFilter.greaterThan, 2) + .where('rating', WhereFilter.lessThan, 5), + ); + query( + 'order-by/implicit-from-composite-or', + (db) => col(db).whereFilter( + Filter.or([ + Filter.where('rating', WhereFilter.greaterThan, 4), + Filter.where('genre', WhereFilter.notIn, ['Horror', 'Romance']), + ]), + ), + ); + query( + 'order-by/implicit-field-path-order', + (db) => col(db) + .where('price-tier', WhereFilter.lessThan, 3) + .where('price.amount', WhereFilter.greaterThan, 1), + ); + query( + 'order-by/implicit-quoted-segments', + (db) => col(db) + .where('field', WhereFilter.greaterThanOrEqual, 'field 100') + .where(FieldPath(const ['field.dot']), WhereFilter.notEqual, 300) + .where(r'field\slash', WhereFilter.lessThan, 400) + .orderBy('name', descending: true), + ); + query( + 'order-by/not-equal-explicitly-ordered', + (db) => col(db) + .where('genre', WhereFilter.notEqual, 'Horror') + .orderBy('genre', descending: true), + ); + query( + 'order-by/not-in-and-descending', + (db) => col(db) + .where('genre', WhereFilter.notIn, ['Horror', 'Romance']) + .orderBy('rating', descending: true), + ); + query( + 'order-by/document-id-and-inequality', + (db) => col(db) + .where( + FieldPath.documentId, + WhereFilter.greaterThan, + col(db).doc('book1'), + ) + .where('rating', WhereFilter.lessThan, 5), + ); + + query('limit/with-order-by', (db) => col(db).orderBy('rating').limit(5)); + query('limit/without-order-by', (db) => col(db).limit(5)); + query( + 'limit-to-last/plain', + (db) => col(db).orderBy('rating').limitToLast(5), + ); + query( + 'limit-to-last/with-inequality', + (db) => col(db) + .where('genre', WhereFilter.notEqual, 'Horror') + .orderBy('rating') + .limitToLast(2), + ); + query('offset/plain', (db) => col(db).offset(10)); + query( + 'offset/with-limit', + (db) => col(db).orderBy('rating').limit(5).offset(10), + ); + + query('cursor/start-at', (db) => col(db).orderBy('rating').startAt([4])); + query( + 'cursor/start-after', + (db) => col(db).orderBy('rating').startAfter([4]), + ); + query('cursor/end-at', (db) => col(db).orderBy('rating').endAt([4])); + query('cursor/end-before', (db) => col(db).orderBy('rating').endBefore([4])); + query( + 'cursor/multiple-fields', + (db) => col(db).orderBy('rating').orderBy('title').startAt([4, 'M']), + ); + query( + 'cursor/start-and-end', + (db) => col(db).orderBy('rating').startAfter([2]).endAt([4]), + ); + query( + 'cursor/limit-to-last', + (db) => + col(db).orderBy('rating').startAt([2]).endBefore([5]).limitToLast(3), + ); + query( + 'cursor/start-at-descending', + (db) => col(db).orderBy('rating', descending: true).startAt([4]), + ); + query( + 'cursor/end-before-descending', + (db) => col(db).orderBy('rating', descending: true).endBefore([2]), + ); + query( + 'cursor/mixed-directions', + (db) => col( + db, + ).orderBy('rating', descending: true).orderBy('title').startAfter([4, 'M']), + ); + query( + 'cursor/descending-limit-to-last', + (db) => col(db) + .orderBy('rating', descending: true) + .startAt([4]) + .endAt([1]) + .limitToLast(2), + ); + query( + 'cursor/with-inequality', + (db) => col(db) + .where('genre', WhereFilter.notEqual, 'Horror') + .orderBy('rating') + .startAfter([4]), + ); + query( + 'cursor/with-inequality-and-limit-to-last', + (db) => col(db) + .where('genre', WhereFilter.notIn, ['Horror']) + .orderBy('rating') + .startAfter([2]) + .endAt([4]) + .limitToLast(3), + ); + + query( + 'select/fields', + (db) => col(db).select([ + FieldPath(const ['title']), + FieldPath(const ['author']), + ]), + ); + query('select/empty', (db) => col(db).select()); + query( + 'select/special-characters', + (db) => col(db).select([ + FieldPath(const ['first-name']), + FieldPath(const ['last name']), + ]), + ); + + VectorQuery nearest( + Query base, { + Object vectorField = 'embedding', + Object queryVector = const [0.1, 0.2, 0.3], + DistanceMeasure distanceMeasure = DistanceMeasure.euclidean, + String? distanceResultField, + double? distanceThreshold, + }) { + return base.findNearest( + vectorField: vectorField, + queryVector: queryVector, + limit: 5, + distanceMeasure: distanceMeasure, + distanceResultField: distanceResultField, + distanceThreshold: distanceThreshold, + ); + } + + query('vector/euclidean', (db) => nearest(col(db))); + query( + 'vector/cosine', + (db) => nearest(col(db), distanceMeasure: DistanceMeasure.cosine), + ); + query( + 'vector/dot-product', + (db) => nearest(col(db), distanceMeasure: DistanceMeasure.dotProduct), + ); + query('vector/vector-value', (db) => nearest(col(db), queryVector: _vector)); + query( + 'vector/with-prefilter', + (db) => nearest(col(db).where('genre', WhereFilter.equal, 'Fantasy')), + ); + query( + 'vector/distance-result-field', + (db) => nearest(col(db), distanceResultField: 'distance'), + ); + query( + 'vector/distance-threshold', + (db) => nearest(col(db), distanceThreshold: 0.5), + ); + query( + 'vector/special-characters', + (db) => nearest(col(db), vectorField: 'my embedding'), + ); + query( + 'vector/field-path-object', + (db) => nearest(col(db), vectorField: FieldPath(const ['a.b'])), + ); + query( + 'vector/distance-threshold-cosine', + (db) => nearest( + col(db), + distanceMeasure: DistanceMeasure.cosine, + distanceThreshold: 0.5, + ), + ); + query( + 'vector/distance-threshold-dot-product', + (db) => nearest( + col(db), + distanceMeasure: DistanceMeasure.dotProduct, + distanceThreshold: 0.5, + ), + ); + query( + 'vector/distance-threshold-and-result-field', + (db) => nearest( + col(db), + distanceThreshold: 0.5, + distanceResultField: 'distance', + ), + ); + query( + 'vector/with-inequality-prefilter', + (db) => nearest(col(db).where('genre', WhereFilter.notEqual, 'Horror')), + ); +} + +// ----------------------------------------------------------------------------- +// Stages and execute options +// ----------------------------------------------------------------------------- + +void _registerStages(_Registry r) { + void stage(String id, _Build build, [List<_Build> alternatives = const []]) { + r.pipeline('stages/$id', build, alternatives); + } + + stage( + 'where/condition', + (db) => _books(db).where(field('rating').greaterThan(4)), + ); + stage( + 'where/chained', + (db) => _books(db) + .where(field('rating').greaterThan(4)) + .where(field('genre').equal('Fantasy')), + ); + stage( + 'where/raw-options', + (db) => _books(db).where( + field('rating').greaterThan(4), + rawOptions: const {'foo': 'bar', 'outer.inner': true}, + ), + ); + + stage('select/field-names', (db) => _books(db).select(['title', 'author'])); + stage( + 'select/fields', + (db) => _books(db).select([field('title'), field('author')]), + ); + stage( + 'select/aliased', + (db) => _books(db).select([ + field('title').as('name'), + field('rating').multiply(2).as('doubled'), + ]), + ); + stage( + 'select/mixed', + (db) => _books( + db, + ).select(['title', field('author'), field('rating').as('score')]), + ); + stage( + 'select/nested-field-path', + (db) => _books(db).select(['metadata.lang', field('awards.hugo')]), + [ + (db) => _books(db).select([ + FieldPath(const ['metadata', 'lang']), + field('awards.hugo'), + ]), + ], + ); + stage( + 'select/special-characters', + (db) => _books(db).select(['first-name', field('last name')]), + [ + (db) => _books(db).select([ + FieldPath(const ['first-name']), + FieldPath(const ['last name']), + ]), + ], + ); + stage( + 'select/options-object', + (db) => _books(db).select(['title', field('rating').as('score')]), + ); + stage( + 'select/raw-options', + (db) => _books(db).select(['title'], rawOptions: const {'foo': 'bar'}), + ); + + stage( + 'add-fields/single', + (db) => _books(db).addFields([field('rating').as('copiedRating')]), + ); + stage( + 'add-fields/multiple', + (db) => _books(db).addFields([ + field('rating').as('copiedRating'), + constant(true).as('annotated'), + ]), + ); + // Dart's addFields only takes aliased expressions; a field aliased to its + // own name is what Node sends for a bare field. + stage( + 'add-fields/field', + (db) => _books(db).addFields([field('rating').as('rating')]), + ); + stage( + 'add-fields/raw-options', + (db) => _books(db).addFields( + [field('rating').as('copiedRating')], + rawOptions: const {'foo': 'bar'}, + ), + ); + + stage( + 'remove-fields/field-names', + (db) => _books(db).removeFields(['title', 'author']), + ); + stage( + 'remove-fields/fields', + (db) => _books(db).removeFields([field('title'), field('author')]), + ); + stage( + 'remove-fields/nested', + (db) => _books(db).removeFields(['metadata.lang']), + [ + (db) => _books(db).removeFields([ + FieldPath(const ['metadata', 'lang']), + ]), + ], + ); + stage( + 'remove-fields/raw-options', + (db) => + _books(db).removeFields(['title'], rawOptions: const {'foo': 'bar'}), + ); + stage( + 'remove-fields/special-characters', + (db) => _books(db).removeFields(['first-name', field('last name')]), + [ + (db) => _books(db).removeFields([ + FieldPath(const ['first-name']), + FieldPath(const ['last name']), + ]), + ], + ); + + stage( + 'sort/ascending-method', + (db) => _books(db).sort([field('rating').ascending()]), + ); + stage( + 'sort/descending-method', + (db) => _books(db).sort([field('rating').descending()]), + ); + stage( + 'sort/multiple', + (db) => _books( + db, + ).sort([field('rating').descending(), field('title').ascending()]), + ); + stage( + 'sort/top-level-field-name', + (db) => _books(db).sort([ascending('rating'), descending('title')]), + ); + stage( + 'sort/top-level-expression', + (db) => _books(db).sort([ + ascending(field('rating')), + descending(field('title').charLength()), + ]), + ); + stage( + 'sort/expression-method', + (db) => _books(db).sort([field('title').charLength().descending()]), + ); + stage( + 'sort/raw-options', + (db) => _books( + db, + ).sort([field('rating').descending()], rawOptions: const {'foo': 'bar'}), + ); + stage( + 'sort/special-characters', + (db) => _books( + db, + ).sort([ascending('first-name'), field('last name').descending()]), + [ + (db) => _books(db).sort([ + ascending(FieldPath(const ['first-name'])), + descending(FieldPath(const ['last name'])), + ]), + ], + ); + + stage('offset/plain', (db) => _books(db).offset(10)); + stage( + 'offset/raw-options', + (db) => _books(db).offset(10, rawOptions: const {'foo': 'bar'}), + ); + stage('limit/plain', (db) => _books(db).limit(10)); + stage('limit/zero', (db) => _books(db).limit(0)); + stage( + 'limit/raw-options', + (db) => _books(db).limit(10, rawOptions: const {'foo': 'bar'}), + ); + + stage( + 'distinct/field-names', + (db) => _books(db).distinct(['genre', 'author']), + ); + stage('distinct/fields', (db) => _books(db).distinct([field('genre')])); + stage( + 'distinct/aliased', + (db) => + _books(db).distinct([field('genre').toLowerCase().as('lowerGenre')]), + ); + stage( + 'distinct/raw-options', + (db) => _books(db).distinct(['genre'], rawOptions: const {'foo': 'bar'}), + ); + stage( + 'distinct/special-characters', + (db) => _books(db).distinct(['first-name', field('last name')]), + [ + (db) => _books(db).distinct([ + FieldPath(const ['first-name']), + FieldPath(const ['last name']), + ]), + ], + ); + + stage( + 'aggregate/single', + (db) => _books(db).aggregate([PipelineFunctions.countAll().as('total')]), + ); + stage( + 'aggregate/multiple', + (db) => _books(db).aggregate([ + PipelineFunctions.countAll().as('total'), + PipelineFunctions.average('rating').as('averageRating'), + ]), + ); + stage( + 'aggregate/groups-field-names', + (db) => _books( + db, + ).aggregate([PipelineFunctions.countAll().as('total')], groups: ['genre']), + ); + stage( + 'aggregate/groups-mixed', + (db) => _books(db).aggregate( + [ + PipelineFunctions.countAll().as('total'), + PipelineFunctions.maximum('rating').as('best'), + ], + groups: [ + 'genre', + field('author'), + field('published').divide(10).floor().as('decade'), + ], + ), + ); + stage( + 'aggregate/raw-options', + (db) => _books(db).aggregate( + [PipelineFunctions.countAll().as('total')], + groups: ['genre'], + rawOptions: const {'foo': 'bar'}, + ), + ); + stage( + 'aggregate/groups-special-characters', + (db) => _books(db).aggregate( + [PipelineFunctions.countAll().as('total')], + groups: ['first-name', field('last name')], + ), + [ + (db) => _books(db).aggregate( + [PipelineFunctions.countAll().as('total')], + groups: [ + FieldPath(const ['first-name']), + FieldPath(const ['last name']), + ], + ), + ], + ); + + Pipeline findNearest( + Firestore db, { + Object vectorField = 'embedding', + Object queryVector = const [0.1, 0.2, 0.3], + DistanceMeasure distanceMeasure = DistanceMeasure.euclidean, + int? limit, + Object? distanceResultField, + Map rawOptions = const {}, + }) { + return _books(db).findNearest( + vectorField: vectorField, + queryVector: queryVector, + distanceMeasure: distanceMeasure, + limit: limit, + distanceResultField: distanceResultField, + rawOptions: rawOptions, + ); + } + + stage('find-nearest/number-array', findNearest); + stage( + 'find-nearest/integer-array', + (db) => findNearest(db, queryVector: const [1, 2, 3]), + ); + stage( + 'find-nearest/vector-value', + (db) => findNearest(db, queryVector: _vector), + ); + stage( + 'find-nearest/field', + (db) => findNearest(db, vectorField: field('embedding')), + ); + stage( + 'find-nearest/cosine', + (db) => findNearest(db, distanceMeasure: DistanceMeasure.cosine), + ); + stage( + 'find-nearest/dot-product', + (db) => findNearest(db, distanceMeasure: DistanceMeasure.dotProduct), + ); + stage('find-nearest/limit', (db) => findNearest(db, limit: 10)); + stage( + 'find-nearest/distance-field', + (db) => findNearest(db, distanceResultField: 'distance'), + ); + stage( + 'find-nearest/all-options', + (db) => findNearest(db, limit: 10, distanceResultField: 'distance'), + ); + stage( + 'find-nearest/raw-options', + (db) => findNearest( + db, + limit: 10, + distanceResultField: 'distance', + rawOptions: const {'limit': 20, 'extra.flag': true}, + ), + ); + stage( + 'find-nearest/special-characters', + (db) => findNearest( + db, + vectorField: 'my embedding', + distanceResultField: 'my distance', + ), + [ + (db) => findNearest( + db, + vectorField: FieldPath(const ['my embedding']), + distanceResultField: FieldPath(const ['my distance']), + ), + ], + ); + + stage('replace-with/field-name', (db) => _books(db).replaceWith('metadata'), [ + (db) => _books(db).replaceWith(FieldPath(const ['metadata'])), + ]); + stage( + 'replace-with/field', + (db) => _books(db).replaceWith(field('metadata')), + ); + stage( + 'replace-with/map-expression', + (db) => _books(db).replaceWith( + PipelineFunctions.map({ + 'title': field('title'), + 'rating': field('rating'), + }), + ), + ); + stage( + 'replace-with/raw-options', + (db) => + _books(db).replaceWith('metadata', rawOptions: const {'foo': 'bar'}), + ); + + stage('sample/documents', (db) => _books(db).sample(documents: 10)); + stage('sample/documents-option', (db) => _books(db).sample(documents: 10)); + stage('sample/percentage', (db) => _books(db).sample(percentage: 0.25)); + stage( + 'sample/raw-options', + (db) => _books(db).sample(documents: 10, rawOptions: const {'foo': 'bar'}), + ); + + stage( + 'union/plain', + (db) => _books(db).union(db.pipeline().collection('magazines')), + ); + stage( + 'union/complex', + (db) => _books(db) + .select(['title']) + .union( + db + .pipeline() + .collectionGroup('reviews') + .where(field('rating').greaterThan(3)) + .select(['title']), + ), + ); + stage( + 'union/raw-options', + (db) => _books(db).union( + db.pipeline().collection('magazines'), + rawOptions: const {'foo': 'bar'}, + ), + ); + + stage('unnest/field', (db) => _books(db).unnest(field('tags')), [ + (db) => _books(db).unnest('tags'), + ]); + stage( + 'unnest/aliased-field', + (db) => _books(db).unnest(field('tags').as('tag')), + ); + stage( + 'unnest/index-field', + (db) => _books(db).unnest(field('tags').as('tag'), indexField: 'index'), + ); + stage( + 'unnest/expression', + (db) => _books( + db, + ).unnest(PipelineFunctions.array([1, 2, field('rating')]).as('item')), + ); + stage( + 'unnest/options-object', + (db) => _books(db).unnest(field('tags').as('tag'), indexField: 'idx'), + ); + stage( + 'unnest/raw-options', + (db) => _books(db).unnest( + field('tags').as('tag'), + indexField: 'idx', + rawOptions: {'index_field': field('position'), 'foo': 'bar'}, + ), + ); + stage( + 'unnest/special-characters', + (db) => + _books(db).unnest(field('tags').as('my tag'), indexField: 'tag index'), + ); + stage( + 'unnest/field-special-characters', + (db) => _books(db).unnest(field('my tags')), + [ + (db) => _books(db).unnest('my tags'), + (db) => _books(db).unnest(FieldPath(const ['my tags'])), + ], + ); + + stage( + 'raw-stage/expression-params', + (db) => _books(db).rawStage('where', [field('rating').greaterThan(4)]), + ); + stage('raw-stage/literal-params', (db) => _books(db).rawStage('limit', [10])); + stage( + 'raw-stage/string-params', + (db) => _books(db).rawStage('custom', ['a', 1.5, true, null]), + ); + stage( + 'raw-stage/plain-object-param', + (db) => _books(db).rawStage('select', [ + {'title': field('title'), 'one': 1}, + ]), + ); + stage( + 'raw-stage/nested-plain-object-param', + (db) => _books(db).rawStage('select', [ + { + 'meta': {'lang': field('lang')}, + }, + ]), + ); + // A map param is a literal map whose values are converted like a value + // position: a nested map or list becomes a map(...) or array(...) function, + // with or without expressions in it. + stage( + 'raw-stage/plain-object-param-literals', + (db) => _books(db).rawStage('custom', [ + {'a': 1, 'b': 'x', 'c': null}, + ]), + ); + stage( + 'raw-stage/empty-object-param', + (db) => _books(db).rawStage('custom', [{}]), + ); + stage( + 'raw-stage/nested-plain-object-param-literals', + (db) => _books(db).rawStage('custom', [ + { + 'meta': {'lang': 'en', 'year': 2020}, + }, + ]), + ); + stage( + 'raw-stage/nested-empty-object-param', + (db) => _books(db).rawStage('custom', [ + {'meta': {}}, + ]), + ); + stage( + 'raw-stage/array-in-object-param', + (db) => _books(db).rawStage('custom', [ + { + 'tags': [field('genre'), 'classic'], + }, + ]), + ); + stage( + 'raw-stage/array-in-object-param-literals', + (db) => _books(db).rawStage('custom', [ + { + 'tags': [1, 2], + }, + ]), + ); + stage( + 'raw-stage/deeply-nested-object-param', + (db) => _books(db).rawStage('custom', [ + { + 'a': { + 'b': {'c': field('x')}, + 'd': [ + field('y'), + {'e': 1}, + ], + }, + }, + ]), + ); + // constant() keeps a nested map literal. + stage( + 'raw-stage/constant-in-object-param', + (db) => _books(db).rawStage('custom', [ + { + 'meta': constant({'lang': 'en'}), + }, + ]), + ); + stage( + 'raw-stage/array-param', + (db) => _books(db).rawStage('custom', [ + [1, 'a', field('title')], + ]), + ); + // Any other param, a list included, is sent as is: a literal value all the + // way down. + stage( + 'raw-stage/array-param-literals', + (db) => _books(db).rawStage('custom', [ + [1, 2, 3], + ]), + ); + stage( + 'raw-stage/nested-array-param', + (db) => _books(db).rawStage('custom', [ + [ + 1, + [2, field('x')], + ], + ]), + ); + stage( + 'raw-stage/object-in-array-param', + (db) => _books(db).rawStage('custom', [ + [ + { + 'a': field('x'), + 'b': {'c': 1}, + }, + ], + ]), + ); + stage( + 'raw-stage/aggregate-params', + (db) => _books(db).rawStage('aggregate', [ + { + 'total': PipelineFunctions.countAll(), + 'best': PipelineFunctions.maximum('rating'), + }, + {'genre': field('genre')}, + ]), + ); + stage( + 'raw-stage/options', + (db) => _books(db).rawStage( + 'find_nearest', + [field('embedding'), _vector, 'euclidean'], + options: {'limit': 10, 'distance_field': field('distance')}, + ), + ); + stage( + 'raw-stage/options-dot-notation', + (db) => + _books(db).rawStage('custom', [], options: {'outer.inner': 'value'}), + ); + void rawOptions(String id, Map options) { + stage( + 'raw-stage/$id', + (db) => _books(db).rawStage('custom', const [], options: options), + ); + } + + rawOptions('options-nested-object', { + 'outer': {'inner': 'value', 'n': 1}, + }); + rawOptions('options-expression-values', { + 'target': field('x'), + 'nested': {'target': field('y')}, + }); + rawOptions('options-dot-notation-siblings', {'outer.a': 1, 'outer.b': 2}); + rawOptions('options-dot-notation-deep', {'a.b.c': true}); + // Keys apply in order: a dotted key merges into an earlier map, a map + // replaces what earlier dotted keys built, and a dotted key replaces an + // earlier value that is not a map. + rawOptions('options-dot-notation-merges-object', { + 'outer': {'a': 1}, + 'outer.b': 2, + }); + rawOptions('options-object-replaces-dot-notation', { + 'outer.a': 1, + 'outer': {'b': 2}, + }); + rawOptions('options-dot-notation-replaces-scalar', { + 'outer': 'x', + 'outer.a': 1, + }); + // Segments are not unescaped: backticks are part of the name. + rawOptions('options-dot-notation-backticks', {'a.`b.c`': 1}); + + // Dart's search stage takes the backend options as-is. + final breakfast = documentMatches('breakfast'); + stage('search/query-string', (db) => _books(db).search({'query': breakfast})); + stage( + 'search/query-expression', + (db) => _books(db).search({'query': breakfast}), + ); + stage( + 'search/limit-offset', + (db) => _books(db).search({'query': breakfast, 'limit': 10, 'offset': 5}), + ); + stage( + 'search/retrieval-depth-language', + (db) => _books(db).search({ + 'query': breakfast, + 'retrieval_depth': 100, + 'language_code': 'en', + }), + ); + stage( + 'search/sort-single', + (db) => _books(db).search({ + 'query': breakfast, + 'sort': [score().descending()], + }), + ); + stage( + 'search/sort-list', + (db) => _books(db).search({ + 'query': breakfast, + 'sort': [score().descending(), field('title').ascending()], + }), + ); + stage( + 'search/add-fields', + (db) => _books(db).search({ + 'query': breakfast, + 'add_fields': {'searchScore': score()}, + }), + ); + stage( + 'search/raw-options', + (db) => _books(db).search( + {'query': breakfast, 'limit': 10}, + rawOptions: const {'limit': 20, 'extra.flag': true}, + ), + ); +} + +void _registerOptions(_Registry r) { + void execute( + String id, + Future Function(Pipeline pipeline) run, + ) { + r.run('options/$id', [(db) => run(_books(db))]); + } + + const analyze = PipelineExplainMode.analyze; + const text = PipelineExplainOutputFormat.text; + + execute('none/plain', (p) => p.execute()); + execute('none/empty-object', (p) => p.execute()); + execute( + 'index-mode/recommended', + (p) => p.execute(indexMode: PipelineIndexMode.recommended), + ); + execute( + 'explain/mode-analyze', + (p) => p.execute(explain: const PipelineExplainOptions(mode: analyze)), + ); + execute( + 'explain/mode-execute', + (p) => p.execute( + explain: const PipelineExplainOptions(mode: PipelineExplainMode.execute), + ), + ); + execute( + 'explain/output-format-text', + (p) => p.execute(explain: const PipelineExplainOptions(outputFormat: text)), + ); + execute( + 'explain/mode-and-output-format', + (p) => p.execute( + explain: const PipelineExplainOptions(mode: analyze, outputFormat: text), + ), + ); + execute( + 'explain/empty', + (p) => p.execute(explain: const PipelineExplainOptions()), + ); + execute( + 'combined/index-mode-and-explain', + (p) => p.execute( + indexMode: PipelineIndexMode.recommended, + explain: const PipelineExplainOptions(mode: analyze, outputFormat: text), + ), + ); + execute( + 'raw-options/flat', + (p) => p.execute(rawOptions: {'foo': 'bar', 'count': 3}), + ); + execute( + 'raw-options/nested-object', + (p) => p.execute( + rawOptions: { + 'explain_options': {'mode': 'analyze'}, + }, + ), + ); + execute( + 'raw-options/dot-notation', + (p) => p.execute(rawOptions: {'explain_options.mode': 'analyze'}), + ); + execute( + 'raw-options/override-known', + (p) => p.execute( + indexMode: PipelineIndexMode.recommended, + rawOptions: {'index_mode': 'custom'}, + ), + ); + execute( + 'raw-options/merge-into-explain', + (p) => p.execute( + explain: const PipelineExplainOptions(mode: analyze), + rawOptions: {'explain_options.output_format': 'text'}, + ), + ); + execute( + 'raw-options/dot-notation-overrides-known', + (p) => p.execute( + explain: const PipelineExplainOptions(mode: analyze, outputFormat: text), + rawOptions: {'explain_options.mode': 'execute'}, + ), + ); + execute( + 'raw-options/object-replaces-known', + (p) => p.execute( + explain: const PipelineExplainOptions(mode: analyze, outputFormat: text), + rawOptions: { + 'explain_options': {'mode': 'execute'}, + }, + ), + ); + execute( + 'raw-options/dot-notation-into-empty-explain', + (p) => p.execute( + explain: const PipelineExplainOptions(), + rawOptions: {'explain_options.mode': 'analyze'}, + ), + ); + execute( + 'raw-options/dot-notation-replaces-known-scalar', + (p) => p.execute( + indexMode: PipelineIndexMode.recommended, + rawOptions: {'index_mode.mode': 'custom'}, + ), + ); + execute( + 'raw-options/dot-notation-siblings', + (p) => p.execute(rawOptions: {'outer.a': 1, 'outer.b': 'two'}), + ); + execute( + 'raw-options/dot-notation-deep', + (p) => p.execute(rawOptions: {'a.b.c': true}), + ); +} + +// ----------------------------------------------------------------------------- +// Values +// ----------------------------------------------------------------------------- + +void _registerValues(_Registry r) { + void selectConstant(String id, Object? value, [List<_Expr> alts = const []]) { + r.expr('values/constant/$id', () => constant(value), alts); + } + + selectConstant('integer', 42); + selectConstant('negative-integer', -7); + selectConstant('zero', 0); + selectConstant('double', 3.14); + selectConstant('nan', double.nan); + selectConstant('infinity', double.infinity); + selectConstant('negative-infinity', double.negativeInfinity); + selectConstant('string', 'Fantasy'); + selectConstant('empty-string', ''); + selectConstant('true', true); + selectConstant('false', false); + selectConstant('null', null); + selectConstant('timestamp', _timestamp); + selectConstant( + 'timestamp-millis', + Timestamp(seconds: 1700000000, nanoseconds: 123000000), + ); + selectConstant('date', _date); + selectConstant('geo-point', _geoPoint); + selectConstant('bytes', _bytes); + r.dbExpr('values/constant/document-reference', (db) => constant(_docRef(db))); + selectConstant('vector', _vector, [ + () => Expression.vector([0.1, 0.2, 0.3]), + ]); + selectConstant('array', [1, 'a', true, null]); + selectConstant('map', { + 'a': 1, + 'b': 'x', + 'nested': { + 'c': [1, 2], + }, + }); + + void selectField(String id, Object name) { + r.expr('values/field/$id', () => field(name), [ + () => Expression.field(name), + ]); + } + + selectField('simple', 'title'); + selectField('nested', 'metadata.lang'); + selectField('document-id', '__name__'); + selectField('special-characters', 'first-name'); + selectField('space', 'last name'); + selectField('non-ascii', 'naïve'); + selectField('backtick', 'a`b'); + selectField('backslash', r'a\b'); + selectField('reserved-characters', 'a/b'); + selectField('field-path-object', FieldPath(const ['a.b', 'c'])); + r.expr('values/field/field-name-argument', () => equal('first-name', 'Ada'), [ + () => PipelineFunctions.equal('first-name', 'Ada'), + ]); + + r.expr('values/variable/plain', () => variable('x'), [ + () => Expression.variable('x'), + ]); + + void equalValue(String id, Object? Function() value) { + r.expr('values/value-position/$id', () => field('value').equal(value())); + } + + equalValue('integer', () => 1); + equalValue('double', () => 1.5); + equalValue('string', () => 'a'); + equalValue('boolean', () => true); + equalValue('null', () => null); + equalValue('timestamp', () => _timestamp); + equalValue('date', () => _date); + equalValue('geo-point', () => _geoPoint); + equalValue('bytes', () => _bytes); + r.dbExpr( + 'values/value-position/document-reference', + (db) => field('value').equal(_docRef(db)), + ); + equalValue('vector', () => _vector); + equalValue('list-plain', () => ['a', 'b']); + equalValue('list-with-expression', () => ['a', field('b')]); + equalValue( + 'list-nested', + () => [ + ['a'], + {'k': 'v'}, + ], + ); + equalValue('list-empty', () => []); + equalValue('map-plain', () => {'a': 1, 'b': 'x'}); + equalValue('map-with-expression', () => {'a': field('x'), 'b': 1}); + equalValue( + 'map-nested', + () => { + 'inner': {'k': field('y')}, + 'list': [field('z')], + }, + ); + equalValue('map-empty', () => {}); + equalValue('constant-list', () => constant(['a', 'b'])); + + r.pipeline('values/data-types/all-constants', (db) { + final docRef = _docRef(db); + return _books(db).limit(1).select([ + constant(1).as('number'), + constant('a string').as('string'), + constant(true).as('boolean'), + constant(null).as('null'), + constant(_geoPoint).as('geoPoint'), + constant(_timestamp).as('timestamp'), + constant(_date).as('date'), + constant(_bytes).as('bytes'), + constant(docRef).as('documentReference'), + constant(_vector).as('vectorValue'), + PipelineFunctions.map({ + 'number': 1, + 'string': 'a string', + 'boolean': true, + 'null': null, + 'geoPoint': _geoPoint, + 'timestamp': _timestamp, + 'date': _date, + 'uint8Array': _bytes, + 'documentReference': docRef, + 'vectorValue': _vector, + 'map': {'number': 2, 'string': 'b string'}, + 'array': [1, 'c string'], + }).as('map'), + PipelineFunctions.array([ + 1, + 'a string', + true, + null, + _geoPoint, + _timestamp, + _date, + _bytes, + docRef, + _vector, + {'number': 2, 'string': 'b string'}, + ]).as('array'), + ]); + }); + + r.pipeline('values/data-types/array-and-map-functions', (db) { + List metadataArray() => [ + 1, + 2, + field('genre'), + PipelineFunctions.multiply('rating', 10), + [field('title')], + {'published': field('published')}, + ]; + Map metadata() => { + 'genre': field('genre'), + 'rating': PipelineFunctions.multiply('rating', 10), + 'nestedArray': [field('title')], + 'nestedMap': {'published': field('published')}, + }; + + return _books(db) + .sort([field('rating').descending()]) + .limit(1) + .select(['title', 'author', 'genre', 'rating', 'published', 'tags']) + .addFields([ + PipelineFunctions.array(metadataArray()).as('metadataArray'), + PipelineFunctions.map(metadata()).as('metadata'), + ]) + .where( + and([ + PipelineFunctions.equal('metadataArray', metadataArray()), + PipelineFunctions.equal('metadata', metadata()), + ]), + ); + }); +} + +// ----------------------------------------------------------------------------- +// Functions +// ----------------------------------------------------------------------------- + +void _registerArithmetic(_Registry r) { + r.binary( + 'add', + 'rating', + 1, + () => field('bonus'), + PipelineFunctions.add, + (e, v) => e.add(v), + ); + r.expr('functions/add/method-double', () => field('rating').add(0.5)); + r.expr( + 'functions/add/method-variadic', + () => field('rating').add(1, [field('bonus')]), + ); + r.expr( + 'functions/add/static-variadic', + () => PipelineFunctions.add('rating', 1, [field('bonus')]), + ); + r.binary( + 'subtract', + 'rating', + 1, + () => field('penalty'), + PipelineFunctions.subtract, + (e, v) => e.subtract(v), + ); + r.binary( + 'multiply', + 'rating', + 2, + () => field('weight'), + PipelineFunctions.multiply, + (e, v) => e.multiply(v), + ); + r.expr( + 'functions/multiply/method-variadic', + () => field('rating').multiply(2, [field('weight')]), + ); + r.expr( + 'functions/multiply/static-variadic', + () => PipelineFunctions.multiply('rating', 2, [field('weight')]), + ); + r.binary( + 'divide', + 'rating', + 2, + () => field('count'), + PipelineFunctions.divide, + (e, v) => e.divide(v), + ); + r.binary( + 'mod', + 'rating', + 2, + () => field('divisor'), + PipelineFunctions.mod, + (e, v) => e.mod(v), + ); + r.binary( + 'pow', + 'rating', + 2, + () => field('exponent'), + PipelineFunctions.pow, + (e, v) => e.pow(v), + ); + + r.unary('abs', 'rating', PipelineFunctions.abs, (e) => e.abs()); + r.unary('ceil', 'rating', PipelineFunctions.ceil, (e) => e.ceil()); + r.unary('floor', 'rating', PipelineFunctions.floor, (e) => e.floor()); + r.unary('exp', 'rating', PipelineFunctions.exp, (e) => e.exp()); + r.unary('ln', 'rating', PipelineFunctions.ln, (e) => e.ln()); + r.unary('sqrt', 'rating', PipelineFunctions.sqrt, (e) => e.sqrt()); + r.unary('log10', 'rating', PipelineFunctions.log10, (e) => e.log10()); + + for (final (name, static, method) in [ + ( + 'round', + PipelineFunctions.round, + (PipelineExpression e, [Object? places]) => e.round(places), + ), + ( + 'trunc', + PipelineFunctions.trunc, + (PipelineExpression e, [Object? places]) => e.trunc(places), + ), + ]) { + r.unary(name, 'rating', static, method); + r.expr( + 'functions/$name/static-field-name-places-literal', + () => static('rating', 2), + ); + r.expr( + 'functions/$name/static-expression-places-expression', + () => static(field('rating'), field('places')), + ); + r.expr( + 'functions/$name/method-places-literal', + () => method(field('rating'), 2), + ); + r.expr( + 'functions/$name/method-places-expression', + () => method(field('rating'), field('places')), + ); + } + + r.expr('functions/rand/static', PipelineFunctions.rand); +} + +void _registerComparisonAndLogic(_Registry r) { + for (final (name, static, topLevel, method) + in <(String, _Static2, _Static2, _Method1)>[ + ('equal', PipelineFunctions.equal, equal, (e, v) => e.equal(v)), + ( + 'notEqual', + PipelineFunctions.notEqual, + notEqual, + (e, v) => e.notEqual(v), + ), + ( + 'lessThan', + PipelineFunctions.lessThan, + lessThan, + (e, v) => e.lessThan(v), + ), + ( + 'lessThanOrEqual', + PipelineFunctions.lessThanOrEqual, + lessThanOrEqual, + (e, v) => e.lessThanOrEqual(v), + ), + ( + 'greaterThan', + PipelineFunctions.greaterThan, + greaterThan, + (e, v) => e.greaterThan(v), + ), + ( + 'greaterThanOrEqual', + PipelineFunctions.greaterThanOrEqual, + greaterThanOrEqual, + (e, v) => e.greaterThanOrEqual(v), + ), + ]) { + r.binary( + name, + 'rating', + 4, + () => field('threshold'), + static, + method, + staticAlternatives: [topLevel], + ); + r.expr( + 'functions/$name/static-field-name-string', + () => static('genre', 'Fantasy'), + [() => topLevel('genre', 'Fantasy')], + ); + } + + r.expr( + 'functions/and/static-two', + () => PipelineFunctions.and([_isFantasy(), _isGood()]), + [ + () => and([_isFantasy(), _isGood()]), + ], + ); + r.expr( + 'functions/and/static-three', + () => PipelineFunctions.and([_isFantasy(), _isGood(), _isOld()]), + [ + () => and([_isFantasy(), _isGood(), _isOld()]), + ], + ); + r.expr( + 'functions/or/static-two', + () => PipelineFunctions.or([_isFantasy(), _isGood()]), + [ + () => or([_isFantasy(), _isGood()]), + ], + ); + r.expr( + 'functions/or/static-three', + () => PipelineFunctions.or([_isFantasy(), _isGood(), _isOld()]), + [ + () => or([_isFantasy(), _isGood(), _isOld()]), + ], + ); + r.expr( + 'functions/xor/static-two', + () => PipelineFunctions.xor([_isFantasy(), _isGood()]), + ); + r.expr( + 'functions/xor/static-three', + () => PipelineFunctions.xor([_isFantasy(), _isGood(), _isOld()]), + ); + r.expr( + 'functions/nor/static-two', + () => PipelineFunctions.nor([_isFantasy(), _isGood()]), + ); + r.expr( + 'functions/nor/static-three', + () => PipelineFunctions.nor([_isFantasy(), _isGood(), _isOld()]), + ); + r.expr('functions/not/static', () => PipelineFunctions.not(_isFantasy()), [ + () => not(_isFantasy()), + ]); + r.expr('functions/not/method', () => _isFantasy().not()); + r.expr( + 'functions/conditional/static', + () => PipelineFunctions.conditional( + _isGood(), + field('title'), + constant('n/a'), + ), + ); + r.expr( + 'functions/conditional/method', + () => _isGood().conditional(field('title'), constant('n/a')), + ); + r.expr( + 'functions/switchOn/static-pair', + () => PipelineFunctions.switchOn([_isGood(), constant('good')]), + ); + r.expr( + 'functions/switchOn/static-pairs-and-default', + () => PipelineFunctions.switchOn([ + _isGood(), + constant('good'), + _isOld(), + constant('old'), + constant('other'), + ]), + ); + + r.expr( + 'functions/logicalMaximum/static-field-name-literal', + () => PipelineFunctions.logicalMaximum('rating', 1), + ); + r.expr( + 'functions/logicalMaximum/static-expression-variadic', + () => + PipelineFunctions.logicalMaximum(field('rating'), field('floor'), [3]), + ); + r.expr( + 'functions/logicalMaximum/method-literal', + () => field('rating').logicalMaximum(1), + ); + r.expr( + 'functions/logicalMaximum/method-variadic', + () => field('rating').logicalMaximum(field('floor'), [3]), + ); + r.expr( + 'functions/logicalMinimum/static-field-name-literal', + () => PipelineFunctions.logicalMinimum('rating', 5), + ); + r.expr( + 'functions/logicalMinimum/static-expression-variadic', + () => PipelineFunctions.logicalMinimum(field('rating'), field('ceiling'), [ + 3, + ]), + ); + r.expr( + 'functions/logicalMinimum/method-literal', + () => field('rating').logicalMinimum(5), + ); + r.expr( + 'functions/logicalMinimum/method-variadic', + () => field('rating').logicalMinimum(field('ceiling'), [3]), + ); + + r.unary('exists', 'title', PipelineFunctions.exists, (e) => e.exists()); + r.unary('isAbsent', 'title', PipelineFunctions.isAbsent, (e) => e.isAbsent()); + r.binary( + 'ifAbsent', + 'nickname', + 'anonymous', + () => field('name'), + PipelineFunctions.ifAbsent, + (e, v) => e.ifAbsent(v), + ); + r.binary( + 'ifNull', + 'nickname', + 'anonymous', + () => field('name'), + PipelineFunctions.ifNull, + (e, v) => e.ifNull(v), + ); + + PipelineExpression failing() => field('rating').divide(0); + r.expr( + 'functions/isError/static-expression', + () => PipelineFunctions.isError(failing()), + ); + r.expr('functions/isError/method', () => failing().isError()); + r.expr( + 'functions/ifError/static-expression-literal', + () => PipelineFunctions.ifError(failing(), 0), + ); + r.expr( + 'functions/ifError/static-expression-expression', + () => PipelineFunctions.ifError(failing(), field('fallback')), + ); + // Node types ifError of two booleans as a boolean, hence the where stage. + r.expr( + 'functions/ifError/static-boolean-boolean', + () => PipelineFunctions.ifError(_isGood(), constant(false)).asBoolean(), + ); + r.expr('functions/ifError/method-literal', () => failing().ifError(0)); + r.expr( + 'functions/ifError/method-expression', + () => failing().ifError(field('fallback')), + ); + r.expr( + 'functions/ifError/method-boolean-boolean', + () => _isGood().ifError(constant(false)).asBoolean(), + ); + r.expr( + 'functions/ifError/method-boolean-literal', + () => _isGood().ifError(false).asBoolean(), + ); + + r.expr( + 'functions/coalesce/static-field-name-literal', + () => PipelineFunctions.coalesce('nickname', 'anonymous'), + ); + r.expr( + 'functions/coalesce/static-expression-variadic', + () => PipelineFunctions.coalesce(field('nickname'), field('name'), [ + 'anonymous', + ]), + ); + r.expr( + 'functions/coalesce/method-literal', + () => field('nickname').coalesce('anonymous'), + ); + r.expr( + 'functions/coalesce/method-variadic', + () => field('nickname').coalesce(field('name'), ['anonymous']), + ); +} + +void _registerStrings(_Registry r) { + r.unary( + 'charLength', + 'title', + PipelineFunctions.charLength, + (e) => e.charLength(), + ); + r.unary( + 'byteLength', + 'title', + PipelineFunctions.byteLength, + (e) => e.byteLength(), + ); + r.unary( + 'toLower', + 'title', + PipelineFunctions.toLower, + (e) => e.toLowerCase(), + ); + r.unary( + 'toUpper', + 'title', + PipelineFunctions.toUpper, + (e) => e.toUpperCase(), + ); + r.unary( + 'stringReverse', + 'title', + PipelineFunctions.stringReverse, + (e) => e.stringReverse(), + ); + r.unary('reverse', 'title', PipelineFunctions.reverse, (e) => e.reverse()); + r.unary('length', 'title', PipelineFunctions.length, (e) => e.length()); + + r.binary( + 'like', + 'title', + 'The%', + () => field('pattern'), + PipelineFunctions.like, + (e, v) => e.like(v), + ); + r.binary( + 'regexContains', + 'title', + 'Lord', + () => field('pattern'), + PipelineFunctions.regexContains, + (e, v) => e.regexContains(v), + ); + r.binary( + 'regexMatch', + 'title', + '^The.*', + () => field('pattern'), + PipelineFunctions.regexMatch, + (e, v) => e.regexMatch(v), + ); + r.binary( + 'regexFind', + 'title', + r'[A-Z]\w+', + () => field('pattern'), + PipelineFunctions.regexFind, + (e, v) => e.regexFind(v), + ); + r.binary( + 'regexFindAll', + 'title', + r'[A-Z]\w+', + () => field('pattern'), + PipelineFunctions.regexFindAll, + (e, v) => e.regexFindAll(v), + ); + r.binary( + 'stringContains', + 'title', + 'Ring', + () => field('needle'), + PipelineFunctions.stringContains, + (e, v) => e.stringContains(v), + ); + r.binary( + 'startsWith', + 'title', + 'The', + () => field('prefix'), + PipelineFunctions.startsWith, + (e, v) => e.startsWith(v), + ); + r.binary( + 'endsWith', + 'title', + 'Rings', + () => field('suffix'), + PipelineFunctions.endsWith, + (e, v) => e.endsWith(v), + ); + r.binary( + 'stringIndexOf', + 'title', + 'Ring', + () => field('needle'), + PipelineFunctions.stringIndexOf, + (e, v) => e.stringIndexOf(v), + ); + r.binary( + 'stringRepeat', + 'title', + 2, + () => field('times'), + PipelineFunctions.stringRepeat, + (e, v) => e.stringRepeat(v), + ); + r.binary( + 'split', + 'csv', + ',', + () => field('delimiter'), + PipelineFunctions.split, + (e, v) => e.split(v), + methodAlternatives: [(e, v) => e.splitLiteral(v! as String)], + ); + r.binary( + 'join', + 'tags', + ', ', + () => field('delimiter'), + PipelineFunctions.join, + (e, v) => e.join(v), + methodAlternatives: [(e, v) => e.joinLiteral(v! as String)], + ); + + for (final (name, static, method) in [ + ( + 'trim', + PipelineFunctions.trim, + (PipelineExpression e, [Object? chars]) => e.trim(chars), + ), + ( + 'ltrim', + PipelineFunctions.ltrim, + (PipelineExpression e, [Object? chars]) => e.ltrim(chars), + ), + ( + 'rtrim', + PipelineFunctions.rtrim, + (PipelineExpression e, [Object? chars]) => e.rtrim(chars), + ), + ]) { + r.unary(name, 'title', static, method); + r.expr( + 'functions/$name/static-field-name-literal', + () => static('title', '-'), + ); + r.expr( + 'functions/$name/static-expression-expression', + () => static(field('title'), field('chars')), + ); + r.expr('functions/$name/method-literal', () => method(field('title'), '-')); + r.expr( + 'functions/$name/method-expression', + () => method(field('title'), field('chars')), + ); + } + + r.expr( + 'functions/stringConcat/static-field-name-literal', + () => PipelineFunctions.stringConcat(['title', ' by ']), + ); + r.expr( + 'functions/stringConcat/static-field-name-variadic', + () => PipelineFunctions.stringConcat(['title', ' by ', field('author')]), + ); + r.expr( + 'functions/stringConcat/static-expression-expression', + () => PipelineFunctions.stringConcat([field('title'), field('author')]), + ); + r.expr( + 'functions/stringConcat/method-literal', + () => field('title').stringConcat([' by ']), + ); + r.expr( + 'functions/stringConcat/method-variadic', + () => field('title').stringConcat([' by ', field('author')]), + ); + + r.expr( + 'functions/concat/static-field-name-literal', + () => PipelineFunctions.concat('title', '!'), + ); + r.expr( + 'functions/concat/static-expression-variadic', + () => PipelineFunctions.concat(field('title'), ' by ', [field('author')]), + ); + r.expr('functions/concat/method-literal', () => field('title').concat(['!'])); + r.expr( + 'functions/concat/method-list', + () => field('tags').concat([ + ['extra'], + ]), + ); + r.expr( + 'functions/concat/method-variadic', + () => field('title').concat([' by ', field('author')]), + ); + + for (final (name, static, method, literalMethod) in [ + ( + 'stringReplaceAll', + PipelineFunctions.stringReplaceAll, + (PipelineExpression e, Object? f, Object? t) => e.stringReplaceAll(f, t), + (PipelineExpression e, String f, String t) => + e.stringReplaceAllLiteral(f, t), + ), + ( + 'stringReplaceOne', + PipelineFunctions.stringReplaceOne, + (PipelineExpression e, Object? f, Object? t) => e.stringReplaceOne(f, t), + (PipelineExpression e, String f, String t) => + e.stringReplaceOneLiteral(f, t), + ), + ]) { + r.expr( + 'functions/$name/static-field-name-literals', + () => static('title', 'a', 'b'), + ); + r.expr( + 'functions/$name/static-expression-expressions', + () => static(field('title'), field('find'), field('replacement')), + ); + r.expr( + 'functions/$name/method-literals', + () => method(field('title'), 'a', 'b'), + [() => literalMethod(field('title'), 'a', 'b')], + ); + r.expr( + 'functions/$name/method-expressions', + () => method(field('title'), field('find'), field('replacement')), + ); + } + + r.expr( + 'functions/substring/static-field-name-position', + () => PipelineFunctions.substring('title', 2), + ); + r.expr( + 'functions/substring/static-field-name-position-length', + () => PipelineFunctions.substring('title', 2, 3), + ); + r.expr( + 'functions/substring/static-expression-expressions', + () => PipelineFunctions.substring( + field('title'), + field('start'), + field('count'), + ), + ); + r.expr( + 'functions/substring/method-position', + () => field('title').substring(2), + [() => field('title').substringLiteral(2)], + ); + r.expr( + 'functions/substring/method-position-length', + () => field('title').substring(2, 3), + [() => field('title').substringLiteral(2, 3)], + ); + r.expr( + 'functions/substring/method-expressions', + () => field('title').substring(field('start'), field('count')), + ); +} + +void _registerArrays(_Registry r) { + void array(String variant, List Function() values) { + r.expr( + 'functions/array/$variant', + () => PipelineFunctions.array(values()), + [() => Expression.array(values())], + ); + } + + array('static-literals', () => [1, 'a', true, null]); + array('static-with-expressions', () => [field('title'), 1]); + array( + 'static-nested', + () => [ + [1, field('a')], + {'k': field('b')}, + ], + ); + array('static-empty', () => []); + + r.expr( + 'functions/arrayConcat/static-field-name-list', + () => PipelineFunctions.arrayConcat([ + 'tags', + ['x', 'y'], + ]), + ); + r.expr( + 'functions/arrayConcat/static-field-name-expression', + () => PipelineFunctions.arrayConcat(['tags', field('moreTags')]), + ); + r.expr( + 'functions/arrayConcat/static-expression-variadic', + () => PipelineFunctions.arrayConcat([ + field('tags'), + field('moreTags'), + ['z'], + ]), + ); + r.expr( + 'functions/arrayConcat/method-list', + () => field('tags').arrayConcat(['x', 'y']), + ); + r.expr( + 'functions/arrayConcat/method-expression', + () => field('tags').arrayConcat(field('moreTags')), + ); + r.expr( + 'functions/arrayConcat/method-variadic', + () => field('tags').arrayConcatMultiple([ + field('moreTags'), + ['z'], + ]), + ); + + r.binary( + 'arrayContains', + 'tags', + 'magic', + () => field('wanted'), + PipelineFunctions.arrayContains, + (e, v) => e.arrayContains(v), + ); + + for (final (name, target, values, withExpression, nested, static, method) in [ + ( + 'arrayContainsAll', + 'tags', + ['magic', 'epic'], + () => ['magic', field('wanted')], + [ + 'magic', + ['epic'], + ], + PipelineFunctions.arrayContainsAll, + (PipelineExpression e, Object? v) => switch (v) { + Iterable() => e.arrayContainsAll(v), + _ => e.arrayContainsAllFrom(v), + }, + ), + ( + 'arrayContainsAny', + 'tags', + ['magic', 'epic'], + () => ['magic', field('wanted')], + [ + 'magic', + ['epic'], + ], + PipelineFunctions.arrayContainsAny, + // PipelineExpression.arrayContainsAny only takes a list; the + // PipelineFunctions form takes an array expression. + (PipelineExpression e, Object? v) => switch (v) { + Iterable() => e.arrayContainsAny(v), + _ => PipelineFunctions.arrayContainsAny(e, v), + }, + ), + ( + 'equalAny', + 'genre', + ['Fantasy', 'Sci-Fi'], + () => ['Fantasy', field('favorite')], + [ + 'Fantasy', + ['Sci-Fi'], + ], + PipelineFunctions.equalAny, + (PipelineExpression e, Object? v) => e.equalAny(v), + ), + ( + 'notEqualAny', + 'genre', + ['Fantasy', 'Sci-Fi'], + () => ['Fantasy', field('favorite')], + [ + 'Fantasy', + ['Sci-Fi'], + ], + PipelineFunctions.notEqualAny, + (PipelineExpression e, Object? v) => e.notEqualAny(v), + ), + ]) { + final arrayExpression = target == 'tags' ? 'wanted' : 'genres'; + r.expr( + 'functions/$name/static-field-name-list', + () => static(target, values), + ); + r.expr( + 'functions/$name/static-field-name-list-with-expression', + () => static(target, withExpression()), + ); + r.expr( + 'functions/$name/static-field-name-list-nested', + () => static(target, nested), + ); + r.expr( + 'functions/$name/static-field-name-array-expression', + () => static(target, field(arrayExpression)), + ); + r.expr( + 'functions/$name/static-expression-list', + () => static(field(target), values), + ); + r.expr('functions/$name/method-list', () => method(field(target), values)); + r.expr( + 'functions/$name/method-list-with-expression', + () => method(field(target), withExpression()), + ); + r.expr( + 'functions/$name/method-array-expression', + () => method(field(target), field(arrayExpression)), + ); + } + + r.unary( + 'arrayLength', + 'tags', + PipelineFunctions.arrayLength, + (e) => e.arrayLength(), + ); + r.unary( + 'arrayFirst', + 'tags', + PipelineFunctions.arrayFirst, + (e) => e.arrayFirst(), + ); + r.unary( + 'arrayLast', + 'tags', + PipelineFunctions.arrayLast, + (e) => e.arrayLast(), + ); + r.unary( + 'arrayReverse', + 'tags', + PipelineFunctions.arrayReverse, + (e) => e.arrayReverse(), + ); + r.unary('arraySum', 'tags', PipelineFunctions.arraySum, (e) => e.arraySum()); + r.unary( + 'arrayMaximum', + 'tags', + PipelineFunctions.arrayMaximum, + (e) => e.arrayMaximum(), + ); + r.unary( + 'arrayMinimum', + 'tags', + PipelineFunctions.arrayMinimum, + (e) => e.arrayMinimum(), + ); + + r.binary( + 'arrayFirstN', + 'scores', + 2, + () => field('n'), + PipelineFunctions.arrayFirstN, + (e, v) => e.arrayFirstN(v), + ); + r.binary( + 'arrayLastN', + 'scores', + 2, + () => field('n'), + PipelineFunctions.arrayLastN, + (e, v) => e.arrayLastN(v), + ); + r.binary( + 'arrayMaximumN', + 'scores', + 2, + () => field('n'), + PipelineFunctions.arrayMaximumN, + (e, v) => e.arrayMaximumN(v), + ); + r.binary( + 'arrayMinimumN', + 'scores', + 2, + () => field('n'), + PipelineFunctions.arrayMinimumN, + (e, v) => e.arrayMinimumN(v), + ); + r.binary( + 'arrayGet', + 'tags', + 1, + () => field('index'), + PipelineFunctions.arrayGet, + (e, v) => e.arrayGet(v), + ); + r.binary( + 'arrayIndexOf', + 'tags', + 'magic', + () => field('wanted'), + PipelineFunctions.arrayIndexOf, + (e, v) => e.arrayIndexOf(v), + ); + r.binary( + 'arrayLastIndexOf', + 'tags', + 'magic', + () => field('wanted'), + PipelineFunctions.arrayLastIndexOf, + (e, v) => e.arrayLastIndexOf(v), + ); + r.binary( + 'arrayIndexOfAll', + 'tags', + 'magic', + () => field('wanted'), + PipelineFunctions.arrayIndexOfAll, + (e, v) => e.arrayIndexOfAll(v), + ); + + r.expr( + 'functions/arraySlice/static-field-name-offset-length', + () => PipelineFunctions.arraySlice('tags', 1, 2), + ); + r.expr( + 'functions/arraySlice/static-expression-expressions', + () => PipelineFunctions.arraySlice( + field('tags'), + field('start'), + field('count'), + ), + ); + r.expr( + 'functions/arraySlice/method-offset', + () => field('tags').arraySlice(1), + ); + r.expr( + 'functions/arraySlice/method-offset-length', + () => field('tags').arraySlice(1, 2), + ); + r.expr( + 'functions/arraySlice/method-expressions', + () => field('tags').arraySlice(field('start'), field('count')), + ); + + PipelineBooleanExpression isMagic() => variable('tag').equal('magic'); + r.expr( + 'functions/arrayFilter/static-field-name', + () => PipelineFunctions.arrayFilter('tags', 'tag', isMagic()), + ); + r.expr( + 'functions/arrayFilter/static-expression', + () => PipelineFunctions.arrayFilter(field('tags'), 'tag', isMagic()), + ); + r.expr( + 'functions/arrayFilter/method', + () => field('tags').arrayFilter('tag', isMagic()), + ); + + PipelineExpression upper() => variable('tag').toUpperCase(); + r.expr( + 'functions/arrayTransform/static-field-name', + () => PipelineFunctions.arrayTransform('tags', 'tag', upper()), + ); + r.expr( + 'functions/arrayTransform/static-expression', + () => PipelineFunctions.arrayTransform(field('tags'), 'tag', upper()), + ); + r.expr( + 'functions/arrayTransform/method', + () => field('tags').arrayTransform('tag', upper()), + ); + + PipelineExpression indexed() => variable('tag').stringConcat([variable('i')]); + r.expr( + 'functions/arrayTransformWithIndex/static-field-name', + () => PipelineFunctions.arrayTransformWithIndex( + 'tags', + 'tag', + 'i', + indexed(), + ), + ); + r.expr( + 'functions/arrayTransformWithIndex/static-expression', + () => PipelineFunctions.arrayTransformWithIndex( + field('tags'), + 'tag', + 'i', + indexed(), + ), + ); + r.expr( + 'functions/arrayTransformWithIndex/method', + () => field('tags').arrayTransformWithIndex('tag', 'i', indexed()), + ); +} + +void _registerMaps(_Registry r) { + // Node's map(Record) is a Dart Map; the alternating-Iterable form must + // send the same request. + r.expr( + 'functions/map/static-literals', + () => PipelineFunctions.map({'a': 1, 'b': 'x'}), + [ + () => PipelineFunctions.map(['a', 1, 'b', 'x']), + ], + ); + r.expr( + 'functions/map/static-with-expressions', + () => PipelineFunctions.map({'a': field('title'), 'b': 1}), + [ + () => PipelineFunctions.map(['a', field('title'), 'b', 1]), + ], + ); + r.expr( + 'functions/map/static-nested', + () => PipelineFunctions.map({ + 'inner': {'k': field('y')}, + 'list': [field('z'), 1], + }), + [ + () => PipelineFunctions.map([ + 'inner', + {'k': field('y')}, + 'list', + [field('z'), 1], + ]), + ], + ); + r.expr( + 'functions/map/static-empty', + () => PipelineFunctions.map({}), + [() => PipelineFunctions.map(const [])], + ); + + r.expr( + 'functions/mapGet/static-field-name', + () => PipelineFunctions.mapGet('metadata', 'lang'), + ); + r.expr( + 'functions/mapGet/static-expression', + () => PipelineFunctions.mapGet(field('metadata'), 'lang'), + ); + r.expr('functions/mapGet/method', () => field('metadata').mapGet('lang'), [ + () => field('metadata').mapGetLiteral('lang'), + ]); + + r.expr( + 'functions/mapSet/static-field-name-literal', + () => PipelineFunctions.mapSet('metadata', ['lang', 'en']), + ); + r.expr( + 'functions/mapSet/static-expression-variadic', + () => PipelineFunctions.mapSet(field('metadata'), [ + 'lang', + 'en', + 'pages', + field('pageCount'), + ]), + ); + r.expr( + 'functions/mapSet/method-literal', + () => field('metadata').mapSet('lang', 'en'), + ); + r.expr( + 'functions/mapSet/method-expression-key', + () => field('metadata').mapSet(field('key'), field('value')), + ); + r.expr( + 'functions/mapSet/method-variadic', + () => field('metadata').mapSet('lang', 'en', ['pages', field('pageCount')]), + ); + + r.binary( + 'mapRemove', + 'metadata', + 'lang', + () => field('key'), + PipelineFunctions.mapRemove, + (e, v) => e.mapRemove(v), + ); + + r.expr( + 'functions/mapMerge/static-field-name-map', + () => PipelineFunctions.mapMerge([ + 'metadata', + {'extra': 1}, + ]), + ); + r.expr( + 'functions/mapMerge/static-expression-expression', + () => PipelineFunctions.mapMerge([field('metadata'), field('overrides')]), + ); + r.expr( + 'functions/mapMerge/static-field-name-variadic', + () => PipelineFunctions.mapMerge([ + 'metadata', + {'extra': field('title')}, + field('overrides'), + ]), + ); + r.expr( + 'functions/mapMerge/method-map', + () => field('metadata').mapMerge([ + {'extra': 1}, + ]), + ); + r.expr( + 'functions/mapMerge/method-variadic', + () => field('metadata').mapMerge([ + {'extra': field('title')}, + field('overrides'), + ]), + ); + + r.unary('mapKeys', 'metadata', PipelineFunctions.mapKeys, (e) => e.mapKeys()); + r.unary( + 'mapValues', + 'metadata', + PipelineFunctions.mapValues, + (e) => e.mapValues(), + ); + r.unary( + 'mapEntries', + 'metadata', + PipelineFunctions.mapEntries, + (e) => e.mapEntries(), + ); + r.binary( + 'getField', + 'metadata', + 'lang', + () => field('key'), + PipelineFunctions.getField, + (e, v) => e.getField(v), + ); +} + +void _registerTimestamps(_Registry r) { + r.unary( + 'unixMicrosToTimestamp', + 'published', + PipelineFunctions.unixMicrosToTimestamp, + (e) => e.unixMicrosToTimestamp(), + ); + r.unary( + 'unixMillisToTimestamp', + 'published', + PipelineFunctions.unixMillisToTimestamp, + (e) => e.unixMillisToTimestamp(), + ); + r.unary( + 'unixSecondsToTimestamp', + 'published', + PipelineFunctions.unixSecondsToTimestamp, + (e) => e.unixSecondsToTimestamp(), + ); + r.unary( + 'timestampToUnixMicros', + 'published', + PipelineFunctions.timestampToUnixMicros, + (e) => e.timestampToUnixMicros(), + ); + r.unary( + 'timestampToUnixMillis', + 'published', + PipelineFunctions.timestampToUnixMillis, + (e) => e.timestampToUnixMillis(), + ); + r.unary( + 'timestampToUnixSeconds', + 'published', + PipelineFunctions.timestampToUnixSeconds, + (e) => e.timestampToUnixSeconds(), + ); + + for (final (name, static, method) in [ + ( + 'timestampAdd', + PipelineFunctions.timestampAdd, + (PipelineExpression e, Object? unit, Object? amount) => + e.timestampAdd(unit, amount), + ), + ( + 'timestampSubtract', + PipelineFunctions.timestampSubtract, + (PipelineExpression e, Object? unit, Object? amount) => + e.timestampSubtract(unit, amount), + ), + ]) { + r.expr( + 'functions/$name/static-field-name-literals', + () => static('published', 'day', 1), + ); + r.expr( + 'functions/$name/static-expression-literals', + () => static(field('published'), 'hour', 2), + ); + r.expr( + 'functions/$name/static-expression-expressions', + () => static(field('published'), field('unit'), field('amount')), + ); + r.expr( + 'functions/$name/method-literals', + () => method(field('published'), 'minute', 30), + ); + r.expr( + 'functions/$name/method-expressions', + () => method(field('published'), field('unit'), field('amount')), + ); + } + + for (final (name, static, method, literal, timezoneLiteral, methodLiteral) + in [ + ( + 'timestampTruncate', + PipelineFunctions.timestampTruncate, + (PipelineExpression e, Object? unit, [Object? timezone]) => + e.timestampTruncate(unit, timezone), + 'day', + 'week(monday)', + 'year', + ), + ( + 'timestampExtract', + PipelineFunctions.timestampExtract, + (PipelineExpression e, Object? part, [Object? timezone]) => + e.timestampExtract(part, timezone), + 'year', + 'dayofweek', + 'dayofyear', + ), + ]) { + final unit = name == 'timestampTruncate' ? 'granularity' : 'part'; + final literalWithTimezone = name == 'timestampTruncate' ? 'day' : 'hour'; + r.expr( + 'functions/$name/static-field-name-literal', + () => static('published', literal), + ); + r.expr( + 'functions/$name/static-field-name-literal-timezone', + () => static('published', timezoneLiteral, 'America/New_York'), + ); + r.expr( + 'functions/$name/static-expression-expression', + () => static(field('published'), field(unit)), + ); + r.expr( + 'functions/$name/static-expression-timezone-expression', + () => static(field('published'), 'month', field('tz')), + ); + r.expr( + 'functions/$name/method-literal', + () => method(field('published'), methodLiteral), + ); + r.expr( + 'functions/$name/method-literal-timezone', + () => method(field('published'), literalWithTimezone, 'UTC'), + ); + r.expr( + 'functions/$name/method-expression-timezone-expression', + () => method(field('published'), field(unit), field('tz')), + ); + } + + r.expr( + 'functions/timestampDiff/static-field-names', + () => PipelineFunctions.timestampDiff('end', 'start', 'day'), + ); + r.expr( + 'functions/timestampDiff/static-field-name-expression', + () => PipelineFunctions.timestampDiff('end', field('start'), 'second'), + ); + r.expr( + 'functions/timestampDiff/static-expression-field-name', + () => PipelineFunctions.timestampDiff(field('end'), 'start', 'hour'), + ); + r.expr( + 'functions/timestampDiff/static-expressions-unit-expression', + () => PipelineFunctions.timestampDiff( + field('end'), + field('start'), + field('unit'), + ), + ); + r.expr( + 'functions/timestampDiff/method-field-name', + () => field('end').timestampDiff('start', 'day'), + ); + r.expr( + 'functions/timestampDiff/method-expressions', + () => field('end').timestampDiff(field('start'), field('unit')), + ); + r.expr( + 'functions/currentTimestamp/static', + PipelineFunctions.currentTimestamp, + ); +} + +void _registerTypesVectorsAndReferences(_Registry r) { + r.unary('type', 'value', PipelineFunctions.type, (e) => e.type()); + r.expr( + 'functions/isType/static-field-name', + () => PipelineFunctions.isType('value', PipelineValueType.string), + [() => PipelineFunctions.isType('value', 'string')], + ); + r.expr( + 'functions/isType/static-expression', + () => PipelineFunctions.isType(field('value'), PipelineValueType.string), + [() => PipelineFunctions.isType(field('value'), 'string')], + ); + for (final (name, type) in [ + ('null', PipelineValueType.nullValue), + ('array', PipelineValueType.array), + ('boolean', PipelineValueType.boolean), + ('bytes', PipelineValueType.bytes), + ('timestamp', PipelineValueType.timestamp), + ('geo_point', PipelineValueType.geoPoint), + ('number', PipelineValueType.number), + ('int32', PipelineValueType.int32), + ('int64', PipelineValueType.int64), + ('float64', PipelineValueType.double), + ('decimal128', PipelineValueType.decimal128), + ('map', PipelineValueType.map), + ('reference', PipelineValueType.reference), + ('string', PipelineValueType.string), + ('vector', PipelineValueType.vector), + ('max_key', PipelineValueType.maxKey), + ('min_key', PipelineValueType.minKey), + ('object_id', PipelineValueType.objectId), + ('regex', PipelineValueType.regex), + // No PipelineValueType member: isType takes the raw name. + ('request_timestamp', null), + ]) { + final id = 'functions/isType/method-${name.replaceAll('_', '-')}'; + if (type == null) { + r.expr(id, () => field('value').isType(name)); + } else { + r.expr(id, () => field('value').isType(type), [ + () => field('value').isType(name), + ]); + } + } + + for (final (name, static, method) in [ + ( + 'cosineDistance', + PipelineFunctions.cosineDistance, + (PipelineExpression e, Object? v) => e.cosineDistance(v), + ), + ( + 'dotProduct', + PipelineFunctions.dotProduct, + (PipelineExpression e, Object? v) => e.dotProduct(v), + ), + ( + 'euclideanDistance', + PipelineFunctions.euclideanDistance, + (PipelineExpression e, Object? v) => e.euclideanDistance(v), + ), + ]) { + r.expr( + 'functions/$name/static-field-name-number-array', + () => static('embedding', const [0.1, 0.2, 0.3]), + ); + r.expr( + 'functions/$name/static-field-name-integer-array', + () => static('embedding', const [1, 2, 3]), + ); + r.expr( + 'functions/$name/static-field-name-vector-value', + () => static('embedding', _vector), + [ + () => static('embedding', Expression.vector([0.1, 0.2, 0.3])), + ], + ); + r.expr( + 'functions/$name/static-field-name-expression', + () => static('embedding', field('other')), + ); + r.expr( + 'functions/$name/static-expression-number-array', + () => static(field('embedding'), const [0.1, 0.2, 0.3]), + ); + r.expr( + 'functions/$name/static-expression-expression', + () => static(field('embedding'), field('other')), + ); + r.expr( + 'functions/$name/method-number-array', + () => method(field('embedding'), const [0.1, 0.2, 0.3]), + ); + r.expr( + 'functions/$name/method-vector-value', + () => method(field('embedding'), _vector), + ); + r.expr( + 'functions/$name/method-expression', + () => method(field('embedding'), field('other')), + ); + } + r.unary( + 'vectorLength', + 'embedding', + PipelineFunctions.vectorLength, + (e) => e.vectorLength(), + ); + + r.expr( + 'functions/geoDistance/static-field-name-geo-point', + () => PipelineFunctions.geoDistance('location', _geoPoint), + ); + r.expr( + 'functions/geoDistance/static-field-geo-point', + () => PipelineFunctions.geoDistance(field('location'), _geoPoint), + ); + r.expr( + 'functions/geoDistance/static-field-name-expression', + () => PipelineFunctions.geoDistance('location', field('other')), + ); + r.expr( + 'functions/geoDistance/method-geo-point', + () => field('location').geoDistance(_geoPoint), + ); + r.expr( + 'functions/geoDistance/method-expression', + () => field('location').geoDistance(field('other')), + ); + + r.unary( + 'collectionId', + '__name__', + PipelineFunctions.collectionId, + (e) => e.collectionId(), + ); + for (final (name, static, method) in [ + ( + 'documentId', + PipelineFunctions.documentId, + (PipelineExpression e) => e.documentId(), + ), + ('parent', PipelineFunctions.parent, (PipelineExpression e) => e.parent()), + ]) { + r.expr('functions/$name/static-path', () => static('books/book1')); + r.dbExpr('functions/$name/static-reference', (db) => static(_docRef(db))); + r.expr( + 'functions/$name/static-expression', + () => static(field('__name__')), + ); + r.expr('functions/$name/method', () => method(field('__name__'))); + } + + r.expr( + 'functions/currentDocument/static', + PipelineFunctions.currentDocument, + [currentDocument], + ); + r.expr( + 'functions/documentMatches/static-literal', + () => PipelineFunctions.documentMatches('breakfast'), + [() => documentMatches('breakfast')], + ); + r.expr( + 'functions/documentMatches/static-expression', + () => PipelineFunctions.documentMatches(field('query')), + [() => documentMatches(field('query'))], + ); + r.expr('functions/score/static', PipelineFunctions.score, [score]); +} + +void _registerAggregates(_Registry r) { + r.aggregate('aggregates/countAll/static', PipelineFunctions.countAll, [ + PipelineFunctions.count, + ]); + for (final (name, static, method) in <(String, _Static1, _Method0)>[ + ('count', PipelineFunctions.count, (e) => e.count()), + ('sum', PipelineFunctions.sum, (e) => e.sum()), + ('average', PipelineFunctions.average, (e) => e.average()), + ('minimum', PipelineFunctions.minimum, (e) => e.minimum()), + ('maximum', PipelineFunctions.maximum, (e) => e.maximum()), + ('first', PipelineFunctions.first, (e) => e.first()), + ('last', PipelineFunctions.last, (e) => e.last()), + ('arrayAgg', PipelineFunctions.arrayAgg, (e) => e.arrayAgg()), + ( + 'arrayAggDistinct', + PipelineFunctions.arrayAggDistinct, + (e) => e.arrayAggDistinct(), + ), + ( + 'countDistinct', + PipelineFunctions.countDistinct, + (e) => e.countDistinct(), + ), + ]) { + r.unary(name, 'rating', static, method, area: 'aggregates'); + } + r.aggregate( + 'aggregates/countIf/static', + () => PipelineFunctions.countIf(_isGood()), + ); + r.aggregate('aggregates/countIf/method', () => _isGood().countIf()); +} diff --git a/packages/google_cloud_firestore/test/pipeline_single_implementation_test.dart b/packages/google_cloud_firestore/test/pipeline_single_implementation_test.dart new file mode 100644 index 00000000..279ebf4a --- /dev/null +++ b/packages/google_cloud_firestore/test/pipeline_single_implementation_test.dart @@ -0,0 +1,1467 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Enforces that every Pipeline function has exactly one implementation. +/// +/// A Pipeline function can be written in several forms: the static +/// `PipelineFunctions.equal('price', 1)`, the fluent `field('price').equal(1)`, +/// a top-level `equal('price', 1)`, and FlutterFire-style `Expression` aliases +/// such as `Expression.field`. When forms were implemented separately they +/// drifted apart (different backend names, argument conversions and optional +/// arguments). So this test resolves `lib/src/pipeline.dart` with +/// `package:analyzer`, groups its public members by name, and requires every +/// form but one to be a single call forwarding its parameters, in order, to +/// that one implementation. It then encodes every pair through each of its +/// forms and checks they send identical requests. +@TestOn('vm') +library; + +import 'dart:isolate'; + +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart' as ast; +import 'package:analyzer/dart/ast/visitor.dart' as ast; +import 'package:analyzer/dart/element/element.dart'; +import 'package:google_cloud_firestore/google_cloud_firestore.dart'; +import 'package:google_cloud_firestore/src/firestore_http_client.dart'; +import 'package:google_cloud_firestore_v1/firestore.dart' as firestore_v1; +import 'package:google_cloud_firestore_v1/testing.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart' hide greaterThan, lessThan; + +/// Pairs that cannot forward to their counterpart, with the reason. +/// +/// Every entry must name a pair that fails the forwarding check; an entry +/// that no longer fails is stale and fails the test. +const _exceptions = { + 'concat': + 'The fluent form keeps the FlutterFire-style `concat(List others)`, ' + "while PipelineFunctions.concat takes Node's `(first, second, [others])`. " + 'It still builds the function only through PipelineFunctions.concat, but ' + 'has to split `others` into `second` and the rest to call it. Making it ' + '`concat(second, [others])` would silently turn existing `concat([x])` ' + 'calls into concatenating the array `[x]`.', + 'ascending': _orderingException, + 'descending': _orderingException, +}; + +const _orderingException = + 'Inside PipelineExpression the name resolves to the fluent method itself, ' + 'so it cannot call the top-level function, and PipelineFunctions has no ' + 'ordering helpers to share. Both forms are a single PipelineOrdering ' + 'constructor call, and _behaviour checks they encode identically.'; + +/// Public fluent members that are not a form of any Pipeline function, so +/// they have nothing to forward to. +const _notFunctions = { + 'as': 'Wraps the expression in a PipelineAliasedExpression.', + 'asBoolean': 'Casts the expression to a PipelineBooleanExpression.', +}; + +void main() { + group( + 'Pipeline functions have a single implementation', + timeout: const Timeout(Duration(minutes: 3)), + () { + late _Inventory inventory; + + setUpAll(() async { + inventory = await _Inventory.load(); + }); + + test('derives the pairs from the library', () { + // Guards against a derivation that silently finds nothing to check. + expect(inventory.pairs.length, greaterThanOrEqualTo(100)); + expect(inventory.pairs['equal']?.map((member) => member.label), [ + 'PipelineFunctions.equal', + 'equal', + 'PipelineExpression.equal', + ]); + expect( + inventory.pairs['not']?.map((member) => member.label), + containsAll([ + 'PipelineFunctions.not', + 'PipelineBooleanExpression.not', + ]), + ); + }); + + test('every form forwards to the one implementation', () { + final problems = []; + for (final MapEntry(key: name, value: members) + in inventory.pairs.entries) { + if (_exceptions.containsKey(name)) continue; + final implementation = members.first; + for (final member in members.skip(1)) { + final problem = _forwardingProblem(member, implementation); + if (problem != null) { + problems.add( + '${member.label} must forward to ${implementation.label}: ' + '$problem.', + ); + } + } + } + expect(problems, isEmpty, reason: problems.join('\n')); + }); + + test('fluent members without a same-named counterpart forward', () { + final problems = []; + for (final member in inventory.unpairedFluent) { + if (_notFunctions.containsKey(member.name)) continue; + final target = inventory.calledMember(member); + final problem = target == null + ? 'its body must be a single call forwarding to a ' + 'PipelineFunctions function or another fluent method, ' + 'but it is `${_source(member.body)}`' + : _forwardingProblem(member, target); + if (problem != null) problems.add('${member.label}: $problem.'); + } + expect(problems, isEmpty, reason: problems.join('\n')); + }); + + test('exceptions are still needed', () { + final stale = []; + for (final name in _exceptions.keys) { + final members = inventory.pairs[name]; + if (members == null) { + stale.add('$name is no longer a pair.'); + continue; + } + final forwards = members + .skip(1) + .every( + (member) => _forwardingProblem(member, members.first) == null, + ); + if (forwards) { + stale.add('$name now forwards; remove it from _exceptions.'); + } + } + for (final name in _notFunctions.keys) { + if (!inventory.unpairedFluent.any((member) => member.name == name)) { + stale.add( + '$name is no longer an unpaired fluent member; remove it from ' + '_notFunctions.', + ); + } + } + expect(stale, isEmpty, reason: stale.join('\n')); + }); + + test('every pair has a behavioural check covering each form', () { + final expected = >{ + for (final MapEntry(:key, :value) in inventory.pairs.entries) + key: {for (final member in value) member.form.name}, + for (final alias in inventory.unpairedFluent) + if (!_notFunctions.containsKey(alias.name)) + alias.name: { + 'alias', + if (inventory.calledMember(alias) case final target?) + target.form.name, + }, + }; + final problems = []; + for (final MapEntry(key: name, value: forms) in expected.entries) { + final calls = _behaviour[name]; + if (calls == null) { + problems.add('$name has no entry in _behaviour.'); + continue; + } + final covered = {for (final call in calls) ...call.forms.keys}; + if (covered.difference(forms) case final extra + when extra.isNotEmpty) { + problems.add('$name has no ${extra.join(', ')} form.'); + } + if (forms.difference(covered) case final missing + when missing.isNotEmpty) { + problems.add( + '_behaviour never calls $name in ${missing.join(', ')} form.', + ); + } + for (final call in calls) { + if (call.forms.length < 2) { + problems.add('$name: "${call.description}" compares one form.'); + } + } + } + for (final name in _behaviour.keys) { + if (!expected.containsKey(name)) { + problems.add('_behaviour.$name is not a pair; remove it.'); + } + } + expect(problems, isEmpty, reason: problems.join('\n')); + }); + }, + ); + + group('every form of a Pipeline function encodes identically', () { + late Firestore firestore; + firestore_v1.ExecutePipelineRequest? captured; + + setUp(() { + final client = _MockFirestoreHttpClient(); + firestore = Firestore.internal( + settings: const Settings(projectId: _projectId, databaseId: 'db'), + client: client, + ); + when(() => client.cachedProjectId).thenReturn(_projectId); + when( + () => client.v1>(any()), + ).thenAnswer((invocation) async { + final callback = + invocation.positionalArguments.single + as Future> + Function(firestore_v1.Firestore api, String projectId); + final api = FakeFirestore( + executePipeline: (request) { + captured = request; + return const Stream.empty(); + }, + ); + return callback(api, _projectId); + }); + }); + + /// The JSON of the request that executes a Pipeline using [built]. + Future encode(Object built) async { + final pipeline = firestore.pipeline().collection('books'); + captured = null; + await switch (built) { + PipelineOrdering() => pipeline.sort([built]), + PipelineExpression() => pipeline.select([built.as('result')]), + _ => throw ArgumentError.value(built, 'built'), + }.execute(); + return captured!.toJson(); + } + + for (final MapEntry(key: name, value: calls) in _behaviour.entries) { + test(name, () async { + for (final call in calls) { + final [(firstForm, firstBuild), ...others] = [ + for (final MapEntry(:key, :value) in call.forms.entries) + (key, value), + ]; + final expected = await encode(firstBuild()); + for (final (form, build) in others) { + expect( + await encode(build()), + expected, + reason: + '$name ${call.description}: the $form form encodes ' + 'differently from the $firstForm form', + ); + } + } + }); + } + }); +} + +/// The forms a Pipeline function is written in. +/// +/// The first form a function has holds its implementation, and the later +/// ones forward to it. The order is the only one that always compiles: a +/// later form can name an earlier one unambiguously, but inside a class a +/// bare name resolves to that class's own member, so `PipelineExpression.not` +/// or `Expression.field` cannot call the top-level `not` or `field`. +enum _Form { + /// `PipelineFunctions.x(...)`. + static, + + /// The FlutterFire-style `Expression.x(...)`. + expression, + + /// A top-level `x(...)`. + topLevel, + + /// `expression.x(...)`, on `PipelineExpression` or one of its subclasses. + fluent, +} + +/// A public member of `lib/src/pipeline.dart`. +final class _Member { + _Member(this.form, this.element, this.body); + + final _Form form; + final ExecutableElement element; + final ast.FunctionBody body; + + String get name => element.name!; + + String get label { + final owner = element.enclosingElement; + return owner is InterfaceElement ? '${owner.name}.$name' : name; + } + + bool isElement(Element? other) { + return other is ExecutableElement && + other.baseElement == element.baseElement; + } +} + +/// The public Pipeline members, grouped by name. +final class _Inventory { + _Inventory(List<_Member> members) { + for (final member in members) { + _byName.putIfAbsent(member.name, () => []).add(member); + } + for (final group in _byName.values) { + group.sort((a, b) => a.form.index.compareTo(b.form.index)); + } + } + + final _byName = >{}; + + /// The names written in more than one form, each with its members, + /// implementation first. + late final Map> pairs = { + for (final MapEntry(:key, :value) in _byName.entries) + if (value.map((member) => member.form).toSet().length > 1) key: value, + }; + + /// Fluent members whose name no other form uses. + late final List<_Member> unpairedFluent = [ + for (final MapEntry(:key, :value) in _byName.entries) + if (!pairs.containsKey(key)) + ...value.where((member) => member.form == _Form.fluent), + ]; + + /// The `PipelineFunctions` or fluent member that [member]'s body calls, if + /// its body is a single call. + _Member? calledMember(_Member member) { + final call = _singleExpression(member.body); + if (call is! ast.MethodInvocation) return null; + final callee = call.methodName.element; + for (final group in _byName.values) { + for (final candidate in group) { + if ((candidate.form == _Form.static || + candidate.form == _Form.fluent) && + candidate.isElement(callee)) { + return candidate; + } + } + } + return null; + } + + static Future<_Inventory> load() async { + final uri = await Isolate.resolvePackageUri( + Uri.parse('package:google_cloud_firestore/src/firestore.dart'), + ); + final libraryPath = uri!.toFilePath(); + final collection = AnalysisContextCollection(includedPaths: [libraryPath]); + try { + final result = await collection + .contextFor(libraryPath) + .currentSession + .getResolvedLibrary(libraryPath); + if (result is! ResolvedLibraryResult) { + throw StateError('Could not resolve $libraryPath: $result'); + } + final pipelinePath = uri.resolve('pipeline.dart').toFilePath(); + final unit = result.units + .singleWhere((unit) => unit.path == pipelinePath) + .unit; + final members = <_Member>[]; + unit.accept(_MemberCollector(members)); + return _Inventory(members); + } finally { + await collection.dispose(); + } + } +} + +/// Collects the public functions and methods that are forms of a Pipeline +/// function. +final class _MemberCollector extends ast.RecursiveAstVisitor { + _MemberCollector(this._members); + + final List<_Member> _members; + + @override + void visitFunctionDeclaration(ast.FunctionDeclaration node) { + final element = node.declaredFragment?.element; + if (node.parent is ast.CompilationUnit && + element is TopLevelFunctionElement && + element.isPublic) { + _members.add( + _Member(_Form.topLevel, element, node.functionExpression.body), + ); + } + } + + @override + void visitMethodDeclaration(ast.MethodDeclaration node) { + final element = node.declaredFragment?.element; + if (element is! MethodElement || !element.isPublic) return; + final owner = element.enclosingElement; + if (owner is! ClassElement || !owner.isPublic) return; + + final form = switch (owner.name) { + 'PipelineFunctions' when element.isStatic => _Form.static, + 'Expression' when element.isStatic => _Form.expression, + _ when !element.isStatic && _isPipelineExpression(owner) => _Form.fluent, + _ => null, + }; + if (form != null) _members.add(_Member(form, element, node.body)); + } + + static bool _isPipelineExpression(ClassElement element) { + return element.name == 'PipelineExpression' || + element.allSupertypes.any( + (type) => type.element.name == 'PipelineExpression', + ); + } +} + +/// Explains why [member] is not a single call forwarding its parameters to +/// [target], or returns null when it is. +/// +/// The call must pass `this` first when a fluent member forwards to a static +/// or top-level one, followed by each of [member]'s parameters, unchanged and +/// in order. A counterpart that takes its operands as one `Iterable` may +/// receive them packed, in the same order, into a list literal such as +/// `[this, second, ...others]`. Each parameter passed straight through must +/// match its counterpart's optionality and default value, and the counterpart +/// must not take parameters that [member] leaves out. +String? _forwardingProblem(_Member member, _Member target) { + final call = _singleExpression(member.body); + if (call is! ast.MethodInvocation || + !target.isElement(call.methodName.element)) { + return 'its body must be a single call to ${target.label}, but it is ' + '`${_source(member.body)}`'; + } + if (target.form == _Form.fluent && + call.target != null && + call.target is! ast.ThisExpression) { + return 'it must call ${target.label} on `this`'; + } + + final parameters = member.element.formalParameters; + FormalParameterElement? parameterOf(ast.AstNode node) { + if (node is! ast.SimpleIdentifier) return null; + final element = node.element; + return element is FormalParameterElement + ? _matching(element, parameters) + : null; + } + + final targetParameters = target.element.formalParameters; + final bound = {}; + final passed = []; + + for (final argument in call.argumentList.arguments) { + final targetParameter = _matching( + argument.correspondingParameter, + targetParameters, + ); + if (targetParameter == null) { + return 'argument `${argument.toSource()}` does not bind to a parameter ' + 'of ${target.label}'; + } + bound.add(targetParameter); + + // A named argument is `name: value`, whose value is its last child. This + // holds across analyzer versions, which model named arguments as + // different node types. + final value = targetParameter.isNamed + ? argument.childEntities.last + : argument; + + if (value is ast.ThisExpression) { + passed.add('this'); + } else if (value is ast.ListLiteral) { + for (final element in value.elements) { + final packed = switch (element) { + ast.ThisExpression() => 'this', + ast.SpreadElement(isNullAware: false, :final expression) => + parameterOf(expression)?.name, + _ => switch (parameterOf(element)) { + final parameter? when parameter.isRequiredPositional => + parameter.name, + _ => null, + }, + }; + if (packed == null) { + return 'list argument element `${element.toSource()}` must be ' + '`this`, a required parameter, or a spread parameter'; + } + passed.add(packed); + } + } else { + final parameter = value is ast.AstNode ? parameterOf(value) : null; + if (parameter == null || + (targetParameter.isNamed && parameter.name != targetParameter.name)) { + return 'argument `${argument.toSource()}` must pass one of its ' + 'parameters through unchanged'; + } + final mismatch = _parameterMismatch(parameter, targetParameter, target); + if (mismatch != null) return mismatch; + passed.add(parameter.name!); + } + } + + final dropped = [ + for (final parameter in targetParameters) + if (!bound.contains(parameter)) parameter.name, + ]; + if (dropped.isNotEmpty) { + return '${target.label} also takes ${dropped.join(', ')}, which it does ' + 'not expose'; + } + + final expected = [ + if (member.form == _Form.fluent && target.form != _Form.fluent) 'this', + for (final parameter in parameters) parameter.name!, + ]; + if (passed.join(', ') != expected.join(', ')) { + return 'it passes (${passed.join(', ')}) but must pass ' + '(${expected.join(', ')}), in that order'; + } + return null; +} + +/// The parameter of [parameters] that [parameter] refers to. +FormalParameterElement? _matching( + FormalParameterElement? parameter, + List parameters, +) { + for (final candidate in parameters) { + if (candidate.baseElement == parameter?.baseElement) return candidate; + } + return null; +} + +/// Explains how [parameter] differs from the [targetParameter] of [target] it +/// is passed to, or returns null when they match. +String? _parameterMismatch( + FormalParameterElement parameter, + FormalParameterElement targetParameter, + _Member target, +) { + String kind(FormalParameterElement parameter) { + if (parameter.isRequiredPositional) return 'required'; + if (parameter.isOptionalPositional) return 'optional'; + return parameter.isRequiredNamed ? 'required named' : 'optional named'; + } + + if (kind(parameter) != kind(targetParameter)) { + return '`${parameter.name}` is ${kind(parameter)} but ' + '${target.label}\'s `${targetParameter.name}` is ' + '${kind(targetParameter)}'; + } + if (parameter.defaultValueCode != targetParameter.defaultValueCode) { + return '`${parameter.name}` defaults to `${parameter.defaultValueCode}` ' + 'but ${target.label}\'s `${targetParameter.name}` defaults to ' + '`${targetParameter.defaultValueCode}`'; + } + return null; +} + +/// The expression [body] evaluates, when it is `=> e` or `{ return e; }`. +ast.Expression? _singleExpression(ast.FunctionBody body) { + return switch (body) { + ast.ExpressionFunctionBody(:final expression) => expression, + ast.BlockFunctionBody( + block: ast.Block(statements: [ast.ReturnStatement(:final expression)]), + ) => + expression, + _ => null, + }; +} + +String _source(ast.AstNode node) { + return node.toSource().replaceAll(RegExp(r'\s+'), ' '); +} + +const _projectId = 'test-project'; + +class _MockFirestoreHttpClient extends Mock implements FirestoreHttpClient {} + +/// One call to a Pipeline function, written in several of its forms. +/// +/// [forms] maps each form's name (a [_Form] name, or `alias` for a fluent +/// member forwarding to a differently named one) to a builder returning the +/// [PipelineExpression] or [PipelineOrdering] that form produces. +final class _Call { + _Call(this.description, this.forms); + + final String description; + final Map forms; +} + +/// A receiver that is an expression rather than a plain field. +final _expression = field('score').ifAbsent(0); + +/// A boolean receiver. +final _condition = field('score').greaterThan(10); + +/// [description] called on a field name and on an expression. +/// +/// The static and top-level forms take the receiver as their first argument; +/// the fluent and alias forms are called on it. Pass [fieldName] false for +/// functions whose first argument is a value rather than a field. +List<_Call> _on( + String description, { + Object Function(Object? receiver)? static, + Object Function(Object? receiver)? topLevel, + Object Function(PipelineExpression receiver)? fluent, + Object Function(PipelineExpression receiver)? alias, + bool fieldName = true, +}) { + _Call call(String on, Object? argument, PipelineExpression receiver) { + return _Call('$description on $on', { + if (static case final build?) 'static': () => build(argument), + if (topLevel case final build?) 'topLevel': () => build(argument), + if (fluent case final build?) 'fluent': () => build(receiver), + if (alias case final build?) 'alias': () => build(receiver), + }); + } + + return [ + if (fieldName) call('a field name', 'score', field('score')), + call('an expression', _expression, _expression), + ]; +} + +/// A function of its receiver alone. +List<_Call> _unary( + Object Function(Object? receiver) static, + Object Function(PipelineExpression receiver) fluent, { + bool fieldName = true, +}) { + return _on('called', static: static, fluent: fluent, fieldName: fieldName); +} + +/// A function of its receiver and one argument, called with each of +/// [arguments]. +List<_Call> _binary( + Object Function(Object? receiver, Object? argument) static, + Object Function(PipelineExpression receiver, Object? argument) fluent, { + Object Function(Object? receiver, Object? argument)? topLevel, + List? arguments, + bool fieldName = true, +}) { + return [ + for (final argument in arguments ?? [2, 'text', field('other')]) + ..._on( + 'with $argument', + static: (receiver) => static(receiver, argument), + topLevel: topLevel == null + ? null + : (receiver) => topLevel(receiver, argument), + fluent: (receiver) => fluent(receiver, argument), + fieldName: fieldName, + ), + ]; +} + +/// A comparison, which also has a top-level form. +List<_Call> _comparison( + Object Function(Object? left, Object? right) static, + Object Function(Object? left, Object? right) topLevel, + Object Function(PipelineExpression left, Object? right) fluent, +) { + return _binary( + static, + fluent, + topLevel: topLevel, + arguments: [2, 'text', field('other'), null], + ); +} + +/// A function of a boolean receiver. +List<_Call> _onCondition( + String description, { + required Object Function(PipelineBooleanExpression condition) static, + Object Function(PipelineBooleanExpression condition)? topLevel, + required Object Function(PipelineBooleanExpression condition) fluent, +}) { + return [ + _Call(description, { + 'static': () => static(_condition), + if (topLevel case final build?) 'topLevel': () => build(_condition), + 'fluent': () => fluent(_condition), + }), + ]; +} + +/// A list holding an expression, sent as an `array(...)` function. +final _mixedList = [1, field('other')]; + +/// Every Pipeline function pair and fluent alias, written in each form. +final _behaviour = >{ + // Comparisons. + 'equal': _comparison(PipelineFunctions.equal, equal, (e, v) => e.equal(v)), + 'notEqual': _comparison( + PipelineFunctions.notEqual, + notEqual, + (e, v) => e.notEqual(v), + ), + 'lessThan': _comparison( + PipelineFunctions.lessThan, + lessThan, + (e, v) => e.lessThan(v), + ), + 'lessThanOrEqual': _comparison( + PipelineFunctions.lessThanOrEqual, + lessThanOrEqual, + (e, v) => e.lessThanOrEqual(v), + ), + 'greaterThan': _comparison( + PipelineFunctions.greaterThan, + greaterThan, + (e, v) => e.greaterThan(v), + ), + 'greaterThanOrEqual': _comparison( + PipelineFunctions.greaterThanOrEqual, + greaterThanOrEqual, + (e, v) => e.greaterThanOrEqual(v), + ), + 'equalAny': _binary( + PipelineFunctions.equalAny, + (e, v) => e.equalAny(v), + arguments: [ + [1, 2], + _mixedList, + field('options'), + ], + ), + 'notEqualAny': _binary( + PipelineFunctions.notEqualAny, + (e, v) => e.notEqualAny(v), + arguments: [ + [1, 2], + _mixedList, + field('options'), + ], + ), + + // Logic. + 'and': [ + _Call('of two conditions', { + 'static': () => PipelineFunctions.and([_condition, field('b').exists()]), + 'topLevel': () => and([_condition, field('b').exists()]), + }), + ], + 'or': [ + _Call('of two conditions', { + 'static': () => PipelineFunctions.or([_condition, field('b').exists()]), + 'topLevel': () => or([_condition, field('b').exists()]), + }), + ], + 'not': _onCondition( + 'of a condition', + static: PipelineFunctions.not, + topLevel: not, + fluent: (c) => c.not(), + ), + 'countIf': _onCondition( + 'of a condition', + static: PipelineFunctions.countIf, + fluent: (c) => c.countIf(), + ), + 'conditional': [ + ..._onCondition( + 'with literals', + static: (c) => PipelineFunctions.conditional(c, 1, 'text'), + fluent: (c) => c.conditional(1, 'text'), + ), + ..._onCondition( + 'with expressions', + static: (c) => PipelineFunctions.conditional(c, field('a'), _mixedList), + fluent: (c) => c.conditional(field('a'), _mixedList), + ), + ], + 'exists': _unary(PipelineFunctions.exists, (e) => e.exists()), + 'isAbsent': _unary(PipelineFunctions.isAbsent, (e) => e.isAbsent()), + 'isError': _unary( + PipelineFunctions.isError, + (e) => e.isError(), + fieldName: false, + ), + 'ifAbsent': _binary(PipelineFunctions.ifAbsent, (e, v) => e.ifAbsent(v)), + 'ifNull': _binary(PipelineFunctions.ifNull, (e, v) => e.ifNull(v)), + 'ifError': _binary( + PipelineFunctions.ifError, + (e, v) => e.ifError(v), + fieldName: false, + ), + 'coalesce': [ + ..._on( + 'with one replacement', + static: (r) => PipelineFunctions.coalesce(r, 0), + fluent: (e) => e.coalesce(0), + ), + ..._on( + 'with more replacements', + static: (r) => PipelineFunctions.coalesce(r, field('b'), [0, 'text']), + fluent: (e) => e.coalesce(field('b'), [0, 'text']), + ), + ], + 'logicalMaximum': [ + ..._on( + 'with two operands', + static: (r) => PipelineFunctions.logicalMaximum(r, 1), + fluent: (e) => e.logicalMaximum(1), + ), + ..._on( + 'with more operands', + static: (r) => PipelineFunctions.logicalMaximum(r, 1, [field('b'), 3]), + fluent: (e) => e.logicalMaximum(1, [field('b'), 3]), + ), + ], + 'logicalMinimum': [ + ..._on( + 'with two operands', + static: (r) => PipelineFunctions.logicalMinimum(r, 1), + fluent: (e) => e.logicalMinimum(1), + ), + ..._on( + 'with more operands', + static: (r) => PipelineFunctions.logicalMinimum(r, 1, [field('b'), 3]), + fluent: (e) => e.logicalMinimum(1, [field('b'), 3]), + ), + ], + + // Arithmetic. + 'add': [ + ..._binary(PipelineFunctions.add, (e, v) => e.add(v)), + ..._on( + 'with more operands', + static: (r) => PipelineFunctions.add(r, 1, [field('b'), 3]), + fluent: (e) => e.add(1, [field('b'), 3]), + ), + ], + 'subtract': _binary(PipelineFunctions.subtract, (e, v) => e.subtract(v)), + 'multiply': [ + ..._binary(PipelineFunctions.multiply, (e, v) => e.multiply(v)), + ..._on( + 'with more operands', + static: (r) => PipelineFunctions.multiply(r, 2, [field('b'), 3]), + fluent: (e) => e.multiply(2, [field('b'), 3]), + ), + ], + 'divide': _binary(PipelineFunctions.divide, (e, v) => e.divide(v)), + 'mod': _binary(PipelineFunctions.mod, (e, v) => e.mod(v)), + 'pow': _binary(PipelineFunctions.pow, (e, v) => e.pow(v)), + 'abs': _unary(PipelineFunctions.abs, (e) => e.abs()), + 'ceil': _unary(PipelineFunctions.ceil, (e) => e.ceil()), + 'floor': _unary(PipelineFunctions.floor, (e) => e.floor()), + 'sqrt': _unary(PipelineFunctions.sqrt, (e) => e.sqrt()), + 'exp': _unary(PipelineFunctions.exp, (e) => e.exp()), + 'ln': _unary(PipelineFunctions.ln, (e) => e.ln()), + 'log10': _unary(PipelineFunctions.log10, (e) => e.log10()), + 'log': [ + ..._unary(PipelineFunctions.log, (e) => e.log()), + ..._binary(PipelineFunctions.log, (e, v) => e.log(v)), + ], + 'round': [ + ..._unary(PipelineFunctions.round, (e) => e.round()), + ..._binary(PipelineFunctions.round, (e, v) => e.round(v)), + ], + 'trunc': [ + ..._unary(PipelineFunctions.trunc, (e) => e.trunc()), + ..._binary(PipelineFunctions.trunc, (e, v) => e.trunc(v)), + ], + + // Aggregates. + 'count': _unary(PipelineFunctions.count, (e) => e.count()), + 'countDistinct': _unary( + PipelineFunctions.countDistinct, + (e) => e.countDistinct(), + ), + 'sum': _unary(PipelineFunctions.sum, (e) => e.sum()), + 'average': _unary(PipelineFunctions.average, (e) => e.average()), + 'minimum': _unary(PipelineFunctions.minimum, (e) => e.minimum()), + 'maximum': _unary(PipelineFunctions.maximum, (e) => e.maximum()), + 'first': _unary(PipelineFunctions.first, (e) => e.first()), + 'last': _unary(PipelineFunctions.last, (e) => e.last()), + 'arrayAgg': _unary(PipelineFunctions.arrayAgg, (e) => e.arrayAgg()), + 'arrayAggDistinct': _unary( + PipelineFunctions.arrayAggDistinct, + (e) => e.arrayAggDistinct(), + ), + + // Arrays. + 'array': [ + _Call('of literals and expressions', { + 'static': () => PipelineFunctions.array(_mixedList), + 'expression': () => Expression.array(_mixedList), + }), + ], + 'arrayConcat': _binary( + (r, v) => PipelineFunctions.arrayConcat([r, v]), + (e, v) => e.arrayConcat(v), + arguments: [ + ['text'], + _mixedList, + field('other'), + ], + ), + 'arrayConcatMultiple': _on( + 'with two arrays', + static: (r) => PipelineFunctions.arrayConcat([r, _mixedList, field('b')]), + alias: (e) => e.arrayConcatMultiple([_mixedList, field('b')]), + ), + 'arrayContains': _binary( + PipelineFunctions.arrayContains, + (e, v) => e.arrayContains(v), + ), + 'arrayContainsAll': _binary( + PipelineFunctions.arrayContainsAll, + (e, v) => e.arrayContainsAll(v), + arguments: [ + ['a', 'b'], + _mixedList, + field('wanted'), + ], + ), + 'arrayContainsAllFrom': _on( + 'with an array expression', + static: (r) => PipelineFunctions.arrayContainsAll(r, field('wanted')), + alias: (e) => e.arrayContainsAllFrom(field('wanted')), + ), + 'arrayContainsAny': _binary( + PipelineFunctions.arrayContainsAny, + (e, v) => e.arrayContainsAny(v), + arguments: [ + ['a', 'b'], + _mixedList, + field('wanted'), + ], + ), + 'arrayFilter': _on( + 'with a predicate', + static: (r) => PipelineFunctions.arrayFilter( + r, + 'tag', + variable('tag').notEqual('draft'), + ), + fluent: (e) => e.arrayFilter('tag', variable('tag').notEqual('draft')), + ), + 'arrayTransform': _on( + 'with a transform', + static: (r) => + PipelineFunctions.arrayTransform(r, 'n', variable('n').add(1)), + fluent: (e) => e.arrayTransform('n', variable('n').add(1)), + ), + 'arrayTransformWithIndex': _on( + 'with a transform', + static: (r) => PipelineFunctions.arrayTransformWithIndex( + r, + 'n', + 'i', + variable('n').add(variable('i')), + ), + fluent: (e) => + e.arrayTransformWithIndex('n', 'i', variable('n').add(variable('i'))), + ), + 'arrayGet': _binary(PipelineFunctions.arrayGet, (e, v) => e.arrayGet(v)), + 'arrayFirst': _unary(PipelineFunctions.arrayFirst, (e) => e.arrayFirst()), + 'arrayFirstN': _binary( + PipelineFunctions.arrayFirstN, + (e, v) => e.arrayFirstN(v), + ), + 'arrayLast': _unary(PipelineFunctions.arrayLast, (e) => e.arrayLast()), + 'arrayLastN': _binary( + PipelineFunctions.arrayLastN, + (e, v) => e.arrayLastN(v), + ), + 'arrayIndexOf': _binary( + PipelineFunctions.arrayIndexOf, + (e, v) => e.arrayIndexOf(v), + ), + 'arrayIndexOfAll': _binary( + PipelineFunctions.arrayIndexOfAll, + (e, v) => e.arrayIndexOfAll(v), + ), + 'arrayLastIndexOf': _binary( + PipelineFunctions.arrayLastIndexOf, + (e, v) => e.arrayLastIndexOf(v), + ), + 'arrayLength': _unary(PipelineFunctions.arrayLength, (e) => e.arrayLength()), + 'arrayReverse': _unary( + PipelineFunctions.arrayReverse, + (e) => e.arrayReverse(), + ), + 'arrayMaximum': _unary( + PipelineFunctions.arrayMaximum, + (e) => e.arrayMaximum(), + ), + 'arrayMaximumN': _binary( + PipelineFunctions.arrayMaximumN, + (e, v) => e.arrayMaximumN(v), + ), + 'arrayMinimum': _unary( + PipelineFunctions.arrayMinimum, + (e) => e.arrayMinimum(), + ), + 'arrayMinimumN': _binary( + PipelineFunctions.arrayMinimumN, + (e, v) => e.arrayMinimumN(v), + ), + 'arraySum': _unary(PipelineFunctions.arraySum, (e) => e.arraySum()), + 'arraySlice': [ + ..._binary(PipelineFunctions.arraySlice, (e, v) => e.arraySlice(v)), + ..._on( + 'with a length', + static: (r) => PipelineFunctions.arraySlice(r, 1, 2), + fluent: (e) => e.arraySlice(1, 2), + ), + ..._on( + 'with expressions', + static: (r) => PipelineFunctions.arraySlice(r, field('o'), field('l')), + fluent: (e) => e.arraySlice(field('o'), field('l')), + ), + ], + + // Maps. + 'mapGet': _binary(PipelineFunctions.mapGet, (e, v) => e.mapGet(v)), + 'mapGetLiteral': _on( + 'with a key', + fluent: (e) => e.mapGet('key'), + alias: (e) => e.mapGetLiteral('key'), + fieldName: false, + ), + 'getField': _binary(PipelineFunctions.getField, (e, v) => e.getField(v)), + 'mapSet': [ + ..._on( + 'with one entry', + static: (r) => PipelineFunctions.mapSet(r, ['key', 1]), + fluent: (e) => e.mapSet('key', 1), + ), + ..._on( + 'with more entries', + static: (r) => PipelineFunctions.mapSet(r, [ + field('key'), + _mixedList, + 'other', + {'a': field('a')}, + ]), + fluent: (e) => e.mapSet(field('key'), _mixedList, [ + 'other', + {'a': field('a')}, + ]), + ), + ], + 'mapRemove': _binary( + PipelineFunctions.mapRemove, + (e, v) => e.mapRemove(v), + arguments: ['key', field('key')], + ), + 'mapMerge': _on( + 'with two maps', + static: (r) => PipelineFunctions.mapMerge([ + r, + {'a': 1}, + {'b': field('b')}, + ]), + fluent: (e) => e.mapMerge([ + {'a': 1}, + {'b': field('b')}, + ]), + ), + 'mapKeys': _unary(PipelineFunctions.mapKeys, (e) => e.mapKeys()), + 'mapValues': _unary(PipelineFunctions.mapValues, (e) => e.mapValues()), + 'mapEntries': _unary(PipelineFunctions.mapEntries, (e) => e.mapEntries()), + + // Strings. + 'join': _binary(PipelineFunctions.join, (e, v) => e.join(v)), + 'joinLiteral': _on( + 'with a delimiter', + fluent: (e) => e.join(', '), + alias: (e) => e.joinLiteral(', '), + fieldName: false, + ), + 'split': _binary(PipelineFunctions.split, (e, v) => e.split(v)), + 'splitLiteral': _on( + 'with a delimiter', + fluent: (e) => e.split(','), + alias: (e) => e.splitLiteral(','), + fieldName: false, + ), + 'byteLength': _unary(PipelineFunctions.byteLength, (e) => e.byteLength()), + 'charLength': _unary(PipelineFunctions.charLength, (e) => e.charLength()), + 'length': _unary(PipelineFunctions.length, (e) => e.length()), + 'reverse': _unary(PipelineFunctions.reverse, (e) => e.reverse()), + 'stringReverse': _unary( + PipelineFunctions.stringReverse, + (e) => e.stringReverse(), + ), + 'concat': [ + ..._on( + 'with one operand', + static: (r) => PipelineFunctions.concat(r, field('b')), + fluent: (e) => e.concat([field('b')]), + ), + ..._on( + 'with more operands', + static: (r) => PipelineFunctions.concat(r, ' ', [_mixedList]), + fluent: (e) => e.concat([' ', _mixedList]), + ), + ], + 'stringConcat': _on( + 'with two operands', + static: (r) => PipelineFunctions.stringConcat([r, ' ', field('b')]), + fluent: (e) => e.stringConcat([' ', field('b')]), + ), + 'toLowerCase': _on( + 'called', + static: PipelineFunctions.toLower, + alias: (e) => e.toLowerCase(), + ), + 'toUpperCase': _on( + 'called', + static: PipelineFunctions.toUpper, + alias: (e) => e.toUpperCase(), + ), + 'trim': [ + ..._unary(PipelineFunctions.trim, (e) => e.trim()), + ..._binary(PipelineFunctions.trim, (e, v) => e.trim(v)), + ], + 'ltrim': [ + ..._unary(PipelineFunctions.ltrim, (e) => e.ltrim()), + ..._binary(PipelineFunctions.ltrim, (e, v) => e.ltrim(v)), + ], + 'rtrim': [ + ..._unary(PipelineFunctions.rtrim, (e) => e.rtrim()), + ..._binary(PipelineFunctions.rtrim, (e, v) => e.rtrim(v)), + ], + 'stringIndexOf': _binary( + PipelineFunctions.stringIndexOf, + (e, v) => e.stringIndexOf(v), + ), + 'stringContains': _binary( + PipelineFunctions.stringContains, + (e, v) => e.stringContains(v), + ), + 'stringRepeat': _binary( + PipelineFunctions.stringRepeat, + (e, v) => e.stringRepeat(v), + ), + 'stringReplaceAll': [ + ..._on( + 'with literals', + static: (r) => PipelineFunctions.stringReplaceAll(r, 'a', 'b'), + fluent: (e) => e.stringReplaceAll('a', 'b'), + ), + ..._on( + 'with expressions', + static: (r) => + PipelineFunctions.stringReplaceAll(r, field('a'), field('b')), + fluent: (e) => e.stringReplaceAll(field('a'), field('b')), + ), + ], + 'stringReplaceAllLiteral': _on( + 'with literals', + fluent: (e) => e.stringReplaceAll('a', 'b'), + alias: (e) => e.stringReplaceAllLiteral('a', 'b'), + fieldName: false, + ), + 'stringReplaceOne': [ + ..._on( + 'with literals', + static: (r) => PipelineFunctions.stringReplaceOne(r, 'a', 'b'), + fluent: (e) => e.stringReplaceOne('a', 'b'), + ), + ..._on( + 'with expressions', + static: (r) => + PipelineFunctions.stringReplaceOne(r, field('a'), field('b')), + fluent: (e) => e.stringReplaceOne(field('a'), field('b')), + ), + ], + 'stringReplaceOneLiteral': _on( + 'with literals', + fluent: (e) => e.stringReplaceOne('a', 'b'), + alias: (e) => e.stringReplaceOneLiteral('a', 'b'), + fieldName: false, + ), + 'substring': [ + ..._binary(PipelineFunctions.substring, (e, v) => e.substring(v)), + ..._on( + 'with a length', + static: (r) => PipelineFunctions.substring(r, 1, 3), + fluent: (e) => e.substring(1, 3), + ), + ..._on( + 'with expressions', + static: (r) => PipelineFunctions.substring(r, field('p'), field('l')), + fluent: (e) => e.substring(field('p'), field('l')), + ), + ], + 'substringLiteral': [ + ..._on( + 'with a position', + fluent: (e) => e.substring(1), + alias: (e) => e.substringLiteral(1), + fieldName: false, + ), + ..._on( + 'with a length', + fluent: (e) => e.substring(1, 3), + alias: (e) => e.substringLiteral(1, 3), + fieldName: false, + ), + ], + 'startsWith': _binary( + PipelineFunctions.startsWith, + (e, v) => e.startsWith(v), + ), + 'endsWith': _binary(PipelineFunctions.endsWith, (e, v) => e.endsWith(v)), + 'like': _binary(PipelineFunctions.like, (e, v) => e.like(v)), + 'regexContains': _binary( + PipelineFunctions.regexContains, + (e, v) => e.regexContains(v), + ), + 'regexMatch': _binary( + PipelineFunctions.regexMatch, + (e, v) => e.regexMatch(v), + ), + 'regexFind': _binary(PipelineFunctions.regexFind, (e, v) => e.regexFind(v)), + 'regexFindAll': _binary( + PipelineFunctions.regexFindAll, + (e, v) => e.regexFindAll(v), + ), + + // Timestamps. + 'timestampTruncate': [ + ..._binary( + PipelineFunctions.timestampTruncate, + (e, v) => e.timestampTruncate(v), + arguments: ['day', field('granularity')], + ), + ..._on( + 'with a timezone', + static: (r) => PipelineFunctions.timestampTruncate(r, 'day', 'UTC'), + fluent: (e) => e.timestampTruncate('day', 'UTC'), + ), + ], + 'timestampExtract': [ + ..._binary( + PipelineFunctions.timestampExtract, + (e, v) => e.timestampExtract(v), + arguments: ['year', field('part')], + ), + ..._on( + 'with a timezone', + static: (r) => PipelineFunctions.timestampExtract(r, 'year', 'UTC'), + fluent: (e) => e.timestampExtract('year', 'UTC'), + ), + ], + 'timestampAdd': [ + ..._on( + 'with literals', + static: (r) => PipelineFunctions.timestampAdd(r, 'day', 1), + fluent: (e) => e.timestampAdd('day', 1), + ), + ..._on( + 'with expressions', + static: (r) => + PipelineFunctions.timestampAdd(r, field('unit'), field('amount')), + fluent: (e) => e.timestampAdd(field('unit'), field('amount')), + ), + ], + 'timestampSubtract': [ + ..._on( + 'with literals', + static: (r) => PipelineFunctions.timestampSubtract(r, 'day', 1), + fluent: (e) => e.timestampSubtract('day', 1), + ), + ..._on( + 'with expressions', + static: (r) => PipelineFunctions.timestampSubtract( + r, + field('unit'), + field('amount'), + ), + fluent: (e) => e.timestampSubtract(field('unit'), field('amount')), + ), + ], + 'timestampDiff': [ + ..._on( + 'with a start field name', + static: (r) => PipelineFunctions.timestampDiff(r, 'createdAt', 'day'), + fluent: (e) => e.timestampDiff('createdAt', 'day'), + ), + ..._on( + 'with expressions', + static: (r) => + PipelineFunctions.timestampDiff(r, _expression, field('unit')), + fluent: (e) => e.timestampDiff(_expression, field('unit')), + ), + ], + 'timestampToUnixMicros': _unary( + PipelineFunctions.timestampToUnixMicros, + (e) => e.timestampToUnixMicros(), + ), + 'timestampToUnixMillis': _unary( + PipelineFunctions.timestampToUnixMillis, + (e) => e.timestampToUnixMillis(), + ), + 'timestampToUnixSeconds': _unary( + PipelineFunctions.timestampToUnixSeconds, + (e) => e.timestampToUnixSeconds(), + ), + 'unixMicrosToTimestamp': _unary( + PipelineFunctions.unixMicrosToTimestamp, + (e) => e.unixMicrosToTimestamp(), + ), + 'unixMillisToTimestamp': _unary( + PipelineFunctions.unixMillisToTimestamp, + (e) => e.unixMillisToTimestamp(), + ), + 'unixSecondsToTimestamp': _unary( + PipelineFunctions.unixSecondsToTimestamp, + (e) => e.unixSecondsToTimestamp(), + ), + + // References. + 'collectionId': _unary( + PipelineFunctions.collectionId, + (e) => e.collectionId(), + ), + 'documentId': _unary( + PipelineFunctions.documentId, + (e) => e.documentId(), + fieldName: false, + ), + 'parent': _unary( + PipelineFunctions.parent, + (e) => e.parent(), + fieldName: false, + ), + 'referenceSlice': [ + ..._on( + 'with literals', + static: (r) => PipelineFunctions.referenceSlice(r, 0, 2), + fluent: (e) => e.referenceSlice(0, 2), + ), + ..._on( + 'with expressions', + static: (r) => + PipelineFunctions.referenceSlice(r, field('o'), field('l')), + fluent: (e) => e.referenceSlice(field('o'), field('l')), + ), + ], + + // Types. + 'type': _unary(PipelineFunctions.type, (e) => e.type()), + 'isType': _binary( + PipelineFunctions.isType, + (e, v) => e.isType(v), + arguments: ['string', PipelineValueType.double], + ), + + // Vectors and geo points. + 'cosineDistance': _binary( + PipelineFunctions.cosineDistance, + (e, v) => e.cosineDistance(v), + arguments: _vectors, + ), + 'dotProduct': _binary( + PipelineFunctions.dotProduct, + (e, v) => e.dotProduct(v), + arguments: _vectors, + ), + 'euclideanDistance': _binary( + PipelineFunctions.euclideanDistance, + (e, v) => e.euclideanDistance(v), + arguments: _vectors, + ), + 'vectorLength': _unary( + PipelineFunctions.vectorLength, + (e) => e.vectorLength(), + ), + 'geoDistance': _binary( + PipelineFunctions.geoDistance, + (e, v) => e.geoDistance(v), + arguments: [GeoPoint(latitude: 1, longitude: 2), field('location')], + ), + + // Expressions with no receiver. + 'field': [ + _Call('of a path', { + 'expression': () => Expression.field('metadata.lang'), + 'topLevel': () => field('metadata.lang'), + }), + ], + 'constant': [ + for (final value in [ + 1, + 'text', + null, + [1, 2], + ]) + _Call('of $value', { + 'expression': () => Expression.constant(value), + 'topLevel': () => constant(value), + }), + ], + 'variable': [ + _Call('of a name', { + 'expression': () => Expression.variable('tag'), + 'topLevel': () => variable('tag'), + }), + ], + 'raw': [ + _Call('without options', { + 'static': () => PipelineFunctions.raw('fn', [1, field('a')]), + 'expression': () => Expression.raw('fn', [1, field('a')]), + }), + _Call('with options', { + 'static': () => + PipelineFunctions.raw('fn', [field('a')], options: {'mode': 'fast'}), + 'expression': () => + Expression.raw('fn', [field('a')], options: {'mode': 'fast'}), + }), + ], + 'currentDocument': [ + _Call('called', { + 'static': PipelineFunctions.currentDocument, + 'topLevel': currentDocument, + }), + ], + 'score': [ + _Call('called', {'static': PipelineFunctions.score, 'topLevel': score}), + ], + 'documentMatches': [ + for (final query in ['dart', field('query')]) + _Call('with $query', { + 'static': () => PipelineFunctions.documentMatches(query), + 'topLevel': () => documentMatches(query), + }), + ], + + // Orderings. + 'ascending': _on( + 'called', + topLevel: (r) => ascending(r!), + fluent: (e) => e.ascending(), + ), + 'descending': _on( + 'called', + topLevel: (r) => descending(r!), + fluent: (e) => e.descending(), + ), +}; + +/// Vector arguments: a list of numbers, a vector value and an expression. +final _vectors = [ + [1.0, 2.0], + FieldValue.vector([1.0, 2.0]), + field('other'), +]; diff --git a/packages/google_cloud_firestore/test/pipeline_test.dart b/packages/google_cloud_firestore/test/pipeline_test.dart index 08d553e4..eaa77a40 100644 --- a/packages/google_cloud_firestore/test/pipeline_test.dart +++ b/packages/google_cloud_firestore/test/pipeline_test.dart @@ -357,6 +357,161 @@ void main() { expect(snapshot.results.single.ref, isNull); }); + group('reference values in results', () { + const documents = 'projects/$_projectId/databases/enterprise/documents'; + // `parent()` of a top-level document returns the database root, and a + // reference can also name a collection; neither is a document. + const root = documents; + const collection = '$documents/books'; + const subcollection = '$documents/books/book-1/chapters'; + const document = '$documents/books/book-1'; + const subcollectionDocument = '$documents/books/book-1/chapters/c1'; + + firestore_v1.Value ref(String name) { + return firestore_v1.Value(referenceValue: name); + } + + firestore_v1.Value array(List values) { + return firestore_v1.Value( + arrayValue: firestore_v1.ArrayValue(values: values), + ); + } + + firestore_v1.Value map(Map fields) { + return firestore_v1.Value( + mapValue: firestore_v1.MapValue(fields: fields), + ); + } + + Future decode(Map fields) { + when( + () => mockClient.v1>( + any(), + ), + ).thenAnswer((invocation) async { + final callback = + invocation.positionalArguments.single + as Future> + Function(firestore_v1.Firestore api, String projectId); + return callback( + FakeFirestore( + executePipeline: (_) => Stream.value( + firestore_v1.ExecutePipelineResponse( + results: [firestore_v1.Document(fields: fields)], + ), + ), + ), + _projectId, + ); + }); + + return firestore + .pipeline() + .collection('books') + .execute() + .then((snapshot) => snapshot.results.single); + } + + // Every kind of reference, in the order the expectations below use. + final all = [ + ref(document), + ref(subcollectionDocument), + ref(root), + ref(collection), + ref(subcollection), + ]; + // `firestore` is only assigned in setUp, so build these per test. + late List allDecoded; + setUp(() { + allDecoded = [ + firestore.doc('books/book-1'), + firestore.doc('books/book-1/chapters/c1'), + root, + collection, + subcollection, + ]; + }); + + test('decode the same way at the top level', () async { + final result = await decode({ + 'document': all[0], + 'subcollectionDocument': all[1], + 'root': all[2], + 'collection': all[3], + 'subcollection': all[4], + }); + + expect(result.data(), { + 'document': allDecoded[0], + 'subcollectionDocument': allDecoded[1], + 'root': allDecoded[2], + 'collection': allDecoded[3], + 'subcollection': allDecoded[4], + }); + expect(result.get('document'), isA>()); + expect(result.get('root'), isA()); + }); + + test('decode the same way inside a map', () async { + final result = await decode({ + 'm': map({for (var i = 0; i < all.length; i++) 'r$i': all[i]}), + }); + + expect(result.get('m'), { + for (var i = 0; i < allDecoded.length; i++) 'r$i': allDecoded[i], + }); + expect(result.get('m.r0'), firestore.doc('books/book-1')); + expect(result.get('m.r2'), root); + }); + + test('decode the same way inside an array', () async { + final result = await decode({'a': array(all)}); + + expect(result.get('a'), allDecoded); + }); + + test('decode the same way nested two deep', () async { + final result = await decode({ + 'mapInMap': map({ + 'inner': map({'root': all[2], 'document': all[0]}), + }), + 'arrayInMap': map({'inner': array(all)}), + 'mapInArray': array([ + map({'collection': all[3], 'document': all[0]}), + ]), + 'arrayInArray': array([array(all)]), + }); + + expect(result.data(), { + 'mapInMap': { + 'inner': {'root': root, 'document': allDecoded[0]}, + }, + 'arrayInMap': {'inner': allDecoded}, + 'mapInArray': [ + {'collection': collection, 'document': allDecoded[0]}, + ], + 'arrayInArray': [allDecoded], + }); + expect(result.get('mapInMap.inner.root'), root); + }); + + test('leave regular document decoding unchanged', () { + // Outside Pipelines a non-document reference is still rejected, at + // any depth; only document references decode. + final serializer = firestore.serializer; + + expect(serializer.decodeValue(all[0]), allDecoded[0]); + expect(serializer.decodeValue(map({'d': all[0]})), { + 'd': allDecoded[0], + }); + expect(() => serializer.decodeValue(all[3]), throwsArgumentError); + expect( + () => serializer.decodeValue(array([all[4]])), + throwsArgumentError, + ); + }); + }); + test('execute encodes options under their backend names', () async { firestore_v1.ExecutePipelineRequest? capturedRequest; @@ -390,9 +545,10 @@ void main() { ), ); + // The backend rejects index_mode ("Unsupported option: index_mode"), so + // the deprecated indexMode is not sent. final options = capturedRequest!.structuredPipeline!.options; - expect(options.keys, unorderedEquals(['index_mode', 'explain_options'])); - expect(options['index_mode']!.stringValue, 'recommended'); + expect(options.keys, ['explain_options']); final explain = options['explain_options']!.mapValue!.fields; expect(explain['mode']!.stringValue, 'analyze'); @@ -586,9 +742,10 @@ void main() { final request = requests.single; // Pipeline options and the transaction live on different parts of the - // request, so neither clobbers the other. + // request, so neither clobbers the other. The deprecated indexMode is + // not sent, as on Pipeline.execute. final options = request.structuredPipeline!.options; - expect(options['index_mode']!.stringValue, 'recommended'); + expect(options.keys, ['explain_options']); expect( options['explain_options']!.mapValue!.fields['mode']!.stringValue, 'analyze', @@ -1340,6 +1497,773 @@ void main() { }); }); + // Mirrors the Node SDK's `OptionsUtil` and `rawStage`. + group('raw options and raw stages', () { + final requests = []; + + setUp(() { + requests.clear(); + when( + () => mockClient.v1>( + any(), + ), + ).thenAnswer((invocation) async { + final callback = + invocation.positionalArguments.single + as Future> + Function(firestore_v1.Firestore api, String projectId); + + final api = FakeFirestore( + executePipeline: (request) { + requests.add(request); + return Stream.value( + firestore_v1.ExecutePipelineResponse( + transaction: Uint8List.fromList([9, 9]), + ), + ); + }, + ); + + return callback(api, _projectId); + }); + }); + + Pipeline base() => firestore.pipeline().collection('books'); + + Map json(Map options) { + return { + for (final MapEntry(:key, :value) in options.entries) + key: value.toJson(), + }; + } + + Map executeOptions() { + return json(requests.single.structuredPipeline!.options); + } + + List stages() { + return requests.single.structuredPipeline!.pipeline!.stages; + } + + Map string(String value) => {'stringValue': value}; + + Map map(Map fields) { + return { + 'mapValue': {'fields': fields}, + }; + } + + test( + 'execute merges dotted rawOptions keys into typed options', + () async { + await base().execute( + indexMode: PipelineIndexMode.recommended, + explain: const PipelineExplainOptions( + mode: PipelineExplainMode.analyze, + ), + rawOptions: const { + 'explain_options.output_format': 'json', + 'index_mode': 'custom', + 'a.b.c': 'deep', + }, + ); + + expect(executeOptions(), { + // A single map: the dotted key lands next to the typed mode. + 'explain_options': map({ + 'mode': string('analyze'), + 'output_format': string('json'), + }), + 'index_mode': string('custom'), + 'a': map({ + 'b': map({'c': string('deep')}), + }), + }); + }, + ); + + test( + 'a rawOptions key without a dot replaces the typed option', + () async { + await base().execute( + explain: const PipelineExplainOptions( + mode: PipelineExplainMode.analyze, + outputFormat: PipelineExplainOutputFormat.text, + ), + rawOptions: const { + 'explain_options': {'mode': 'execute'}, + }, + ); + + expect(executeOptions(), { + 'explain_options': map({'mode': string('execute')}), + }); + }, + ); + + test( + 'Transaction.executePipeline merges dotted rawOptions keys', + () async { + await firestore.runTransaction( + (transaction) => transaction.executePipeline( + base(), + explain: const PipelineExplainOptions( + mode: PipelineExplainMode.analyze, + ), + rawOptions: const {'explain_options.output_format': 'text'}, + ), + transactionOptions: ReadOnlyTransactionOptions(), + ); + + expect(executeOptions(), { + 'explain_options': map({ + 'mode': string('analyze'), + 'output_format': string('text'), + }), + }); + }, + ); + + test('raw option keys apply in order', () async { + await base() + .rawStage( + 'custom', + const [], + options: const { + // A dotted key merges into an earlier map... + 'merged': {'a': 1}, + 'merged.b': 'two', + // ...a key without a dot replaces what came before... + 'replaced.a': 1, + 'replaced': 'value', + // ...and a dotted key replaces a value that is not a map. + 'promoted': 'value', + 'promoted.a': true, + // Backticks are not unescaped. + 'a.`b`': 1, + }, + ) + .execute(); + + expect(json(stages().last.options), { + 'merged': map({ + 'a': {'integerValue': '1'}, + 'b': string('two'), + }), + 'replaced': string('value'), + 'promoted': map({ + 'a': {'booleanValue': true}, + }), + 'a': map({ + '`b`': {'integerValue': '1'}, + }), + }); + }); + + test('every stage takes rawOptions', () async { + const raw = {'foo': 'bar', 'outer.inner': 1}; + final other = firestore.pipeline().collection('magazines'); + + await firestore + .pipeline() + .collection('books', rawOptions: raw) + .where(field('rating').greaterThan(1), rawOptions: raw) + .select(['title', 'rating', 'tags'], rawOptions: raw) + .addFields([field('rating').as('score')], rawOptions: raw) + .removeFields(['score'], rawOptions: raw) + .sort([field('title').ascending()], rawOptions: raw) + .offset(1, rawOptions: raw) + .limit(10, rawOptions: raw) + .distinct(['title', 'tags'], rawOptions: raw) + .aggregate( + [PipelineFunctions.countAll().as('total')], + groups: ['title'], + rawOptions: raw, + ) + .replaceWith('title', rawOptions: raw) + .union(other, rawOptions: raw) + .sample(documents: 5, rawOptions: raw) + .unnest('tags', rawOptions: raw) + .findNearest( + vectorField: 'embedding', + queryVector: const [1, 2], + distanceMeasure: DistanceMeasure.cosine, + rawOptions: raw, + ) + .search({'query': documentMatches('dart')}, rawOptions: raw) + .execute(); + + final expected = { + 'foo': string('bar'), + 'outer': map({ + 'inner': {'integerValue': '1'}, + }), + }; + expect(stages().map((stage) => stage.name), [ + 'collection', + 'where', + 'select', + 'add_fields', + 'remove_fields', + 'sort', + 'offset', + 'limit', + 'distinct', + 'aggregate', + 'replace_with', + 'union', + 'sample', + 'unnest', + 'find_nearest', + 'search', + ]); + for (final stage in stages()) { + final options = json(stage.options); + if (stage.name == 'search') options.remove('query'); + expect(options, expected, reason: stage.name); + } + // The nested Pipeline keeps its own, empty, options. + final union = stages().firstWhere((stage) => stage.name == 'union'); + expect(union.args.single.pipelineValue!.stages.single.options, isEmpty); + }); + + test('every source takes rawOptions', () async { + const raw = {'foo': 'bar'}; + final source = firestore.pipeline(); + + for (final pipeline in [ + source.collection('books', rawOptions: raw), + source.collectionReference( + firestore.collection('books'), + rawOptions: raw, + ), + source.collectionGroup('books', rawOptions: raw), + source.database(rawOptions: raw), + source.documents([firestore.doc('books/b')], rawOptions: raw), + ]) { + requests.clear(); + await pipeline.execute(); + expect(json(stages().single.options), {'foo': string('bar')}); + } + }); + + test('rawOptions override typed stage options', () async { + await base() + .findNearest( + vectorField: 'embedding', + queryVector: const [1, 2], + distanceMeasure: DistanceMeasure.euclidean, + limit: 10, + distanceResultField: 'distance', + rawOptions: const {'limit': 20, 'extra.flag': true}, + ) + .unnest( + 'tags', + indexField: 'index', + rawOptions: {'index_field': field('position')}, + ) + .search( + {'query': documentMatches('dart'), 'limit': 10}, + rawOptions: const {'limit': 20}, + ) + .execute(); + + final [_, nearest, unnest, search] = stages(); + expect(json(nearest.options), { + 'limit': {'integerValue': '20'}, + 'distance_field': {'fieldReferenceValue': 'distance'}, + 'extra': map({ + 'flag': {'booleanValue': true}, + }), + }); + expect(json(unnest.options), { + 'index_field': {'fieldReferenceValue': 'position'}, + }); + expect(search.options['limit']!.integerValue, 20); + }); + + test('rejects raw option keys with an empty segment', () async { + for (final key in ['', '.', 'a.', '.a', 'a..b']) { + final options = {key: 1}; + final invalid = throwsA( + isA().having((e) => e.invalidValue, 'key', key), + ); + + expect( + () => base().rawStage('custom', [], options: options), + invalid, + ); + expect(() => base().limit(1, rawOptions: options), invalid); + expect( + () => firestore.pipeline().database(rawOptions: options), + invalid, + ); + await expectLater(base().execute(rawOptions: options), invalid); + await expectLater( + firestore.runTransaction( + (transaction) => + transaction.executePipeline(base(), rawOptions: options), + transactionOptions: ReadOnlyTransactionOptions(), + ), + invalid, + ); + } + expect(requests, isEmpty); + }); + + test('rawStage converts collections nested in a map argument', () async { + await base().rawStage('custom', [ + { + 'field': field('f'), + 'literal': 1, + 'nestedMap': {'lang': field('lang')}, + 'nestedList': [field('a'), 1], + 'literalMap': constant({'x': 1}), + }, + // A list argument stays a literal value, as in Node. + [ + field('b'), + {'c': 1}, + ], + ]).execute(); + + final [mapArg, listArg] = stages().last.args; + final fields = mapArg.mapValue!.fields; + expect(fields['field']!.fieldReferenceValue, 'f'); + expect(fields['literal']!.integerValue, 1); + + final nestedMap = fields['nestedMap']!.functionValue!; + expect(nestedMap.name, 'map'); + expect(nestedMap.args.map((arg) => arg.toJson()), [ + string('lang'), + {'fieldReferenceValue': 'lang'}, + ]); + + final nestedList = fields['nestedList']!.functionValue!; + expect(nestedList.name, 'array'); + expect(nestedList.args.map((arg) => arg.toJson()), [ + {'fieldReferenceValue': 'a'}, + {'integerValue': '1'}, + ]); + + expect(fields['literalMap']!.mapValue!.fields['x']!.integerValue, 1); + + expect(listArg.arrayValue!.values.map((value) => value.toJson()), [ + {'fieldReferenceValue': 'b'}, + map({ + 'c': {'integerValue': '1'}, + }), + ]); + }); + }); + + // Mirrors the Node SDK's `field()`, which sends + // `FieldPath.fromArgument(path).formattedName`. + group('field paths', () { + group('field() canonicalizes the path', () { + void expectPath(Object fieldPath, String expected) { + expect(field(fieldPath).path, expected, reason: '$fieldPath'); + expect(Expression.field(fieldPath).path, expected); + } + + test('keeps simple identifiers as they are', () { + expectPath('title', 'title'); + expectPath('_private', '_private'); + expectPath('isbn13', 'isbn13'); + expectPath('__name__', '__name__'); + }); + + test('quotes segments that are not simple identifiers', () { + expectPath('first-name', '`first-name`'); + expectPath('last name', '`last name`'); + expectPath('naïve', '`naïve`'); + expectPath('1st', '`1st`'); + expectPath('a/b', '`a/b`'); + }); + + test('escapes backticks and backslashes inside a segment', () { + expectPath('a`b', r'`a\`b`'); + expectPath(r'a\b', r'`a\\b`'); + }); + + test('reads a dotted String as nested segments', () { + expectPath('metadata.lang', 'metadata.lang'); + expectPath('author.first-name', 'author.`first-name`'); + expectPath('my map.key', '`my map`.key'); + }); + + test('keeps each FieldPath segment whole, dots included', () { + expectPath(FieldPath(const ['a.b']), '`a.b`'); + expectPath(FieldPath(const ['a.b', 'c']), '`a.b`.c'); + expectPath(FieldPath(const ['metadata', 'lang']), 'metadata.lang'); + expectPath(FieldPath(const ['first-name']), '`first-name`'); + expectPath(FieldPath.documentId, '__name__'); + }); + + test('rejects other types and empty segments', () { + for (final invalid in [42, '', 'a..b', '.a', 'a.']) { + expect( + () => field(invalid), + throwsArgumentError, + reason: '$invalid', + ); + expect(() => Expression.field(invalid), throwsArgumentError); + } + }); + }); + + group('on the wire', () { + late List stages; + + Future run(Object pipelineOrQuery) async { + firestore_v1.ExecutePipelineRequest? capturedRequest; + + when( + () => mockClient.v1>( + any(), + ), + ).thenAnswer((invocation) async { + final callback = + invocation.positionalArguments.single + as Future> + Function(firestore_v1.Firestore api, String projectId); + + final api = FakeFirestore( + executePipeline: (firestore_v1.ExecutePipelineRequest request) { + capturedRequest = request; + return const Stream< + firestore_v1.ExecutePipelineResponse + >.empty(); + }, + ); + + return callback(api, _projectId); + }); + + final pipeline = pipelineOrQuery is Pipeline + ? pipelineOrQuery + : firestore.pipeline().createFrom(pipelineOrQuery); + await pipeline.execute(); + stages = capturedRequest!.structuredPipeline!.pipeline!.stages; + } + + Pipeline base() => firestore.pipeline().collection('books'); + Map ref(String path) => {'fieldReferenceValue': path}; + Object? json(firestore_v1.Value value) => value.toJson(); + + test( + 'field() and String field arguments send the quoted path', + () async { + await run( + base().where(PipelineFunctions.equal('first-name', 'Ada')).select( + [ + field(FieldPath(const ['a.b'])).as('result'), + ], + ), + ); + + final equal = stages[1].args.single.functionValue!; + expect(json(equal.args[0]), ref('`first-name`')); + // The value position keeps the string as a literal. + expect(equal.args[1].stringValue, 'Ada'); + + final select = stages[2].args.single.mapValue!.fields; + expect(json(select['result']!), ref('`a.b`')); + }, + ); + + // The backend reads select keys as field paths and rejects an + // unquoted `last name` ("Invalid property path"), so a String is keyed + // by its quoted path, like a PipelineField. + test( + 'select keys a String and a PipelineField by their quoted path', + () async { + await run( + base().select([ + 'first-name', + field('last name'), + 'metadata.lang', + field(FieldPath(const ['a.b'])), + ]), + ); + + final fields = stages[1].args.single.mapValue!.fields; + expect(fields.map((key, value) => MapEntry(key, json(value))), { + '`first-name`': ref('`first-name`'), + '`last name`': ref('`last name`'), + 'metadata.lang': ref('metadata.lang'), + '`a.b`': ref('`a.b`'), + }); + }, + ); + + test('distinct and aggregate groups are keyed like select', () async { + await run( + base() + .distinct(['first-name', field('last name')]) + .aggregate( + [PipelineFunctions.countAll().as('total')], + groups: ['first-name', field('last name')], + ), + ); + + final expected = { + '`first-name`': ref('`first-name`'), + '`last name`': ref('`last name`'), + }; + final distinct = stages[1].args.single.mapValue!.fields; + expect( + distinct.map((key, value) => MapEntry(key, json(value))), + expected, + ); + final groups = stages[2].args[1].mapValue!.fields; + expect( + groups.map((key, value) => MapEntry(key, json(value))), + expected, + ); + }); + + test('removeFields and sort send quoted paths', () async { + await run( + base().removeFields(['first-name', field('last name')]).sort([ + ascending('first-name'), + field('last name').descending(), + ]), + ); + + expect(stages[1].args.map(json), [ + ref('`first-name`'), + ref('`last name`'), + ]); + expect( + stages[2].args.map( + (ordering) => json(ordering.mapValue!.fields['expression']!), + ), + [ref('`first-name`'), ref('`last name`')], + ); + }); + + test('unnest quotes its target and index field once', () async { + await run(base().unnest(field('my tags'))); + // Node quotes this target twice; see the golden corpus. + expect(stages[1].args.map(json), [ + ref('`my tags`'), + ref('`my tags`'), + ]); + + await run( + base().unnest(field('tags').as('my tag'), indexField: 'tag index'), + ); + expect(stages[1].args.map(json), [ref('tags'), ref('`my tag`')]); + expect(json(stages[1].options['index_field']!), ref('`tag index`')); + }); + + test('findNearest quotes the vector and distance fields', () async { + await run( + base().findNearest( + vectorField: 'my embedding', + queryVector: const [1.0, 2.0], + distanceMeasure: DistanceMeasure.euclidean, + distanceResultField: 'my distance', + ), + ); + + expect(json(stages[1].args[0]), ref('`my embedding`')); + expect( + json(stages[1].options['distance_field']!), + ref('`my distance`'), + ); + }); + + // A FieldPath is a field name wherever a String is, as in field(). + group('a FieldPath names a field', () { + final dotted = FieldPath(const ['a.b', 'c']); + final dottedRef = ref('`a.b`.c'); + + test('in a function target', () async { + await run( + base().select([ + PipelineFunctions.toUpper(dotted).as('upper'), + PipelineFunctions.add(dotted, 1, [2]).as('sum'), + PipelineFunctions.equalAny(dotted, [1, 2]).as('any'), + PipelineFunctions.count(dotted).as('count'), + ]), + ); + + final fields = stages[1].args.single.mapValue!.fields; + Object? target(String alias) => + json(fields[alias]!.functionValue!.args.first); + expect(target('upper'), dottedRef); + expect(target('any'), dottedRef); + expect(target('count'), dottedRef); + // add nests its further operands; the field is the innermost one. + final inner = fields['sum']!.functionValue!.args.first; + expect(json(inner.functionValue!.args.first), dottedRef); + }); + + test('in ascending and descending', () async { + await run(base().sort([ascending(dotted), descending(dotted)])); + + expect( + stages[1].args.map( + (ordering) => json(ordering.mapValue!.fields['expression']!), + ), + [dottedRef, dottedRef], + ); + expect(ascending(dotted).expr, isA()); + }); + + test('in select, distinct and aggregate groups', () async { + await run( + base() + .select([ + dotted, + FieldPath(const ['title']), + ]) + .distinct([dotted]) + .aggregate( + [PipelineFunctions.countAll().as('n')], + groups: [dotted], + ), + ); + + Map projection(firestore_v1.Value value) { + return value.mapValue!.fields.map( + (key, value) => MapEntry(key, json(value)), + ); + } + + expect(projection(stages[1].args.single), { + '`a.b`.c': dottedRef, + 'title': ref('title'), + }); + expect(projection(stages[2].args.single), {'`a.b`.c': dottedRef}); + expect(projection(stages[3].args[1]), {'`a.b`.c': dottedRef}); + }); + + test('as the same key as its String and field forms', () { + expect( + () => base().select([ + 'first-name', + FieldPath(const ['first-name']), + ]), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains("'`first-name`'"), + ), + ), + ); + }); + + test('in removeFields, unnest and replaceWith', () async { + await run( + base().removeFields([dotted]).unnest(dotted).replaceWith(dotted), + ); + + expect(stages[1].args.map(json), [dottedRef]); + expect(stages[2].args.map(json), [dottedRef, dottedRef]); + expect(json(stages[3].args.first), dottedRef); + }); + + test('in findNearest', () async { + await run( + base().findNearest( + vectorField: dotted, + queryVector: const [1.0, 2.0], + distanceMeasure: DistanceMeasure.euclidean, + distanceResultField: FieldPath(const ['my.distance']), + distanceThreshold: 0.5, + ), + ); + + final [_, nearest, where] = stages; + expect(json(nearest.args[0]), dottedRef); + expect( + json(nearest.options['distance_field']!), + ref('`my.distance`'), + ); + final condition = where.args.single.functionValue!; + expect(json(condition.args[0]), ref('`my.distance`')); + }); + }); + + group('createFrom', () { + test('quotes FieldPath filters and orderings once', () async { + await run( + firestore + .collection('books') + .where(FieldPath(const ['a.b']), WhereFilter.equal, 1) + .where('a`b', WhereFilter.equal, 2) + .orderBy(FieldPath(const ['c.d'])), + ); + + final dotted = stages[1].args.single.functionValue!; + expect( + json(dotted.args[0].functionValue!.args.single), + ref('`a.b`'), + ); + final backtick = stages[2].args.single.functionValue!; + expect( + json(backtick.args[0].functionValue!.args.single), + ref(r'`a\`b`'), + ); + final sort = stages.singleWhere((stage) => stage.name == 'sort'); + expect( + json(sort.args.first.mapValue!.fields['expression']!), + ref('`c.d`'), + ); + }); + + test('keeps the projection paths quoted once', () async { + await run( + firestore.collection('books').select([ + FieldPath(const ['first-name']), + FieldPath(const ['a.b']), + FieldPath(const ['metadata', 'lang']), + ]), + ); + + final select = stages.singleWhere( + (stage) => stage.name == 'select', + ); + final fields = select.args.single.mapValue!.fields; + // Node quotes these twice; see the golden corpus. + expect(fields.map((key, value) => MapEntry(key, json(value))), { + '`first-name`': ref('`first-name`'), + '`a.b`': ref('`a.b`'), + 'metadata.lang': ref('metadata.lang'), + }); + }); + + test('reads FieldPath vector and distance fields', () async { + await run( + firestore + .collection('books') + .findNearest( + vectorField: FieldPath(const ['a.b']), + queryVector: const [1.0, 2.0], + limit: 3, + distanceMeasure: DistanceMeasure.euclidean, + distanceResultField: FieldPath(const ['my distance']), + ), + ); + + final nearest = stages.last; + expect(nearest.name, 'find_nearest'); + expect(json(nearest.args[0]), ref('`a.b`')); + expect( + json(nearest.options['distance_field']!), + ref('`my distance`'), + ); + final exists = stages[stages.length - 2].args.single.functionValue!; + expect(json(exists.args.single), ref('`a.b`')); + }); + }); + }); + }); + // Mirrors the Node SDK's `selectablesToObject` / `aliasedAggregateToMap`, // which throw rather than let a later entry overwrite an earlier one. group('duplicate aliases or fields', () { @@ -1368,6 +2292,14 @@ void main() { ); }); + test('select rejects a String and a field with the same quoted path', () { + // Both are keyed by `x-y`. + expect( + () => base().select(['x-y', field('x-y')]), + duplicateError('`x-y`', 'selections'), + ); + }); + test('select rejects an alias that collides with a field name', () { expect( () => base().select(['title', constant('x').as('title')]), @@ -1791,6 +2723,82 @@ void main() { 'target', ); }); + + // The backend rejects a distance_threshold option on find_nearest, + // so the threshold is applied as createFrom(vectorQuery) applies it. + group('applies distanceThreshold as a filter', () { + for (final (measure, comparison, distanceFunction) in [ + ( + DistanceMeasure.euclidean, + 'less_than_or_equal', + 'euclidean_distance', + ), + (DistanceMeasure.cosine, 'less_than_or_equal', 'cosine_distance'), + // A larger dot product means more similar vectors. + ( + DistanceMeasure.dotProduct, + 'greater_than_or_equal', + 'dot_product', + ), + ]) { + test('${measure.name} keeps $comparison the threshold', () async { + await run( + base().findNearest( + vectorField: 'embedding', + queryVector: const [1.0, 2.0], + distanceMeasure: measure, + limit: 3, + distanceThreshold: 0.5, + ), + ); + + final [_, findNearest, where] = stages; + expect(findNearest.name, 'find_nearest'); + expect(findNearest.options.keys, ['limit']); + expect(where.name, 'where'); + final condition = where.args.single.functionValue!; + expect(condition.name, comparison); + expect(condition.args[1].doubleValue, 0.5); + final distance = condition.args[0].functionValue!; + expect(distance.name, distanceFunction); + expect(distance.args[0].fieldReferenceValue, 'embedding'); + expectVector(distance.args[1], [1.0, 2.0]); + }); + } + + test('reads distanceResultField when given', () async { + await run( + base().findNearest( + vectorField: 'embedding', + queryVector: const [1.0, 2.0], + distanceMeasure: DistanceMeasure.euclidean, + distanceResultField: 'distance', + distanceThreshold: 0.5, + ), + ); + + final [_, findNearest, where] = stages; + expect(findNearest.options.keys, ['distance_field']); + final condition = where.args.single.functionValue!; + expect(condition.name, 'less_than_or_equal'); + expect(condition.args[0].fieldReferenceValue, 'distance'); + }); + + test('adds no stage without a threshold', () async { + await run( + base().findNearest( + vectorField: 'embedding', + queryVector: const [1.0, 2.0], + distanceMeasure: DistanceMeasure.euclidean, + ), + ); + + expect(stages.map((stage) => stage.name), [ + 'collection', + 'find_nearest', + ]); + }); + }); }); }); @@ -2355,6 +3363,258 @@ void main() { expect(fields['minN']!.functionValue!.args[1].integerValue, 2); }); + group('Node SDK signatures', () { + late List stages; + + Future run(Pipeline pipeline) async { + firestore_v1.ExecutePipelineRequest? capturedRequest; + + when( + () => mockClient.v1>( + any(), + ), + ).thenAnswer((invocation) async { + final callback = + invocation.positionalArguments.single + as Future> + Function(firestore_v1.Firestore api, String projectId); + + final api = FakeFirestore( + executePipeline: (firestore_v1.ExecutePipelineRequest request) { + capturedRequest = request; + return const Stream.empty(); + }, + ); + + return callback(api, _projectId); + }); + + await pipeline.execute(); + stages = capturedRequest!.structuredPipeline!.pipeline!.stages; + } + + Future> select( + Iterable selections, + ) async { + await run(firestore.pipeline().collection('books').select(selections)); + return stages[1].args.single.mapValue!.fields; + } + + group('variadic add and multiply', () { + for (final (name, static, fluent) in [ + ( + 'add', + PipelineFunctions.add, + (PipelineExpression e, Object? second, Iterable others) => + e.add(second, others), + ), + ( + 'multiply', + PipelineFunctions.multiply, + (PipelineExpression e, Object? second, Iterable others) => + e.multiply(second, others), + ), + ]) { + test('$name nests further operands in two-operand calls', () async { + final fields = await select([ + static('rating', 2, [field('bonus'), 3]).as('static'), + fluent(field('rating'), 2, [field('bonus'), 3]).as('fluent'), + ]); + + // The backend's add and multiply take exactly two operands, so + // (rating, 2, [bonus, 3]) is sent as name(name(name(rating, 2), + // bonus), 3). + Map call(Object? left, Object? right) => { + 'functionValue': { + 'name': name, + 'args': [left, right], + }, + }; + for (final alias in ['static', 'fluent']) { + expect( + fields[alias]!.toJson(), + call( + call( + call( + {'fieldReferenceValue': 'rating'}, + {'integerValue': '2'}, + ), + {'fieldReferenceValue': 'bonus'}, + ), + {'integerValue': '3'}, + ), + reason: alias, + ); + } + }); + + test('$name still takes exactly two operands', () async { + final fields = await select([ + static('rating', 2).as('static'), + fluent(field('rating'), 2, const []).as('fluent'), + ]); + + for (final alias in ['static', 'fluent']) { + final function = fields[alias]!.functionValue!; + expect(function.name, name, reason: alias); + expect(function.args, hasLength(2), reason: alias); + } + }); + } + + test('collections among the operands are built as functions', () async { + final fields = await select([ + PipelineFunctions.add('a', 1, [ + [field('b')], + ]).as('sum'), + ]); + + final operand = fields['sum']!.functionValue!.args[1]; + expect(operand.functionValue!.name, 'array'); + expect(operand.functionValue!.args.single.fieldReferenceValue, 'b'); + }); + }); + + group('documents', () { + test('takes document paths', () async { + await run( + firestore.pipeline().documents([ + 'books/book-1', + '/books/book-2', + 'authors/author-1/books/book-3', + ]), + ); + + final stage = stages.single; + expect(stage.name, 'documents'); + expect(stage.args.map((arg) => arg.referenceValue), [ + '/books/book-1', + '/books/book-2', + '/authors/author-1/books/book-3', + ]); + }); + + test('mixes paths and references', () async { + await run( + firestore.pipeline().documents([ + 'books/book-1', + firestore.doc('books/book-2'), + ]), + ); + + expect(stages.single.args.map((arg) => arg.referenceValue), [ + '/books/book-1', + '/books/book-2', + ]); + }); + + test('rejects paths that do not point to a document', () { + for (final path in ['books', 'authors/author-1/books', '', 'a//b']) { + expect( + () => firestore.pipeline().documents([path]), + throwsArgumentError, + reason: path, + ); + } + }); + + test('rejects values that are neither paths nor references', () { + expect( + () => firestore.pipeline().documents([firestore.collection('a')]), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('DocumentReference or a document path'), + ), + ), + ); + }); + + test('rejects an empty list', () { + expect( + () => firestore.pipeline().documents(const []), + throwsArgumentError, + ); + }); + }); + + group('PipelineOrdering', () { + test('exposes its expression and direction', () { + final rating = field('rating'); + final ascendingOrdering = rating.ascending(); + final descendingOrdering = rating.descending(); + + expect(ascendingOrdering.expr, same(rating)); + expect(ascendingOrdering.direction, 'ascending'); + expect(descendingOrdering.expr, same(rating)); + expect(descendingOrdering.direction, 'descending'); + }); + + test('reads a field name passed to the top-level helpers', () { + for (final (ordering, direction) in [ + (ascending('rating'), 'ascending'), + (descending('rating'), 'descending'), + ]) { + expect( + ordering.expr, + isA().having((f) => f.path, 'path', 'rating'), + ); + expect(ordering.direction, direction); + } + }); + }); + + group('map', () { + test('takes a Map, encoded like alternating keys and values', () async { + final fields = await select([ + PipelineFunctions.map({ + 'title': field('title'), + 'rating': 5, + 'nested': {'genre': field('genre')}, + 'list': [field('tags'), 1], + }).as('fromMap'), + PipelineFunctions.map([ + 'title', + field('title'), + 'rating', + 5, + 'nested', + {'genre': field('genre')}, + 'list', + [field('tags'), 1], + ]).as('fromIterable'), + ]); + + final fromMap = fields['fromMap']!.functionValue!; + expect(fromMap.name, 'map'); + expect(fromMap.args[0].stringValue, 'title'); + expect(fromMap.args[1].fieldReferenceValue, 'title'); + expect(fromMap.args[5].functionValue!.name, 'map'); + expect(fromMap.args[7].functionValue!.name, 'array'); + expect( + fromMap.toJson(), + fields['fromIterable']!.functionValue!.toJson(), + ); + }); + + test('takes an empty Map', () async { + final fields = await select([ + PipelineFunctions.map({}).as('empty'), + ]); + + final function = fields['empty']!.functionValue!; + expect(function.name, 'map'); + expect(function.args, isEmpty); + }); + + test('rejects values that are neither a Map nor an Iterable', () { + expect(() => PipelineFunctions.map('title'), throwsArgumentError); + expect(() => PipelineFunctions.map(field('a')), throwsArgumentError); + }); + }); + }); + group('createFrom', () { late List stages; @@ -2455,6 +3715,77 @@ void main() { expect(orderings, ['__name__']); }); + /// The (field, direction) pairs of the last sort stage. + List<(String?, String?)> sortOrder() => [ + for (final arg in stages.lastWhere((s) => s.name == 'sort').args) + ( + arg.mapValue!.fields['expression']!.fieldReferenceValue, + arg.mapValue!.fields['direction']!.stringValue, + ), + ]; + + test('orders by != and not-in fields like range fields', () async { + // Regression: '!=' and 'not-in' fields were left out of the implicit + // ordering, so the Pipeline sorted only by the document key. + await run( + firestore + .collection('books') + .where('rating', WhereFilter.greaterThan, 4) + .where('genre', WhereFilter.notEqual, 'Horror') + .where('author', WhereFilter.notIn, ['Anonymous']), + ); + + // Inequality fields in field path order, then the document key. + expect(sortOrder(), [ + ('author', 'ascending'), + ('genre', 'ascending'), + ('rating', 'ascending'), + ('__name__', 'ascending'), + ]); + }); + + test('sorts inequality fields by segment, not quoted path', () async { + await run( + firestore + .collection('books') + .where('price-tier', WhereFilter.lessThan, 3) + .where('price.amount', WhereFilter.greaterThan, 1), + ); + + // `price-tier` is quoted on the wire, but `price` sorts first. + expect(sortOrder().map((order) => order.$1), [ + 'price.amount', + '`price-tier`', + '__name__', + ]); + }); + + test('implicit orderings take the last explicit direction', () async { + await run( + firestore + .collection('books') + .where('genre', WhereFilter.notIn, ['Horror']) + .orderBy('rating', descending: true), + ); + + expect(sortOrder(), [ + ('rating', 'descending'), + ('genre', 'descending'), + ('__name__', 'descending'), + ]); + + // Existence checks cover the explicit ordering and the key; the + // inequality filter brings its own (none, for not-in). + final exists = stages[2].args.single.functionValue!; + expect(exists.name, 'and'); + expect( + exists.args.map( + (arg) => arg.functionValue!.args.single.fieldReferenceValue, + ), + ['rating', '__name__'], + ); + }); + test('converts composite filters', () async { await run( firestore @@ -2599,6 +3930,43 @@ void main() { expect(endBefore.name, 'less_than'); }); + test('flips cursor comparisons for descending orderings', () async { + // Regression: cursors compared as if every ordering were ascending + // (as Node does), so startAt(4) on a descending rating kept ratings of + // 4 and more instead of 4 and less. + await run( + firestore + .collection('books') + .orderBy('rating', descending: true) + .orderBy('title') + .startAt([4, 'M']) + .endBefore([1]), + ); + + expect(stages.map((stage) => stage.name), [ + 'collection', + 'where', // existence checks + 'sort', + 'where', // startAt + 'where', // endBefore + ]); + + // rating < 4 || (rating == 4 && (title > 'M' || title == 'M')) + final startAt = stages[3].args.single.functionValue!; + expect(startAt.name, 'or'); + expect(startAt.args[0].functionValue!.name, 'less_than'); + final tie = startAt.args[1].functionValue!; + expect(tie.args[0].functionValue!.name, 'equal'); + final title = tie.args[1].functionValue!; + expect(title.args[0].functionValue!.name, 'greater_than'); + expect(title.args[1].functionValue!.name, 'equal'); + + // endBefore(1) on the descending rating keeps ratings above 1. + final endBefore = stages[4].args.single.functionValue!; + expect(endBefore.name, 'greater_than'); + expect(endBefore.args[1].integerValue, 1); + }); + test('sorts twice for limitToLast queries', () async { await run( firestore.collection('books').orderBy('rating').limitToLast(3), @@ -2661,6 +4029,87 @@ void main() { ); }); + group('applies a VectorQuery distanceThreshold as a filter', () { + Future runThreshold( + DistanceMeasure distanceMeasure, { + String? distanceResultField, + }) async { + await run( + firestore + .collection('books') + .findNearest( + vectorField: 'embedding', + queryVector: [1.0, 2.0, 3.0], + limit: 5, + distanceMeasure: distanceMeasure, + distanceResultField: distanceResultField, + distanceThreshold: 0.5, + ), + ); + + // The backend rejects a threshold option on find_nearest, so none is + // sent. + final findNearest = stages[stages.length - 2]; + expect(findNearest.name, 'find_nearest'); + expect( + findNearest.options.keys, + isNot(contains('distance_threshold')), + ); + + final where = stages.last; + expect(where.name, 'where'); + final condition = where.args.single.functionValue!; + expect(condition.args[1].doubleValue, 0.5); + return condition; + } + + test('keeps euclidean distances at most the threshold', () async { + final condition = await runThreshold(DistanceMeasure.euclidean); + + expect(condition.name, 'less_than_or_equal'); + final distance = condition.args[0].functionValue!; + expect(distance.name, 'euclidean_distance'); + expect(distance.args[0].fieldReferenceValue, 'embedding'); + expect( + distance.args[1].mapValue!.fields['value']!.arrayValue!.values.map( + (v) => v.doubleValue, + ), + [1.0, 2.0, 3.0], + ); + }); + + test('keeps cosine distances at most the threshold', () async { + final condition = await runThreshold(DistanceMeasure.cosine); + + expect(condition.name, 'less_than_or_equal'); + expect(condition.args[0].functionValue!.name, 'cosine_distance'); + }); + + test('keeps dot products at least the threshold', () async { + // A larger dot product means more similar vectors. + final condition = await runThreshold(DistanceMeasure.dotProduct); + + expect(condition.name, 'greater_than_or_equal'); + expect(condition.args[0].functionValue!.name, 'dot_product'); + }); + + test('reads the distance result field when there is one', () async { + final condition = await runThreshold( + DistanceMeasure.euclidean, + distanceResultField: 'distance', + ); + + expect(condition.name, 'less_than_or_equal'); + expect(condition.args[0].fieldReferenceValue, 'distance'); + expect( + stages[stages.length - 2] + .options['distance_field']! + .fieldReferenceValue, + 'distance', + ); + }); + }); + test('rejects a query from a different database', () { final other = Firestore.internal( settings: const Settings( @@ -2700,6 +4149,23 @@ void main() { () => firestore.pipeline().documents([other.doc('books/book-1')]), throwsA(_crossDatabaseError), ); + // Every reference is checked, not only the first; paths resolve + // against this Pipeline's database. + expect( + () => firestore.pipeline().documents([ + 'books/book-1', + firestore.doc('books/book-2'), + other.doc('books/book-3'), + ]), + throwsA(_crossDatabaseError), + ); + expect( + () => firestore.pipeline().documents([ + 'books/book-1', + firestore.doc('books/book-2'), + ]), + returnsNormally, + ); }); test('rejects a union with a different database', () { diff --git a/packages/google_cloud_firestore/test/query_test.dart b/packages/google_cloud_firestore/test/query_test.dart index d0fd7c5c..074fef44 100644 --- a/packages/google_cloud_firestore/test/query_test.dart +++ b/packages/google_cloud_firestore/test/query_test.dart @@ -18,6 +18,7 @@ import 'package:google_cloud_firestore/google_cloud_firestore.dart' hide greaterThan, lessThan; import 'package:google_cloud_firestore/src/firestore_http_client.dart'; import 'package:google_cloud_firestore_v1/firestore.dart' as firestore_v1; +import 'package:google_cloud_firestore_v1/testing.dart'; import 'package:google_cloud_protobuf/protobuf.dart' as protobuf_v1; import 'package:http/http.dart' as http; import 'package:mocktail/mocktail.dart'; @@ -125,6 +126,154 @@ void main() { }); }); + group('field paths in the request (unit)', () { + test('quotes and escapes segments that are not identifiers', () async { + final mockClient = MockFirestoreHttpClient(); + final firestore = Firestore.internal( + settings: const Settings(projectId: mockProjectId), + client: mockClient, + ); + when(() => mockClient.cachedProjectId).thenReturn(mockProjectId); + + firestore_v1.RunQueryRequest? captured; + when( + () => mockClient.v1>(any()), + ).thenAnswer((invocation) async { + final callback = + invocation.positionalArguments.single + as Future> Function( + firestore_v1.Firestore api, + String projectId, + ); + final api = FakeFirestore( + runQuery: (request) { + captured = request; + return const Stream.empty(); + }, + ); + return callback(api, mockProjectId); + }); + + await firestore + .collection('books') + .where('a`b', WhereFilter.equal, 1) + .orderBy(FieldPath(const ['c.d', r'e\f'])) + .get(); + + final query = captured!.structuredQuery!; + // Matches the Node SDK's FieldPath.formattedName: a backtick inside a + // segment is escaped, not replaced. + expect(query.where!.fieldFilter!.field!.fieldPath, r'`a\`b`'); + expect(query.orderBy.first.field!.fieldPath, r'`c.d`.`e\\f`'); + }); + }); + + group('DocumentSnapshot cursors (unit)', () { + late MockFirestoreHttpClient mockClient; + late Firestore firestore; + late List requests; + + setUp(() { + mockClient = MockFirestoreHttpClient(); + firestore = Firestore.internal( + settings: const Settings(projectId: mockProjectId), + client: mockClient, + ); + requests = []; + + when(() => mockClient.cachedProjectId).thenReturn(mockProjectId); + when( + () => mockClient.v1>(any()), + ).thenAnswer((invocation) async { + final callback = + invocation.positionalArguments.single + as Future> Function( + firestore_v1.Firestore api, + String projectId, + ); + final api = FakeFirestore( + runQuery: (request) { + requests.add(request); + return Stream.value( + _documentResponse( + documentPath: 'books/book1', + fields: {'genre': 'Fantasy', 'rating': 5, 'title': 'Dune'}, + firestore: firestore, + ), + ); + }, + ); + return callback(api, mockProjectId); + }); + }); + + /// Runs `query.startAfterDocument(...)` and returns the orderings and the + /// cursor values it sends. + Future<(List, List)> startAfterDocument( + Query query, + ) async { + final snapshot = (await query.get()).docs.single; + await query.startAfterDocument(snapshot).get(); + + final structuredQuery = requests.last.structuredQuery!; + final orderings = [ + for (final order in structuredQuery.orderBy) + '${order.field!.fieldPath} ${order.direction.value}', + ]; + expect(structuredQuery.startAt!.before, isFalse); + return (orderings, structuredQuery.startAt!.values); + } + + test('orders by a != field before the document key', () async { + // Regression: '!=' was not an inequality, so the cursor ordered by the + // document key alone instead of the order the backend uses. + final (orderings, values) = await startAfterDocument( + firestore + .collection('books') + .where('genre', WhereFilter.notEqual, 'Horror'), + ); + + expect(orderings, ['genre ASCENDING', '__name__ ASCENDING']); + expect(values.first.stringValue, 'Fantasy'); + expect(values.last.referenceValue, endsWith('/documents/books/book1')); + }); + + test('orders by every inequality field, in field path order', () async { + final (orderings, values) = await startAfterDocument( + firestore + .collection('books') + .where('rating', WhereFilter.greaterThan, 4) + .where('genre', WhereFilter.notIn, ['Horror']), + ); + + expect(orderings, [ + 'genre ASCENDING', + 'rating ASCENDING', + '__name__ ASCENDING', + ]); + expect(values.map((value) => value.stringValue ?? value.integerValue), [ + 'Fantasy', + 5, + null, + ]); + }); + + test('appends inequality fields after the explicit orderBy', () async { + final (orderings, _) = await startAfterDocument( + firestore + .collection('books') + .where('rating', WhereFilter.greaterThan, 4) + .orderBy('title', descending: true), + ); + + expect(orderings, [ + 'title DESCENDING', + 'rating DESCENDING', + '__name__ DESCENDING', + ]); + }); + }); + group('query interface', () { late Firestore firestore; diff --git a/packages/google_cloud_firestore/tool/pipeline_api_parity/.gitignore b/packages/google_cloud_firestore/tool/pipeline_api_parity/.gitignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/packages/google_cloud_firestore/tool/pipeline_api_parity/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/packages/google_cloud_firestore/tool/pipeline_api_parity/README.md b/packages/google_cloud_firestore/tool/pipeline_api_parity/README.md new file mode 100644 index 00000000..8810d441 --- /dev/null +++ b/packages/google_cloud_firestore/tool/pipeline_api_parity/README.md @@ -0,0 +1,58 @@ +# Pipeline API parity snapshot + +`test/pipeline_api_parity_test.dart` checks that the Dart Pipelines API keeps +the same signatures as the Node.js Admin SDK: argument counts, optional +parameters, single values versus lists, which kinds of values each parameter +accepts, options objects versus named parameters, and the `isType` values. +Unit tests cannot catch these mismatches, since a test only exercises the +signature it was written against. + +The test does not need Node. It reads +`test/fixtures/node_pipeline_api.json`, which this tool extracts from the +TypeScript declarations of the pinned `@google-cloud/firestore` release +(`types/firestore.d.ts`, namespace `FirebaseFirestore.Pipelines`), with the +TypeScript compiler API. It also lists the few functions and methods that the +JavaScript build exports without declaring them, so the test does not report +them as Dart-only. + +## Regenerating the snapshot + +```sh +cd packages/google_cloud_firestore/tool/pipeline_api_parity +npm ci +npm run extract +``` + +The output only depends on the pinned versions in `package-lock.json` and on +`extract_node_api.mjs`, so it is byte-for-byte reproducible. The +"Pipeline API snapshot" workflow regenerates it and fails when the committed +file differs. + +Regenerate it when you change `extract_node_api.mjs`, or to compare against a +newer Node SDK: + +```sh +npm install --save-exact @google-cloud/firestore@ +npm run extract +``` + +Then update `_nodeVersion` in `test/pipeline_api_parity_test.dart`, run that +test from `packages/google_cloud_firestore`, and triage what it reports. + +## Triaging a failure + +Each difference is one line, `id: message`, for example: + +``` +PipelineFunctions.join#arity: Node requires 2 argument(s), Dart requires 1 — Node join(arrayExpression: expression|string, delimiter: expression|string); Dart join(Object? array, [Object? delimiter]) +``` + +Fix the Dart API, or record the difference in the test: + +- `_knownDifferences`: intentional, with the reason. +- `_pendingFixes`: a Dart bug or gap to fix later. +- `_dartOnly`: a public Dart member Node has no counterpart for. +- `_renames`: a Node member Dart spells differently. + +The test fails on stale entries, so remove an entry once the difference is +gone. diff --git a/packages/google_cloud_firestore/tool/pipeline_api_parity/extract_node_api.mjs b/packages/google_cloud_firestore/tool/pipeline_api_parity/extract_node_api.mjs new file mode 100644 index 00000000..45481481 --- /dev/null +++ b/packages/google_cloud_firestore/tool/pipeline_api_parity/extract_node_api.mjs @@ -0,0 +1,741 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Extracts the Pipelines API of the Node.js Firestore SDK into a JSON +// snapshot that test/pipeline_api_parity_test.dart compares the Dart API +// against. See README.md in this directory. +// +// The declarations are read syntactically with the TypeScript compiler API +// (no type checking), so the output only depends on the pinned +// @google-cloud/firestore and typescript versions and on this script. + +import fs from 'node:fs'; +import path from 'node:path'; +import {createRequire} from 'node:module'; +import {fileURLToPath} from 'node:url'; +import ts from 'typescript'; + +const toolDir = path.dirname(fileURLToPath(import.meta.url)); +const outputPath = path.resolve( + toolDir, + '../../test/fixtures/node_pipeline_api.json', +); + +const require = createRequire(import.meta.url); +const packageJsonPath = require.resolve('@google-cloud/firestore/package.json'); +const packageDir = path.dirname(packageJsonPath); +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); +const pinnedVersion = JSON.parse( + fs.readFileSync(path.join(toolDir, 'package.json'), 'utf8'), +).dependencies['@google-cloud/firestore']; +if (packageJson.version !== pinnedVersion) { + throw new Error( + `Installed @google-cloud/firestore ${packageJson.version} does not match ` + + `the pinned ${pinnedVersion}. Run \`npm ci\` in ${toolDir}.`, + ); +} + +const typingsRelativePath = 'types/firestore.d.ts'; +const runtimeIndexRelativePath = 'build/src/pipelines/index.js'; + +// The simplified kind of the classes that need one; any other class is named +// after itself (`VectorValue` is `vectorValue`). +const classKinds = new Map([ + ['Expression', 'expression'], + ['FunctionExpression', 'expression'], + ['Constant', 'expression'], + ['BooleanExpression', 'booleanExpression'], + ['Field', 'field'], + ['AggregateFunction', 'aggregateFunction'], + ['AliasedExpression', 'aliasedExpression'], + ['AliasedAggregate', 'aliasedAggregate'], + ['Selectable', 'selectable'], + ['Ordering', 'ordering'], + ['Pipeline', 'pipeline'], + ['PipelineSnapshot', 'pipelineSnapshot'], + ['PipelineResult', 'pipelineResult'], + ['PipelineSource', 'pipelineSource'], + ['ExplainStats', 'explainStats'], + ['Buffer', 'bytes'], + ['Uint8Array', 'bytes'], +]); + +function readSource(relativePath, scriptKind) { + const filePath = path.join(packageDir, relativePath); + return ts.createSourceFile( + filePath, + fs.readFileSync(filePath, 'utf8'), + ts.ScriptTarget.Latest, + /* setParentNodes */ true, + scriptKind, + ); +} + +function isExported(node) { + return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0; +} + +function hasModifier(node, kind) { + return (node.modifiers ?? []).some(modifier => modifier.kind === kind); +} + +function nameOf(node) { + return node.name && ts.isIdentifier(node.name) + ? node.name.text + : node.name?.getText(); +} + +const printer = ts.createPrinter({removeComments: true}); + +/** Comment-free, single-line text of a type, for humans reading diffs. */ +function typeText(node) { + if (!node) return null; + return printer + .printNode(ts.EmitHint.Unspecified, node, node.getSourceFile()) + .replace(/\s+/g, ' ') + .replace(/([({[<]) /g, '$1') + .replace(/,? ([)}\]>])/g, '$1') + .replace(/^\| /, ''); +} + +function sorted(values) { + return [...new Set(values)].sort(); +} + +// --- Locate the Pipelines namespace --------------------------------------- + +const typings = readSource(typingsRelativePath, ts.ScriptKind.TS); + +function findNamespace(statements, name) { + for (const statement of statements) { + if ( + ts.isModuleDeclaration(statement) && + statement.name.text === name && + statement.body && + ts.isModuleBlock(statement.body) + ) { + return statement.body; + } + } + throw new Error(`Namespace ${name} not found in ${typingsRelativePath}.`); +} + +const firebaseFirestore = findNamespace( + typings.statements, + 'FirebaseFirestore', +); +const pipelines = findNamespace(firebaseFirestore.statements, 'Pipelines'); + +const typeAliases = new Map(); +for (const statement of pipelines.statements) { + if (ts.isTypeAliasDeclaration(statement)) { + typeAliases.set(statement.name.text, statement); + } +} + +// --- Type simplification ---------------------------------------------------- + +/** String literal values of [node] when it is a union of string literals. */ +function stringLiterals(node, seen = new Set()) { + if (ts.isParenthesizedTypeNode(node)) return stringLiterals(node.type, seen); + if (ts.isUnionTypeNode(node)) { + const values = []; + for (const member of node.types) { + const memberValues = stringLiterals(member, seen); + if (memberValues === null) return null; + values.push(...memberValues); + } + return values; + } + if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) { + return [node.literal.text]; + } + if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) { + const alias = typeAliases.get(node.typeName.text); + if (alias && !seen.has(alias)) { + seen.add(alias); + return stringLiterals(alias.type, seen); + } + } + return null; +} + +/** + * Simplifies [node] to a set of kinds such as `expression`, `string`, + * `array` or `options`, collecting details into [info]. + */ +function collectKinds(node, info) { + if (ts.isParenthesizedTypeNode(node)) return collectKinds(node.type, info); + if (ts.isUnionTypeNode(node)) { + for (const member of node.types) collectKinds(member, info); + return; + } + if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) { + info.kinds.add('literal'); + info.literals.add(node.literal.text); + return; + } + if ( + ts.isTypeReferenceNode(node) && + ts.isIdentifier(node.typeName) && + typeAliases.has(node.typeName.text) && + stringLiterals(node) !== null + ) { + // A named string union such as `TimeUnit`; its values are under `types`. + info.kinds.add('literal'); + info.aliases.add(node.typeName.text); + return; + } + switch (node.kind) { + case ts.SyntaxKind.StringKeyword: + info.kinds.add('string'); + return; + case ts.SyntaxKind.NumberKeyword: + info.kinds.add('number'); + return; + case ts.SyntaxKind.BooleanKeyword: + info.kinds.add('boolean'); + return; + case ts.SyntaxKind.UnknownKeyword: + case ts.SyntaxKind.AnyKeyword: + info.kinds.add('any'); + return; + case ts.SyntaxKind.VoidKeyword: + info.kinds.add('void'); + return; + case ts.SyntaxKind.UndefinedKeyword: + info.kinds.add('undefined'); + return; + } + if (ts.isLiteralTypeNode(node)) { + if (node.literal.kind === ts.SyntaxKind.NullKeyword) { + info.kinds.add('null'); + } else if ( + node.literal.kind === ts.SyntaxKind.TrueKeyword || + node.literal.kind === ts.SyntaxKind.FalseKeyword + ) { + info.kinds.add('boolean'); + } else if (ts.isNumericLiteral(node.literal)) { + info.kinds.add('number'); + } else { + info.kinds.add('other'); + } + return; + } + if (ts.isArrayTypeNode(node)) { + info.kinds.add('array'); + collectKinds(node.elementType, info.elements); + return; + } + if (ts.isTypeLiteralNode(node)) { + info.kinds.add('map'); + return; + } + if (ts.isTypeReferenceNode(node)) { + const name = node.typeName.getText().replace(/^Pipelines\./, ''); + const typeArguments = node.typeArguments ?? []; + if (name === 'Array' && typeArguments.length === 1) { + info.kinds.add('array'); + collectKinds(typeArguments[0], info.elements); + return; + } + if (name === 'Record') { + info.kinds.add('map'); + return; + } + if (name === 'Promise' && typeArguments.length === 1) { + info.kinds.add('promise'); + collectKinds(typeArguments[0], info.elements); + return; + } + if (classKinds.has(name)) { + info.kinds.add(classKinds.get(name)); + return; + } + if (typeAliases.has(name) && isOptionsType(typeAliases.get(name))) { + info.kinds.add('options'); + info.options.add(name); + return; + } + const simpleName = name.split('.').pop(); + info.kinds.add(simpleName.charAt(0).toLowerCase() + simpleName.slice(1)); + return; + } + info.kinds.add('other'); +} + +function newInfo() { + return { + kinds: new Set(), + literals: new Set(), + aliases: new Set(), + options: new Set(), + get elements() { + return (this._elements ??= newInfo()); + }, + }; +} + +function encodeInfo(info) { + const result = {kinds: sorted(info.kinds)}; + if (info.literals.size > 0) result.literals = sorted(info.literals); + if (info.aliases.size > 0) result.aliases = sorted(info.aliases); + if (info.options.size > 0) result.options = sorted(info.options); + if (info._elements && info._elements.kinds.size > 0) { + result.elements = encodeInfo(info._elements); + } + return result; +} + +function simplifyType(node) { + if (!node) return {kinds: ['any'], type: null}; + const info = newInfo(); + collectKinds(node, info); + return {...encodeInfo(info), type: typeText(node)}; +} + +// --- Signatures ------------------------------------------------------------- + +function encodeParameter(parameter) { + const rest = parameter.dotDotDotToken !== undefined; + let type = simplifyType(parameter.type); + if (rest) { + // A rest parameter is typed as an array; each argument is one element. + if (!type.elements) { + throw new Error(`Rest parameter without an array type: ${parameter}`); + } + type = {...type.elements, type: type.type}; + } + const result = {name: parameter.name.getText(), ...type}; + // Only flag the exceptions, to keep the snapshot compact. + if (parameter.questionToken || parameter.initializer) result.optional = true; + if (rest) result.rest = true; + return result; +} + +function encodeSignature(node) { + return { + params: node.parameters.map(encodeParameter), + returns: simplifyType(node.type), + }; +} + +function sortOverloads(overloads) { + const unique = new Map(); + for (const overload of overloads) { + unique.set(stableStringify(overload), overload); + } + return [...unique.keys()].sort().map(key => unique.get(key)); +} + +/** Properties of an object type literal, keyed by name. */ +function encodeProperties(typeLiteral) { + const properties = {}; + for (const member of typeLiteral.members) { + if (ts.isPropertySignature(member)) { + const property = { + optional: member.questionToken !== undefined, + ...simplifyType(member.type), + }; + if (member.type && ts.isTypeLiteralNode(member.type)) { + property.properties = encodeProperties(member.type); + } + properties[nameOf(member)] = property; + } else if (ts.isIndexSignatureDeclaration(member)) { + properties['[key]'] = {optional: true, ...simplifyType(member.type)}; + } + } + return properties; +} + +/** + * Whether [alias] is a `*Options` object type, possibly built with + * `StageOptions & {...}` or `OneOf<{...}>`. + */ +function isOptionsType(alias) { + return ( + /Options$/.test(alias.name.text) && objectParts(alias.type) !== null + ); +} + +/** Splits an object type into its base aliases and literal members. */ +function objectParts(node) { + if (ts.isParenthesizedTypeNode(node)) return objectParts(node.type); + if (ts.isTypeLiteralNode(node)) { + return {bases: [], literals: [node], oneOf: []}; + } + if (ts.isIntersectionTypeNode(node)) { + const result = {bases: [], literals: [], oneOf: []}; + for (const member of node.types) { + const part = objectParts(member); + if (part === null) return null; + result.bases.push(...part.bases); + result.literals.push(...part.literals); + result.oneOf.push(...part.oneOf); + } + return result; + } + if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) { + const name = node.typeName.text; + const typeArguments = node.typeArguments ?? []; + if (name === 'OneOf' && typeArguments.length === 1) { + const inner = objectParts(typeArguments[0]); + if (inner === null) return null; + return {bases: [], literals: [], oneOf: inner.literals}; + } + if (typeAliases.has(name) && /Options$/.test(name)) { + return {bases: [name], literals: [], oneOf: []}; + } + } + return null; +} + +function encodeTypeAlias(alias) { + const literals = stringLiterals(alias.type); + if (literals !== null) { + return {kind: 'stringUnion', values: sorted(literals)}; + } + const parts = objectParts(alias.type); + if (parts !== null) { + const properties = {}; + for (const literal of parts.literals) { + Object.assign(properties, encodeProperties(literal)); + } + const oneOf = []; + for (const literal of parts.oneOf) { + const members = encodeProperties(literal); + // OneOf makes every member optional; exactly one must be given. + for (const [name, property] of Object.entries(members)) { + properties[name] = {...property, optional: true}; + } + oneOf.push(sorted(Object.keys(members))); + } + const result = {kind: 'object', properties}; + if (parts.bases.length > 0) result.extends = sorted(parts.bases); + if (oneOf.length > 0) result.oneOf = oneOf; + return result; + } + return {kind: 'other', ...simplifyType(alias.type)}; +} + +function encodeClassLike(node) { + const methods = new Map(); + const properties = {}; + const constructors = []; + for (const member of node.members) { + if ( + hasModifier(member, ts.SyntaxKind.PrivateKeyword) || + hasModifier(member, ts.SyntaxKind.ProtectedKeyword) + ) { + continue; + } + if (ts.isConstructorDeclaration(member)) { + constructors.push(encodeSignature(member)); + continue; + } + const name = nameOf(member); + // Underscored members are internal by convention. + if (!name || name.startsWith('_')) continue; + const isStatic = hasModifier(member, ts.SyntaxKind.StaticKeyword); + if (ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) { + const overloads = methods.get(name) ?? {static: isStatic, overloads: []}; + overloads.overloads.push(encodeSignature(member)); + methods.set(name, overloads); + } else if ( + ts.isPropertyDeclaration(member) || + ts.isPropertySignature(member) + ) { + properties[name] = { + static: isStatic, + readonly: hasModifier(member, ts.SyntaxKind.ReadonlyKeyword), + optional: member.questionToken !== undefined, + ...simplifyType(member.type), + }; + } else if (ts.isGetAccessorDeclaration(member)) { + properties[name] = { + static: isStatic, + readonly: true, + optional: false, + ...simplifyType(member.type), + }; + } + } + const result = { + abstract: hasModifier(node, ts.SyntaxKind.AbstractKeyword), + extends: null, + implements: [], + methods: Object.fromEntries( + [...methods].map(([name, value]) => [ + name, + {...value, overloads: sortOverloads(value.overloads)}, + ]), + ), + properties, + }; + if (constructors.length > 0) { + result.constructors = sortOverloads(constructors); + } + for (const clause of node.heritageClauses ?? []) { + const names = clause.types.map(type => type.expression.getText()); + if (clause.token === ts.SyntaxKind.ExtendsKeyword) { + result.extends = names.length === 1 ? names[0] : sorted(names); + } else { + result.implements = sorted(names); + } + } + return result; +} + +// --- Walk the namespace --------------------------------------------------- + +const functions = new Map(); +const classes = {}; +const interfaces = {}; +const types = {}; + +for (const statement of pipelines.statements) { + if (!isExported(statement)) continue; + if (ts.isFunctionDeclaration(statement)) { + const name = nameOf(statement); + const overloads = functions.get(name) ?? []; + overloads.push(encodeSignature(statement)); + functions.set(name, overloads); + } else if (ts.isClassDeclaration(statement)) { + classes[nameOf(statement)] = encodeClassLike(statement); + } else if (ts.isInterfaceDeclaration(statement)) { + interfaces[nameOf(statement)] = encodeClassLike(statement); + } else if (ts.isTypeAliasDeclaration(statement)) { + types[statement.name.text] = encodeTypeAlias(statement); + } else { + throw new Error( + `Unhandled export in the Pipelines namespace: ${statement.getText()}`, + ); + } +} + +// Pipeline entry points declared outside the namespace. +function findClassMethod(className, methodName) { + for (const statement of firebaseFirestore.statements) { + if (ts.isClassDeclaration(statement) && nameOf(statement) === className) { + const overloads = statement.members + .filter( + member => + ts.isMethodDeclaration(member) && nameOf(member) === methodName, + ) + .map(encodeSignature); + if (overloads.length > 0) return {overloads: sortOverloads(overloads)}; + } + } + throw new Error(`${className}.${methodName} not found.`); +} + +const entryPoints = { + 'Firestore.pipeline': findClassMethod('Firestore', 'pipeline'), + 'Transaction.execute': findClassMethod('Transaction', 'execute'), +}; + +// --- Runtime exports -------------------------------------------------------- + +// The typings omit some functions that the `@google-cloud/firestore/pipelines` +// entry point exports at runtime. Record their names and JavaScript parameter +// names so the Dart side can match them too. +const runtimeIndex = readSource(runtimeIndexRelativePath, ts.ScriptKind.JS); +const moduleVariables = new Map(); +const runtimeExports = new Map(); + +function visitRuntimeIndex(node) { + if ( + ts.isVariableDeclaration(node) && + ts.isIdentifier(node.name) && + node.initializer && + ts.isCallExpression(node.initializer) && + node.initializer.expression.getText() === 'require' + ) { + moduleVariables.set(node.name.text, node.initializer.arguments[0].text); + } + if ( + ts.isCallExpression(node) && + node.expression.getText() === 'Object.defineProperty' && + node.arguments[0].getText() === 'exports' && + ts.isStringLiteral(node.arguments[1]) + ) { + const name = node.arguments[1].text; + const getter = node.arguments[2].getText(); + const match = /return\s+(\w+)\.(\w+)/.exec(getter); + if (name !== '__esModule' && match) { + runtimeExports.set(name, {variable: match[1], symbol: match[2]}); + } + } + ts.forEachChild(node, visitRuntimeIndex); +} +visitRuntimeIndex(runtimeIndex); + +const declaredNames = new Set([ + ...functions.keys(), + ...Object.keys(classes), + ...Object.keys(interfaces), + ...Object.keys(types), +]); + +function encodeJsParameters(declaration) { + return declaration.parameters.map(parameter => { + const result = {name: parameter.name.getText()}; + if (parameter.initializer) result.optional = true; + if (parameter.dotDotDotToken) result.rest = true; + return result; + }); +} + +const runtimeOnlyFunctions = {}; +for (const [name, {variable, symbol}] of runtimeExports) { + if (declaredNames.has(name)) continue; + const modulePath = moduleVariables.get(variable); + const moduleSource = readSource( + path.posix.join(path.posix.dirname(runtimeIndexRelativePath), modulePath) + + '.js', + ts.ScriptKind.JS, + ); + const declaration = moduleSource.statements.find( + statement => + ts.isFunctionDeclaration(statement) && nameOf(statement) === symbol, + ); + if (!declaration) { + throw new Error( + `Runtime export ${name} is neither declared in the typings nor a ` + + `function of ${modulePath}.js; extend this script to handle it.`, + ); + } + runtimeOnlyFunctions[name] = { + module: modulePath.replace(/^\.\//, ''), + params: encodeJsParameters(declaration), + }; +} + +// Likewise, public methods and getters of the declared classes that only the +// JavaScript implementation has. +const runtimeOnlyMembers = {}; +const runtimeDir = path.join(packageDir, path.dirname(runtimeIndexRelativePath)); +for (const file of fs.readdirSync(runtimeDir).sort()) { + if (!file.endsWith('.js')) continue; + const source = readSource( + path.posix.join(path.posix.dirname(runtimeIndexRelativePath), file), + ts.ScriptKind.JS, + ); + for (const statement of source.statements) { + if (!ts.isClassDeclaration(statement)) continue; + const declared = classes[nameOf(statement)]; + if (!declared) continue; + for (const member of statement.members) { + const isMethod = ts.isMethodDeclaration(member); + if (!isMethod && !ts.isGetAccessorDeclaration(member)) continue; + const name = nameOf(member); + if ( + !name || + name.startsWith('_') || + name.startsWith('#') || + name in declared.methods || + name in declared.properties + ) { + continue; + } + const members = (runtimeOnlyMembers[nameOf(statement)] ??= {}); + members[name] = isMethod + ? {kind: 'method', module: file, params: encodeJsParameters(member)} + : {kind: 'getter', module: file}; + } + } +} + +// --- Output ----------------------------------------------------------------- + +function stableStringify(value) { + return JSON.stringify(sortKeys(value)); +} + +/** + * Pretty-prints [value] with sorted keys, keeping any object or array that + * fits on one line on a single line, so that each parameter is one line and + * diffs stay readable. + */ +function format(value, indent = '') { + const compact = stableStringify(value); + if ( + value === null || + typeof value !== 'object' || + indent.length + compact.length <= 120 + ) { + return compact; + } + const inner = indent + ' '; + if (Array.isArray(value)) { + if (value.length === 0) return '[]'; + const items = value.map(item => inner + format(item, inner)); + return `[\n${items.join(',\n')}\n${indent}]`; + } + const keys = Object.keys(value).sort(); + if (keys.length === 0) return '{}'; + const entries = keys.map( + key => `${inner}${JSON.stringify(key)}: ${format(value[key], inner)}`, + ); + return `{\n${entries.join(',\n')}\n${indent}}`; +} + +function sortKeys(value) { + if (Array.isArray(value)) return value.map(sortKeys); + if (value && typeof value === 'object') { + return Object.fromEntries( + Object.keys(value) + .sort() + .map(key => [key, sortKeys(value[key])]), + ); + } + return value; +} + +const snapshot = { + // Bump when the shape of this file changes, so the Dart test can tell. + formatVersion: 1, + source: { + package: packageJson.name, + version: packageJson.version, + typings: typingsRelativePath, + namespace: 'FirebaseFirestore.Pipelines', + runtimeIndex: runtimeIndexRelativePath, + generator: 'packages/google_cloud_firestore/tool/pipeline_api_parity', + }, + functions: Object.fromEntries( + [...functions].map(([name, overloads]) => [ + name, + {overloads: sortOverloads(overloads)}, + ]), + ), + classes, + interfaces, + types, + entryPoints, + runtimeExports: sorted(runtimeExports.keys()), + runtimeOnlyFunctions, + runtimeOnlyMembers, +}; + +fs.writeFileSync(outputPath, format(snapshot) + '\n'); +console.log( + `Wrote ${path.relative(process.cwd(), outputPath)}: ` + + `${functions.size} functions, ${Object.keys(classes).length} classes, ` + + `${Object.keys(types).length} types, ` + + `${Object.keys(runtimeOnlyFunctions).length} runtime-only functions, ` + + `${Object.values(runtimeOnlyMembers).flatMap(Object.keys).length} ` + + 'runtime-only members ' + + `(${packageJson.name} ${packageJson.version}).`, +); diff --git a/packages/google_cloud_firestore/tool/pipeline_api_parity/package-lock.json b/packages/google_cloud_firestore/tool/pipeline_api_parity/package-lock.json new file mode 100644 index 00000000..cf4b27ba --- /dev/null +++ b/packages/google_cloud_firestore/tool/pipeline_api_parity/package-lock.json @@ -0,0 +1,1372 @@ +{ + "name": "pipeline-api-parity", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pipeline-api-parity", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/firestore": "9.3.1", + "typescript": "5.9.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-9.3.1.tgz", + "integrity": "sha512-ofGTiSpfXrHrrS3KaDOmsupSzCHr4WVl1+s6Ke0F8rPBRyb/OuZSC8u1NjCKtt/fO7A1GvqASQGhdAwVsnGpQA==", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/firestore-api": "^0.2.0", + "@opentelemetry/api": "^1.9.0", + "fast-deep-equal": "^3.1.3", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^6.0.1", + "protobufjs": "^7.5.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/firestore-api": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore-api/-/firestore-api-0.2.0.tgz", + "integrity": "sha512-wfHJPZmLqzpy6FN6U+7HkkP/AKhGsbHSpEy/qL2TyI3/pKRAvpamQxZ2qvps5hMRgAjfuHCim+oeJj9RveCaqg==", + "license": "Apache-2.0", + "dependencies": { + "google-gax": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/gcp-metadata": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.4.tgz", + "integrity": "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "7.1.3", + "google-logging-utils": "1.1.3", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/google-gax": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.8.tgz", + "integrity": "sha512-M4vpZcXQIC1gqIVGQ7eaU3jXQA6zecStyTXu514TYfThlgSurYJOxHZo9fzU6hAgwPWvuEynAVHWyaIk80VeEA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "10.5.0", + "google-logging-utils": "1.1.3", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "3.0.4", + "protobufjs": "^7.5.4", + "retry-request": "^8.0.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/proto3-json-serializer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", + "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/retry-request": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.4.tgz", + "integrity": "sha512-pI6/7eabUYkZxamkOq0g0uMxKLLGnjzhefY+vL8bVXag5rto4OU2YBTPytWLuHH7aEKD6fn7kJycQfid0Mwnkw==", + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/teeny-request": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.4.tgz", + "integrity": "sha512-R1Cg4Vu0UULeDfHL/kjABLaTW++9yD/B6n2g48y5dJ04hsEaxcfmAqbNDzNsbqAYJyIpZafjklLG9YxRu9uzOg==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.5.tgz", + "integrity": "sha512-7VZM+SVdEcUUqSQeNI3zM8Qs/BhQKZndPo2h5VkYkAM8Iz0wJIa8mKV5ekQGqG8UUsnkQ0NMxIxwkIHYvj0qOw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT" + }, + "node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.0.4.tgz", + "integrity": "sha512-p6WvbXOhfwnVtv/MyPq5kBiVhfV/qglMIxwwTVNEI1Ha1HmOgz5uHYHtYO6SfGckFaXe8blA11IJJ3wqXmvmBA==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.1.3", + "google-logging-utils": "^2.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.1.0.tgz", + "integrity": "sha512-YIBq5KNoHQrZ9PM4PeodDBqqBGlSTIxh1zibN8u3zNIO9UJg8gvKuCpdKV7fWXhsZ2zRkU6FhGrgKNgLJ2IRgw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "^9.0.0", + "google-logging-utils": "^2.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-gax": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-6.10.0.tgz", + "integrity": "sha512-P9+6mNoC97uSUj55MptrxNj9muoDwc05mcY49R8jyg3kqzkPK+uWHApLuAHsNPKh3dtk1M1IuVZIto8e/fvpQA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "@opentelemetry/api": "^1.9.0", + "duplexify": "^4.1.3", + "google-auth-library": "^11.0.0", + "google-logging-utils": "^2.0.0", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^4.0.0", + "protobufjs": "^7.5.4", + "retry-request": "^9.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-logging-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.0.1.tgz", + "integrity": "sha512-HMhaQghlOTvbcb3c4T5jmmOMtG3JUF1iOQMezaJXL86CDS+Tm2vHd0IeLFRAx3+ewd+bo9E1HFHoy17X5aJa9A==", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + } + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/proto3-json-serializer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-4.0.2.tgz", + "integrity": "sha512-VUQlb/J2WjOG8BKhdLwJUz+MKxSMtBGYh+TaNmRtiYVhlIzXjzeAGIx7gtc3lXlA2iURzfF2+cXaQvC2ofwzpg==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.5.4" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry-request": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-9.0.1.tgz", + "integrity": "sha512-4kGHEBl0ME6gR+8QB1sPMyg58jUxLUcCZNeqLDrfqzk0eWQN8XJFmeu7fmZCjlZCRW0S2u64Ik1HkO/0sWgCYQ==", + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^11.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, + "node_modules/teeny-request": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-11.0.1.tgz", + "integrity": "sha512-bNr5j2YjSdajgCVsp+8JVjRf1uHIbpNISLeKp/7V1NnVMX3Gh6H/kECNGlm2Q3I68ACODQ0iv6aZ3Rvm8WgLGA==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/packages/google_cloud_firestore/tool/pipeline_api_parity/package.json b/packages/google_cloud_firestore/tool/pipeline_api_parity/package.json new file mode 100644 index 00000000..5482c5b5 --- /dev/null +++ b/packages/google_cloud_firestore/tool/pipeline_api_parity/package.json @@ -0,0 +1,17 @@ +{ + "name": "pipeline-api-parity", + "private": true, + "description": "Extracts the Node.js Firestore Pipelines API into test/fixtures/node_pipeline_api.json.", + "license": "Apache-2.0", + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "extract": "node extract_node_api.mjs" + }, + "dependencies": { + "@google-cloud/firestore": "9.3.1", + "typescript": "5.9.3" + } +} diff --git a/packages/google_cloud_firestore/tool/pipeline_golden/.pubignore b/packages/google_cloud_firestore/tool/pipeline_golden/.pubignore new file mode 100644 index 00000000..ecfd1949 --- /dev/null +++ b/packages/google_cloud_firestore/tool/pipeline_golden/.pubignore @@ -0,0 +1,2 @@ +# The Node.js fixture generator is a development tool, not part of the package. +* diff --git a/packages/google_cloud_firestore/tool/pipeline_golden/README.md b/packages/google_cloud_firestore/tool/pipeline_golden/README.md new file mode 100644 index 00000000..8a49d341 --- /dev/null +++ b/packages/google_cloud_firestore/tool/pipeline_golden/README.md @@ -0,0 +1,101 @@ +# Pipeline golden fixtures + +`test/pipeline_golden_test.dart` checks that every Firestore Pipeline request +this package sends is identical to the one the Node.js SDK +(`@google-cloud/firestore`, pinned in `package.json`) sends for the same +Pipeline. The expected requests come from the Node SDK itself, never from this +package, so a test cannot pass by asserting our own output. + +This directory holds the Node side: `cases.js` builds a corpus of named +Pipelines with the Node SDK, and `generate.js` writes the request Node sends for +each one to `test/fixtures/pipeline_golden/.json`. + +## Regenerating + +Requires Node.js 22 or later. From this directory: + +```sh +npm ci +npm run generate +``` + +Then run `dart test test/pipeline_golden_test.dart` from the package root. + +Regenerate when you: + +- **add or change a case** in `cases.js` (add the matching Dart case to + `test/pipeline_golden_test.dart` in the same change), or +- **bump the pinned Node SDK**: change the exact version in `package.json`, + run `npm install` to update `package-lock.json`, regenerate, and review the + fixture diff; it is the list of wire-format changes Node made. + +Never edit the fixtures by hand. CI (`.github/workflows/pipeline_golden.yml`) +regenerates them and fails if the committed files are stale. + +## How capture works + +Nothing touches the network. `generate.js` creates a `Firestore` for project +`test-project`, database `(default)` (the ids the Dart test uses), and replaces +`Firestore.requestStream`, the single call the SDK makes to run +`executePipeline`, with a stub that records the request and returns an empty +response stream. Everything before it is the SDK's own code: +`Pipeline.execute(options)`, user data validation and `StructuredPipeline` +serialization. + +The recorded `ExecutePipelineRequest` is converted to proto3 JSON the way +google-gax does for the REST transport (`Type.fromObject` + `toProto3JSON`, +using the SDK's own `protobufjs`, protos and `proto3-json-serializer`). That is +the same encoding the Dart SDK sends, via +`ExecutePipelineRequest.toJson()`. + +Both sides then apply the same normalization before comparing: + +- object keys are sorted (`name` first, for readability); protobuf maps are + unordered, +- empty `args`, `options`, `fields` and `values` are dropped: proto3 JSON omits + empty repeated and map fields, and protobufjs keeps some (`options: {}` on + every stage). An empty `mapValue` or `arrayValue` is kept: it is a value. + +Numbers compare by value, so a `doubleValue` of `1` (JavaScript) equals `1.0` +(Dart). Every value is deterministic: fixed timestamps, no generated IDs. + +## The corpus + +Case ids are `//`, and each area has its own fixture +file: + +| Area | Covers | +| --- | --- | +| `sources` | `collection` (path, nested path, reference), `collectionGroup`, `database`, `documents` | +| `queries` | `createFrom` of collection, collection-group and vector queries: every filter operator, composite filters, orderings, implicit orderings, limits, `limitToLast`, offsets, cursors, projections | +| `stages` | every stage Node exposes, with each argument shape and option | +| `options` | `execute` options: `indexMode`, `explainOptions`, `rawOptions` | +| `values` | constants of every type, field paths, variables, plain values and collections in value positions | +| `functions` | every Node Pipeline function | +| `aggregates` | every aggregate function | + +Function variants follow one scheme. `static-...` calls the top-level function +(`PipelineFunctions.x`, or a top-level helper such as `equal`, in Dart) and +`method-...` the `Expression` method (`PipelineExpression` in Dart). +`field-name` is a field passed as a string, `expression` an `Expression`, +`literal` a plain value. Functions are wrapped in the smallest Pipeline that +takes them: `aggregate(fn.as('result'))`, `where(fn)` for booleans, otherwise +`select(fn.as('result'))`. + +`generate.js` fails if any top-level function or `Expression` method the Node +SDK exports has no case, so a Node upgrade that adds a function also adds a +gap to fill. + +## When Dart and Node differ + +Every fixture id needs a Dart case in `test/pipeline_golden_test.dart`, and +every Dart case a fixture. A difference that is not listed fails the test with +a path-by-path diff. Three lists in the test document the exceptions, and each +entry is itself checked: + +- `_knownDivergences`: Dart deliberately sends something else. The entry's + check asserts the intended Dart shape. +- `_pendingFixes`: an unintended difference awaiting a fix. The test fails + once it matches Node, so the entry gets removed. +- `_dartApiGaps`: Node APIs Dart cannot express. The test fails if a Dart case + is added for one. diff --git a/packages/google_cloud_firestore/tool/pipeline_golden/cases.js b/packages/google_cloud_firestore/tool/pipeline_golden/cases.js new file mode 100644 index 00000000..c8fc1544 --- /dev/null +++ b/packages/google_cloud_firestore/tool/pipeline_golden/cases.js @@ -0,0 +1,1615 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +/** + * The golden corpus: named Pipelines built with the Node.js SDK. + * + * Every case id must have a matching entry in + * `test/pipeline_golden_test.dart`, which builds the same Pipeline with the + * Dart SDK and compares the requests. + * + * Id scheme: `//`. + * + * - `static-...` variants call the top-level function + * (`PipelineFunctions.x` or a top-level helper in Dart), `method-...` + * variants call the `Expression` method (`PipelineExpression` in Dart). + * - `field-name` is a field passed as a plain string, `expression` an + * `Expression`, `literal` a plain value. + * + * Function cases are wrapped in a minimal Pipeline over `books`: + * aggregate functions in `aggregate(.as('result'))`, boolean + * expressions in `where()`, and everything else in + * `select(.as('result'))`. + * + * @param {object} sdk The loaded `@google-cloud/firestore` module. + * @param {FirebaseFirestore.Firestore} db The Firestore instance under test. + * @returns {Array<{id: string, pipeline: () => object, options?: object}>} + */ +module.exports = function buildCases(sdk, db) { + const {FieldPath, FieldValue, Filter, GeoPoint, Timestamp, Pipelines: P} = + sdk; + const {field, constant, variable} = P; + + const cases = []; + const ids = new Set(); + + /** Registers a case whose Pipeline is built lazily by [pipeline]. */ + function add(id, pipeline, options) { + if (ids.has(id)) throw new Error(`Duplicate case id: ${id}`); + ids.add(id); + cases.push({id, pipeline, options}); + } + + const books = () => db.pipeline().collection('books'); + + /** Wraps an expression in the smallest Pipeline that accepts it. */ + function wrap(expr) { + if (expr instanceof P.AggregateFunction) { + return books().aggregate(expr.as('result')); + } + if (expr instanceof P.BooleanExpression) { + return books().where(expr); + } + return books().select(expr.as('result')); + } + + /** Registers one function case per entry of [variants]. */ + function fn(area, name, variants) { + for (const [variant, makeExpr] of Object.entries(variants)) { + add(`${area}/${name}/${variant}`, () => wrap(makeExpr())); + } + } + + /** + * A function of one target, accepted as a field name or an expression. + * + * `static-field-name`: `fn('target')`, `static-expression`: + * `fn(field('target'))`, `method`: `field('target').method()`. + */ + function unary(name, target, {area = 'functions', method = name} = {}) { + fn(area, name, { + 'static-field-name': () => P[name](target), + 'static-expression': () => P[name](field(target)), + method: () => field(target)[method](), + }); + } + + /** + * A function of a target and a second operand. + * + * The target is a field name or an expression; the operand is [literal] + * or the expression built by [operand]. + */ + function binary(name, target, literal, operand, {method = name} = {}) { + fn('functions', name, { + 'static-field-name-literal': () => P[name](target, literal), + 'static-field-name-expression': () => P[name](target, operand()), + 'static-expression-literal': () => P[name](field(target), literal), + 'static-expression-expression': () => P[name](field(target), operand()), + 'method-literal': () => field(target)[method](literal), + 'method-expression': () => field(target)[method](operand()), + }); + } + + // Fixed, deterministic values. + const timestamp = new Timestamp(1700000000, 123456789); + const date = new Date(Date.UTC(2024, 0, 2, 3, 4, 5, 678)); + const geoPoint = new GeoPoint(37.7749, -122.4194); + const bytes = new Uint8Array([1, 2, 3, 255]); + const vector = FieldValue.vector([0.1, 0.2, 0.3]); + const docRef = () => db.doc('books/book1'); + + // --------------------------------------------------------------------------- + // Sources + // --------------------------------------------------------------------------- + + add('sources/collection/path', () => db.pipeline().collection('books')); + add('sources/collection/nested-path', () => + db.pipeline().collection('authors/author1/books'), + ); + add('sources/collection/reference', () => + db.pipeline().collection(db.collection('books')), + ); + add('sources/collection/force-index', () => + db.pipeline().collection({collection: 'books', forceIndex: 'my_index'}), + ); + add('sources/collection/raw-options', () => + db.pipeline().collection({ + collection: 'books', + rawOptions: {foo: 'bar', 'outer.inner': 1}, + }), + ); + add('sources/collection/reference-raw-options', () => + db.pipeline().collection({ + collection: db.collection('books'), + rawOptions: {foo: 'bar'}, + }), + ); + add('sources/collection-group/id', () => + db.pipeline().collectionGroup('reviews'), + ); + add('sources/collection-group/force-index', () => + db + .pipeline() + .collectionGroup({collectionId: 'reviews', forceIndex: 'my_index'}), + ); + add('sources/collection-group/raw-options', () => + db.pipeline().collectionGroup({ + collectionId: 'reviews', + rawOptions: {foo: 'bar'}, + }), + ); + add('sources/database/plain', () => db.pipeline().database()); + add('sources/database/raw-options', () => + db.pipeline().database({rawOptions: {foo: 'bar'}}), + ); + add('sources/documents/paths', () => + db.pipeline().documents(['books/book1', 'books/book2']), + ); + add('sources/documents/references', () => + db.pipeline().documents([db.doc('books/book1'), db.doc('books/book2')]), + ); + add('sources/documents/nested', () => + db.pipeline().documents([db.doc('authors/author1/books/book1')]), + ); + add('sources/documents/raw-options', () => + db.pipeline().documents({ + docs: [db.doc('books/book1')], + rawOptions: {foo: 'bar'}, + }), + ); + add('sources/documents/nested-path', () => + db.pipeline().documents(['authors/author1/books/book1']), + ); + add('sources/documents/leading-slash-path', () => + db.pipeline().documents(['/books/book1']), + ); + add('sources/documents/paths-and-references', () => + db.pipeline().documents(['books/book1', db.doc('books/book2')]), + ); + add('sources/subcollection/as-array-expression', () => + db + .pipeline() + .collection('books') + .addFields( + P.subcollection('reviews') + .select('rating') + .toArrayExpression() + .as('reviews'), + ), + ); + + // --------------------------------------------------------------------------- + // createFrom(query) + // --------------------------------------------------------------------------- + + const fromQuery = query => () => db.pipeline().createFrom(query()); + const col = () => db.collection('books'); + const query = (id, q) => add(`queries/${id}`, fromQuery(q)); + + query('collection/plain', () => col()); + query('collection/nested-path', () => db.collection('authors/author1/books')); + query('collection-group/plain', () => db.collectionGroup('reviews')); + query('collection-group/where', () => + db.collectionGroup('reviews').where('rating', '>', 3), + ); + query('collection-group/not-equal', () => + db.collectionGroup('reviews').where('rating', '!=', 3), + ); + + query('where/less-than', () => col().where('rating', '<', 4)); + query('where/less-than-or-equal', () => col().where('rating', '<=', 4)); + query('where/equal', () => col().where('genre', '==', 'Fantasy')); + query('where/not-equal', () => col().where('genre', '!=', 'Fantasy')); + query('where/greater-than-or-equal', () => col().where('rating', '>=', 4)); + query('where/greater-than', () => col().where('rating', '>', 4)); + query('where/array-contains', () => + col().where('tags', 'array-contains', 'magic'), + ); + query('where/in', () => col().where('genre', 'in', ['Fantasy', 'Sci-Fi'])); + query('where/not-in', () => + col().where('genre', 'not-in', ['Fantasy', 'Sci-Fi']), + ); + query('where/array-contains-any', () => + col().where('tags', 'array-contains-any', ['magic', 'epic']), + ); + query('where/equal-null', () => col().where('genre', '==', null)); + query('where/not-equal-null', () => col().where('genre', '!=', null)); + query('where/equal-nan', () => col().where('rating', '==', NaN)); + query('where/double', () => col().where('rating', '>', 4.5)); + query('where/timestamp', () => col().where('published', '<', timestamp)); + query('where/nested-field', () => + col().where('metadata.lang', '==', 'en'), + ); + query('where/special-characters', () => + col().where('first-name', '==', 'Ada'), + ); + query('where/field-path-object', () => + col().where(new FieldPath('a.b'), '==', 1), + ); + query('where/document-id', () => + col().where(FieldPath.documentId(), '==', 'book1'), + ); + query('where/multiple', () => + col().where('genre', '==', 'Fantasy').where('rating', '>', 4), + ); + query('where/composite-and', () => + col().where( + Filter.and( + Filter.where('genre', '==', 'Fantasy'), + Filter.where('rating', '>', 4), + ), + ), + ); + query('where/composite-or', () => + col().where( + Filter.or( + Filter.where('genre', '==', 'Fantasy'), + Filter.where('rating', '>', 4), + ), + ), + ); + query('where/composite-nested', () => + col().where( + Filter.or( + Filter.and( + Filter.where('genre', '==', 'Fantasy'), + Filter.where('rating', '>', 4), + ), + Filter.where('published', '<', 1950), + ), + ), + ); + + query('order-by/ascending', () => col().orderBy('rating')); + query('order-by/descending', () => col().orderBy('rating', 'desc')); + query('order-by/multiple', () => + col().orderBy('rating', 'desc').orderBy('title'), + ); + query('order-by/document-id', () => col().orderBy(FieldPath.documentId())); + query('order-by/special-characters', () => col().orderBy('first-name')); + query('order-by/field-path-object', () => + col().orderBy(new FieldPath('a.b')), + ); + query('order-by/implicit-from-inequality', () => + col().where('rating', '>', 4), + ); + query('order-by/implicit-from-inequalities', () => + col().where('rating', '>', 4).where('published', '<', 2000), + ); + query('order-by/explicit-and-inequality', () => + col().where('rating', '>', 4).orderBy('title', 'desc'), + ); + // '!=' and 'not-in' are inequalities too: the backend orders by their + // fields, together with the range fields, in field path order. + query('order-by/implicit-from-mixed-inequalities', () => + col().where('rating', '>', 4).where('genre', '!=', 'Horror'), + ); + query('order-by/implicit-from-repeated-field', () => + col().where('rating', '>', 2).where('rating', '<', 5), + ); + query('order-by/implicit-from-composite-or', () => + col().where( + Filter.or( + Filter.where('rating', '>', 4), + Filter.where('genre', 'not-in', ['Horror', 'Romance']), + ), + ), + ); + // Field paths sort segment by segment, not by their quoted form. + query('order-by/implicit-field-path-order', () => + col().where('price-tier', '<', 3).where('price.amount', '>', 1), + ); + query('order-by/implicit-quoted-segments', () => + col() + .where('field', '>=', 'field 100') + .where(new FieldPath('field.dot'), '!=', 300) + .where('field\\slash', '<', 400) + .orderBy('name', 'desc'), + ); + query('order-by/not-equal-explicitly-ordered', () => + col().where('genre', '!=', 'Horror').orderBy('genre', 'desc'), + ); + query('order-by/not-in-and-descending', () => + col() + .where('genre', 'not-in', ['Horror', 'Romance']) + .orderBy('rating', 'desc'), + ); + query('order-by/document-id-and-inequality', () => + col().where(FieldPath.documentId(), '>', 'book1').where('rating', '<', 5), + ); + + query('limit/with-order-by', () => col().orderBy('rating').limit(5)); + query('limit/without-order-by', () => col().limit(5)); + query('limit-to-last/plain', () => col().orderBy('rating').limitToLast(5)); + query('limit-to-last/with-inequality', () => + col().where('genre', '!=', 'Horror').orderBy('rating').limitToLast(2), + ); + query('offset/plain', () => col().offset(10)); + query('offset/with-limit', () => col().orderBy('rating').limit(5).offset(10)); + + query('cursor/start-at', () => col().orderBy('rating').startAt(4)); + query('cursor/start-after', () => col().orderBy('rating').startAfter(4)); + query('cursor/end-at', () => col().orderBy('rating').endAt(4)); + query('cursor/end-before', () => col().orderBy('rating').endBefore(4)); + query('cursor/multiple-fields', () => + col().orderBy('rating').orderBy('title').startAt(4, 'M'), + ); + query('cursor/start-and-end', () => + col().orderBy('rating').startAfter(2).endAt(4), + ); + query('cursor/limit-to-last', () => + col().orderBy('rating').startAt(2).endBefore(5).limitToLast(3), + ); + // A cursor is a position in the query's order: on a descending ordering, + // startAt keeps the smaller values. + query('cursor/start-at-descending', () => + col().orderBy('rating', 'desc').startAt(4), + ); + query('cursor/end-before-descending', () => + col().orderBy('rating', 'desc').endBefore(2), + ); + query('cursor/mixed-directions', () => + col().orderBy('rating', 'desc').orderBy('title').startAfter(4, 'M'), + ); + query('cursor/descending-limit-to-last', () => + col().orderBy('rating', 'desc').startAt(4).endAt(1).limitToLast(2), + ); + query('cursor/with-inequality', () => + col().where('genre', '!=', 'Horror').orderBy('rating').startAfter(4), + ); + query('cursor/with-inequality-and-limit-to-last', () => + col() + .where('genre', 'not-in', ['Horror']) + .orderBy('rating') + .startAfter(2) + .endAt(4) + .limitToLast(3), + ); + + query('select/fields', () => col().select('title', 'author')); + query('select/empty', () => col().select()); + query('select/special-characters', () => + col().select('first-name', 'last name'), + ); + + const vectorQuery = (id, makeQuery) => query(`vector/${id}`, makeQuery); + const nearest = (options, base = col()) => + base.findNearest({ + vectorField: 'embedding', + queryVector: [0.1, 0.2, 0.3], + limit: 5, + distanceMeasure: 'EUCLIDEAN', + ...options, + }); + vectorQuery('euclidean', () => nearest({})); + vectorQuery('cosine', () => nearest({distanceMeasure: 'COSINE'})); + vectorQuery('dot-product', () => nearest({distanceMeasure: 'DOT_PRODUCT'})); + vectorQuery('vector-value', () => nearest({queryVector: vector})); + vectorQuery('with-prefilter', () => + nearest({}, col().where('genre', '==', 'Fantasy')), + ); + vectorQuery('distance-result-field', () => + nearest({distanceResultField: 'distance'}), + ); + vectorQuery('distance-threshold', () => nearest({distanceThreshold: 0.5})); + vectorQuery('special-characters', () => + nearest({vectorField: 'my embedding'}), + ); + vectorQuery('field-path-object', () => + nearest({vectorField: new FieldPath('a.b')}), + ); + vectorQuery('distance-threshold-cosine', () => + nearest({distanceMeasure: 'COSINE', distanceThreshold: 0.5}), + ); + vectorQuery('distance-threshold-dot-product', () => + nearest({distanceMeasure: 'DOT_PRODUCT', distanceThreshold: 0.5}), + ); + vectorQuery('distance-threshold-and-result-field', () => + nearest({distanceThreshold: 0.5, distanceResultField: 'distance'}), + ); + vectorQuery('with-inequality-prefilter', () => + nearest({}, col().where('genre', '!=', 'Horror')), + ); + + // --------------------------------------------------------------------------- + // Stages + // --------------------------------------------------------------------------- + + const stage = (id, pipeline) => add(`stages/${id}`, pipeline); + + stage('where/condition', () => + books().where(field('rating').greaterThan(4)), + ); + stage('where/chained', () => + books() + .where(field('rating').greaterThan(4)) + .where(field('genre').equal('Fantasy')), + ); + stage('where/raw-options', () => + books().where({ + condition: field('rating').greaterThan(4), + rawOptions: {foo: 'bar', 'outer.inner': true}, + }), + ); + + stage('select/field-names', () => books().select('title', 'author')); + stage('select/fields', () => + books().select(field('title'), field('author')), + ); + stage('select/aliased', () => + books().select( + field('title').as('name'), + field('rating').multiply(2).as('doubled'), + ), + ); + stage('select/mixed', () => + books().select('title', field('author'), field('rating').as('score')), + ); + stage('select/nested-field-path', () => + books().select('metadata.lang', field('awards.hugo')), + ); + stage('select/special-characters', () => + books().select('first-name', field('last name')), + ); + stage('select/options-object', () => + books().select({selections: ['title', field('rating').as('score')]}), + ); + stage('select/raw-options', () => + books().select({selections: ['title'], rawOptions: {foo: 'bar'}}), + ); + + stage('add-fields/single', () => + books().addFields(field('rating').as('copiedRating')), + ); + stage('add-fields/multiple', () => + books().addFields( + field('rating').as('copiedRating'), + constant(true).as('annotated'), + ), + ); + stage('add-fields/field', () => books().addFields(field('rating'))); + stage('add-fields/raw-options', () => + books().addFields({ + fields: [field('rating').as('copiedRating')], + rawOptions: {foo: 'bar'}, + }), + ); + + stage('remove-fields/field-names', () => + books().removeFields('title', 'author'), + ); + stage('remove-fields/fields', () => + books().removeFields(field('title'), field('author')), + ); + stage('remove-fields/nested', () => books().removeFields('metadata.lang')); + stage('remove-fields/raw-options', () => + books().removeFields({fields: ['title'], rawOptions: {foo: 'bar'}}), + ); + stage('remove-fields/special-characters', () => + books().removeFields('first-name', field('last name')), + ); + + stage('sort/ascending-method', () => books().sort(field('rating').ascending())); + stage('sort/descending-method', () => + books().sort(field('rating').descending()), + ); + stage('sort/multiple', () => + books().sort(field('rating').descending(), field('title').ascending()), + ); + stage('sort/top-level-field-name', () => + books().sort(P.ascending('rating'), P.descending('title')), + ); + stage('sort/top-level-expression', () => + books().sort( + P.ascending(field('rating')), + P.descending(field('title').charLength()), + ), + ); + stage('sort/expression-method', () => + books().sort(field('title').charLength().descending()), + ); + stage('sort/raw-options', () => + books().sort({ + orderings: [field('rating').descending()], + rawOptions: {foo: 'bar'}, + }), + ); + stage('sort/special-characters', () => + books().sort(P.ascending('first-name'), field('last name').descending()), + ); + + stage('offset/plain', () => books().offset(10)); + stage('offset/raw-options', () => + books().offset({offset: 10, rawOptions: {foo: 'bar'}}), + ); + stage('limit/plain', () => books().limit(10)); + stage('limit/zero', () => books().limit(0)); + stage('limit/raw-options', () => + books().limit({limit: 10, rawOptions: {foo: 'bar'}}), + ); + + stage('distinct/field-names', () => books().distinct('genre', 'author')); + stage('distinct/fields', () => books().distinct(field('genre'))); + stage('distinct/aliased', () => + books().distinct(field('genre').toLower().as('lowerGenre')), + ); + stage('distinct/raw-options', () => + books().distinct({groups: ['genre'], rawOptions: {foo: 'bar'}}), + ); + stage('distinct/special-characters', () => + books().distinct('first-name', field('last name')), + ); + + stage('aggregate/single', () => books().aggregate(P.countAll().as('total'))); + stage('aggregate/multiple', () => + books().aggregate( + P.countAll().as('total'), + P.average('rating').as('averageRating'), + ), + ); + stage('aggregate/groups-field-names', () => + books().aggregate({ + accumulators: [P.countAll().as('total')], + groups: ['genre'], + }), + ); + stage('aggregate/groups-mixed', () => + books().aggregate({ + accumulators: [ + P.countAll().as('total'), + P.maximum('rating').as('best'), + ], + groups: [ + 'genre', + field('author'), + field('published').divide(10).floor().as('decade'), + ], + }), + ); + stage('aggregate/raw-options', () => + books().aggregate({ + accumulators: [P.countAll().as('total')], + groups: ['genre'], + rawOptions: {foo: 'bar'}, + }), + ); + stage('aggregate/groups-special-characters', () => + books().aggregate({ + accumulators: [P.countAll().as('total')], + groups: ['first-name', field('last name')], + }), + ); + + const findNearest = options => + books().findNearest({ + field: 'embedding', + vectorValue: [0.1, 0.2, 0.3], + distanceMeasure: 'euclidean', + ...options, + }); + stage('find-nearest/number-array', () => findNearest({})); + stage('find-nearest/integer-array', () => + findNearest({vectorValue: [1, 2, 3]}), + ); + stage('find-nearest/vector-value', () => findNearest({vectorValue: vector})); + stage('find-nearest/field', () => findNearest({field: field('embedding')})); + stage('find-nearest/cosine', () => findNearest({distanceMeasure: 'cosine'})); + stage('find-nearest/dot-product', () => + findNearest({distanceMeasure: 'dot_product'}), + ); + stage('find-nearest/limit', () => findNearest({limit: 10})); + stage('find-nearest/distance-field', () => + findNearest({distanceField: 'distance'}), + ); + stage('find-nearest/all-options', () => + findNearest({limit: 10, distanceField: 'distance'}), + ); + // rawOptions override the typed options they share a name with. + stage('find-nearest/raw-options', () => + findNearest({ + limit: 10, + distanceField: 'distance', + rawOptions: {limit: 20, 'extra.flag': true}, + }), + ); + stage('find-nearest/special-characters', () => + findNearest({field: 'my embedding', distanceField: 'my distance'}), + ); + + stage('replace-with/field-name', () => books().replaceWith('metadata')); + stage('replace-with/field', () => books().replaceWith(field('metadata'))); + stage('replace-with/map-expression', () => + books().replaceWith( + P.map({title: field('title'), rating: field('rating')}), + ), + ); + stage('replace-with/raw-options', () => + books().replaceWith({map: 'metadata', rawOptions: {foo: 'bar'}}), + ); + + stage('sample/documents', () => books().sample(10)); + stage('sample/documents-option', () => books().sample({documents: 10})); + stage('sample/percentage', () => books().sample({percentage: 0.25})); + stage('sample/raw-options', () => + books().sample({documents: 10, rawOptions: {foo: 'bar'}}), + ); + + stage('union/plain', () => + books().union(db.pipeline().collection('magazines')), + ); + stage('union/complex', () => + books() + .select('title') + .union( + db + .pipeline() + .collectionGroup('reviews') + .where(field('rating').greaterThan(3)) + .select('title'), + ), + ); + stage('union/raw-options', () => + books().union({ + other: db.pipeline().collection('magazines'), + rawOptions: {foo: 'bar'}, + }), + ); + + stage('unnest/field', () => books().unnest(field('tags'))); + stage('unnest/aliased-field', () => books().unnest(field('tags').as('tag'))); + stage('unnest/index-field', () => + books().unnest(field('tags').as('tag'), 'index'), + ); + stage('unnest/expression', () => + books().unnest(P.array([1, 2, field('rating')]).as('item')), + ); + stage('unnest/options-object', () => + books().unnest({selectable: field('tags').as('tag'), indexField: 'idx'}), + ); + stage('unnest/raw-options', () => + books().unnest({ + selectable: field('tags').as('tag'), + indexField: 'idx', + rawOptions: {index_field: field('position'), foo: 'bar'}, + }), + ); + stage('unnest/special-characters', () => + books().unnest(field('tags').as('my tag'), 'tag index'), + ); + stage('unnest/field-special-characters', () => + books().unnest(field('my tags')), + ); + + stage('raw-stage/expression-params', () => + books().rawStage('where', [field('rating').greaterThan(4)]), + ); + stage('raw-stage/literal-params', () => books().rawStage('limit', [10])); + stage('raw-stage/string-params', () => + books().rawStage('custom', ['a', 1.5, true, null]), + ); + stage('raw-stage/plain-object-param', () => + books().rawStage('select', [{title: field('title'), one: 1}]), + ); + stage('raw-stage/nested-plain-object-param', () => + books().rawStage('select', [{meta: {lang: field('lang')}}]), + ); + // A plain object param is a literal map whose values go through + // valueToDefaultExpr: a nested object or array becomes a map(...) or + // array(...) function, with or without expressions in it. + stage('raw-stage/plain-object-param-literals', () => + books().rawStage('custom', [{a: 1, b: 'x', c: null}]), + ); + stage('raw-stage/empty-object-param', () => books().rawStage('custom', [{}])); + stage('raw-stage/nested-plain-object-param-literals', () => + books().rawStage('custom', [{meta: {lang: 'en', year: 2020}}]), + ); + stage('raw-stage/nested-empty-object-param', () => + books().rawStage('custom', [{meta: {}}]), + ); + stage('raw-stage/array-in-object-param', () => + books().rawStage('custom', [{tags: [field('genre'), 'classic']}]), + ); + stage('raw-stage/array-in-object-param-literals', () => + books().rawStage('custom', [{tags: [1, 2]}]), + ); + stage('raw-stage/deeply-nested-object-param', () => + books().rawStage('custom', [ + {a: {b: {c: field('x')}, d: [field('y'), {e: 1}]}}, + ]), + ); + stage('raw-stage/constant-in-object-param', () => + books().rawStage('custom', [{meta: constant({lang: 'en'})}]), + ); + stage('raw-stage/array-param', () => + books().rawStage('custom', [[1, 'a', field('title')]]), + ); + // Any other param, an array included, is a constant: a literal value all + // the way down. + stage('raw-stage/array-param-literals', () => + books().rawStage('custom', [[1, 2, 3]]), + ); + stage('raw-stage/nested-array-param', () => + books().rawStage('custom', [[1, [2, field('x')]]]), + ); + stage('raw-stage/object-in-array-param', () => + books().rawStage('custom', [[{a: field('x'), b: {c: 1}}]]), + ); + stage('raw-stage/aggregate-params', () => + books().rawStage('aggregate', [ + {total: P.countAll(), best: P.maximum('rating')}, + {genre: field('genre')}, + ]), + ); + stage('raw-stage/options', () => + books().rawStage( + 'find_nearest', + [field('embedding'), vector, 'euclidean'], + {limit: 10, distance_field: field('distance')}, + ), + ); + stage('raw-stage/options-dot-notation', () => + books().rawStage('custom', [], {'outer.inner': 'value'}), + ); + stage('raw-stage/options-nested-object', () => + books().rawStage('custom', [], {outer: {inner: 'value', n: 1}}), + ); + stage('raw-stage/options-expression-values', () => + books().rawStage('custom', [], { + target: field('x'), + nested: {target: field('y')}, + }), + ); + stage('raw-stage/options-dot-notation-siblings', () => + books().rawStage('custom', [], {'outer.a': 1, 'outer.b': 2}), + ); + stage('raw-stage/options-dot-notation-deep', () => + books().rawStage('custom', [], {'a.b.c': true}), + ); + // Keys apply in order: a dotted key merges into an earlier object, an + // object replaces what earlier dotted keys built, and a dotted key + // replaces an earlier value that is not a map. + stage('raw-stage/options-dot-notation-merges-object', () => + books().rawStage('custom', [], {outer: {a: 1}, 'outer.b': 2}), + ); + stage('raw-stage/options-object-replaces-dot-notation', () => + books().rawStage('custom', [], {'outer.a': 1, outer: {b: 2}}), + ); + stage('raw-stage/options-dot-notation-replaces-scalar', () => + books().rawStage('custom', [], {outer: 'x', 'outer.a': 1}), + ); + // Segments are not unescaped: backticks are part of the name. + stage('raw-stage/options-dot-notation-backticks', () => + books().rawStage('custom', [], {'a.`b.c`': 1}), + ); + + stage('search/query-string', () => books().search({query: 'breakfast'})); + stage('search/query-expression', () => + books().search({query: P.documentMatches('breakfast')}), + ); + stage('search/limit-offset', () => + books().search({query: 'breakfast', limit: 10, offset: 5}), + ); + stage('search/retrieval-depth-language', () => + books().search({ + query: 'breakfast', + retrievalDepth: 100, + languageCode: 'en', + }), + ); + stage('search/sort-single', () => + books().search({query: 'breakfast', sort: P.score().descending()}), + ); + stage('search/sort-list', () => + books().search({ + query: 'breakfast', + sort: [P.score().descending(), field('title').ascending()], + }), + ); + stage('search/add-fields', () => + books().search({ + query: 'breakfast', + addFields: [P.score().as('searchScore')], + }), + ); + stage('search/raw-options', () => + books().search({ + query: 'breakfast', + limit: 10, + rawOptions: {limit: 20, 'extra.flag': true}, + }), + ); + + stage('define/plain', () => + books().define(field('rating').as('currentRating')), + ); + stage('delete/plain', () => books().delete()); + stage('update/plain', () => + books().update([field('rating').add(1).as('rating')]), + ); + stage('to-scalar-expression/plain', () => + books().addFields( + db + .pipeline() + .collection('reviews') + .aggregate(P.average('rating').as('avg')) + .toScalarExpression() + .as('averageRating'), + ), + ); + + // --------------------------------------------------------------------------- + // Execute options + // --------------------------------------------------------------------------- + + const execute = (id, options) => add(`options/${id}`, books, options); + + execute('none/plain', undefined); + execute('none/empty-object', {}); + execute('index-mode/recommended', {indexMode: 'recommended'}); + execute('explain/mode-analyze', {explainOptions: {mode: 'analyze'}}); + execute('explain/mode-execute', {explainOptions: {mode: 'execute'}}); + execute('explain/output-format-text', {explainOptions: {outputFormat: 'text'}}); + execute('explain/mode-and-output-format', { + explainOptions: {mode: 'analyze', outputFormat: 'text'}, + }); + execute('explain/empty', {explainOptions: {}}); + execute('combined/index-mode-and-explain', { + indexMode: 'recommended', + explainOptions: {mode: 'analyze', outputFormat: 'text'}, + }); + execute('raw-options/flat', {rawOptions: {foo: 'bar', count: 3}}); + execute('raw-options/nested-object', { + rawOptions: {explain_options: {mode: 'analyze'}}, + }); + execute('raw-options/dot-notation', { + rawOptions: {'explain_options.mode': 'analyze'}, + }); + execute('raw-options/override-known', { + indexMode: 'recommended', + rawOptions: {index_mode: 'custom'}, + }); + execute('raw-options/merge-into-explain', { + explainOptions: {mode: 'analyze'}, + rawOptions: {'explain_options.output_format': 'text'}, + }); + execute('raw-options/dot-notation-overrides-known', { + explainOptions: {mode: 'analyze', outputFormat: 'text'}, + rawOptions: {'explain_options.mode': 'execute'}, + }); + execute('raw-options/object-replaces-known', { + explainOptions: {mode: 'analyze', outputFormat: 'text'}, + rawOptions: {explain_options: {mode: 'execute'}}, + }); + execute('raw-options/dot-notation-into-empty-explain', { + explainOptions: {}, + rawOptions: {'explain_options.mode': 'analyze'}, + }); + execute('raw-options/dot-notation-replaces-known-scalar', { + indexMode: 'recommended', + rawOptions: {'index_mode.mode': 'custom'}, + }); + execute('raw-options/dot-notation-siblings', { + rawOptions: {'outer.a': 1, 'outer.b': 'two'}, + }); + execute('raw-options/dot-notation-deep', {rawOptions: {'a.b.c': true}}); + + // --------------------------------------------------------------------------- + // Values: constants, fields, variables and plain values in value positions + // --------------------------------------------------------------------------- + + const selectConstant = (id, value) => + add(`values/constant/${id}`, () => + books().select(constant(value).as('result')), + ); + selectConstant('integer', 42); + selectConstant('negative-integer', -7); + selectConstant('zero', 0); + selectConstant('double', 3.14); + selectConstant('nan', NaN); + selectConstant('infinity', Infinity); + selectConstant('negative-infinity', -Infinity); + selectConstant('string', 'Fantasy'); + selectConstant('empty-string', ''); + selectConstant('true', true); + selectConstant('false', false); + selectConstant('null', null); + selectConstant('timestamp', timestamp); + selectConstant('timestamp-millis', new Timestamp(1700000000, 123000000)); + selectConstant('date', date); + selectConstant('geo-point', geoPoint); + selectConstant('bytes', bytes); + add('values/constant/document-reference', () => + books().select(constant(docRef()).as('result')), + ); + selectConstant('vector', vector); + selectConstant('array', [1, 'a', true, null]); + selectConstant('map', {a: 1, b: 'x', nested: {c: [1, 2]}}); + + const selectField = (id, name) => + add(`values/field/${id}`, () => books().select(field(name).as('result'))); + selectField('simple', 'title'); + selectField('nested', 'metadata.lang'); + selectField('document-id', '__name__'); + selectField('special-characters', 'first-name'); + selectField('space', 'last name'); + selectField('non-ascii', 'naïve'); + selectField('backtick', 'a`b'); + selectField('backslash', 'a\\b'); + selectField('reserved-characters', 'a/b'); + add('values/field/field-path-object', () => + books().select(field(new FieldPath('a.b', 'c')).as('result')), + ); + add('values/field/field-name-argument', () => + books().where(P.equal('first-name', 'Ada')), + ); + + add('values/variable/plain', () => + books().select(variable('x').as('result')), + ); + + // A plain value in a value position, carried by `equal`. + const equalValue = (id, makeValue) => + add(`values/value-position/${id}`, () => + books().where(field('value').equal(makeValue())), + ); + equalValue('integer', () => 1); + equalValue('double', () => 1.5); + equalValue('string', () => 'a'); + equalValue('boolean', () => true); + equalValue('null', () => null); + equalValue('timestamp', () => timestamp); + equalValue('date', () => date); + equalValue('geo-point', () => geoPoint); + equalValue('bytes', () => bytes); + equalValue('document-reference', docRef); + equalValue('vector', () => vector); + equalValue('list-plain', () => ['a', 'b']); + equalValue('list-with-expression', () => ['a', field('b')]); + equalValue('list-nested', () => [['a'], {k: 'v'}]); + equalValue('list-empty', () => []); + equalValue('map-plain', () => ({a: 1, b: 'x'})); + equalValue('map-with-expression', () => ({a: field('x'), b: 1})); + equalValue('map-nested', () => ({inner: {k: field('y')}, list: [field('z')]})); + equalValue('map-empty', () => ({})); + equalValue('constant-list', () => constant(['a', 'b'])); + + // The "supported data types" Node system test. + add('values/data-types/all-constants', () => + books() + .limit(1) + .select( + constant(1).as('number'), + constant('a string').as('string'), + constant(true).as('boolean'), + constant(null).as('null'), + constant(geoPoint).as('geoPoint'), + constant(timestamp).as('timestamp'), + constant(date).as('date'), + constant(bytes).as('bytes'), + constant(docRef()).as('documentReference'), + constant(vector).as('vectorValue'), + P.map({ + number: 1, + string: 'a string', + boolean: true, + null: null, + geoPoint, + timestamp, + date, + uint8Array: bytes, + documentReference: docRef(), + vectorValue: vector, + map: {number: 2, string: 'b string'}, + array: [1, 'c string'], + }).as('map'), + P.array([ + 1, + 'a string', + true, + null, + geoPoint, + timestamp, + date, + bytes, + docRef(), + vector, + {number: 2, string: 'b string'}, + ]).as('array'), + ), + ); + // The "converts arrays and plain objects to functionValues" Node system + // test. + add('values/data-types/array-and-map-functions', () => + books() + .sort(field('rating').descending()) + .limit(1) + .select('title', 'author', 'genre', 'rating', 'published', 'tags') + .addFields( + P.array([ + 1, + 2, + field('genre'), + P.multiply('rating', 10), + [field('title')], + {published: field('published')}, + ]).as('metadataArray'), + P.map({ + genre: field('genre'), + rating: P.multiply('rating', 10), + nestedArray: [field('title')], + nestedMap: {published: field('published')}, + }).as('metadata'), + ) + .where( + P.and( + P.equal('metadataArray', [ + 1, + 2, + field('genre'), + P.multiply('rating', 10), + [field('title')], + {published: field('published')}, + ]), + P.equal('metadata', { + genre: field('genre'), + rating: P.multiply('rating', 10), + nestedArray: [field('title')], + nestedMap: {published: field('published')}, + }), + ), + ), + ); + + // --------------------------------------------------------------------------- + // Functions: arithmetic + // --------------------------------------------------------------------------- + + binary('add', 'rating', 1, () => field('bonus')); + fn('functions', 'add', { + 'method-double': () => field('rating').add(0.5), + 'method-variadic': () => field('rating').add(1, field('bonus')), + 'static-variadic': () => P.add('rating', 1, field('bonus')), + }); + binary('subtract', 'rating', 1, () => field('penalty')); + binary('multiply', 'rating', 2, () => field('weight')); + fn('functions', 'multiply', { + 'method-variadic': () => field('rating').multiply(2, field('weight')), + 'static-variadic': () => P.multiply('rating', 2, field('weight')), + }); + binary('divide', 'rating', 2, () => field('count')); + binary('mod', 'rating', 2, () => field('divisor')); + binary('pow', 'rating', 2, () => field('exponent')); + for (const name of ['abs', 'ceil', 'floor', 'exp', 'ln', 'sqrt', 'log10']) { + unary(name, 'rating'); + } + for (const name of ['round', 'trunc']) { + unary(name, 'rating'); + fn('functions', name, { + 'static-field-name-places-literal': () => P[name]('rating', 2), + 'static-expression-places-expression': () => + P[name](field('rating'), field('places')), + 'method-places-literal': () => field('rating')[name](2), + 'method-places-expression': () => field('rating')[name](field('places')), + }); + } + fn('functions', 'rand', {static: () => P.rand()}); + + // --------------------------------------------------------------------------- + // Functions: comparison + // --------------------------------------------------------------------------- + + for (const name of [ + 'equal', + 'notEqual', + 'lessThan', + 'lessThanOrEqual', + 'greaterThan', + 'greaterThanOrEqual', + ]) { + binary(name, 'rating', 4, () => field('threshold')); + fn('functions', name, { + 'static-field-name-string': () => P[name]('genre', 'Fantasy'), + }); + } + + // --------------------------------------------------------------------------- + // Functions: logical + // --------------------------------------------------------------------------- + + const isFantasy = () => field('genre').equal('Fantasy'); + const isGood = () => field('rating').greaterThan(4); + const isOld = () => field('published').lessThan(1950); + + for (const name of ['and', 'or', 'xor', 'nor']) { + fn('functions', name, { + 'static-two': () => P[name](isFantasy(), isGood()), + 'static-three': () => P[name](isFantasy(), isGood(), isOld()), + }); + } + fn('functions', 'not', { + static: () => P.not(isFantasy()), + method: () => isFantasy().not(), + }); + fn('functions', 'conditional', { + static: () => P.conditional(isGood(), field('title'), constant('n/a')), + method: () => isGood().conditional(field('title'), constant('n/a')), + }); + fn('functions', 'switchOn', { + 'static-pair': () => P.switchOn(isGood(), constant('good')), + 'static-pairs-and-default': () => + P.switchOn( + isGood(), + constant('good'), + isOld(), + constant('old'), + constant('other'), + ), + }); + fn('functions', 'logicalMaximum', { + 'static-field-name-literal': () => P.logicalMaximum('rating', 1), + 'static-expression-variadic': () => + P.logicalMaximum(field('rating'), field('floor'), 3), + 'method-literal': () => field('rating').logicalMaximum(1), + 'method-variadic': () => field('rating').logicalMaximum(field('floor'), 3), + }); + fn('functions', 'logicalMinimum', { + 'static-field-name-literal': () => P.logicalMinimum('rating', 5), + 'static-expression-variadic': () => + P.logicalMinimum(field('rating'), field('ceiling'), 3), + 'method-literal': () => field('rating').logicalMinimum(5), + 'method-variadic': () => + field('rating').logicalMinimum(field('ceiling'), 3), + }); + + // --------------------------------------------------------------------------- + // Functions: existence, null and error handling + // --------------------------------------------------------------------------- + + unary('exists', 'title'); + unary('isAbsent', 'title'); + binary('ifAbsent', 'nickname', 'anonymous', () => field('name')); + binary('ifNull', 'nickname', 'anonymous', () => field('name')); + const failing = () => field('rating').divide(0); + fn('functions', 'isError', { + 'static-expression': () => P.isError(failing()), + method: () => failing().isError(), + }); + fn('functions', 'ifError', { + 'static-expression-literal': () => P.ifError(failing(), 0), + 'static-expression-expression': () => + P.ifError(failing(), field('fallback')), + 'static-boolean-boolean': () => P.ifError(isGood(), constant(false)), + 'method-literal': () => failing().ifError(0), + 'method-expression': () => failing().ifError(field('fallback')), + 'method-boolean-boolean': () => isGood().ifError(constant(false)), + 'method-boolean-literal': () => isGood().ifError(false), + }); + fn('functions', 'coalesce', { + 'static-field-name-literal': () => P.coalesce('nickname', 'anonymous'), + 'static-expression-variadic': () => + P.coalesce(field('nickname'), field('name'), 'anonymous'), + 'method-literal': () => field('nickname').coalesce('anonymous'), + 'method-variadic': () => + field('nickname').coalesce(field('name'), 'anonymous'), + }); + + // --------------------------------------------------------------------------- + // Functions: strings + // --------------------------------------------------------------------------- + + for (const name of [ + 'charLength', + 'byteLength', + 'toLower', + 'toUpper', + 'stringReverse', + 'reverse', + 'length', + ]) { + unary(name, 'title'); + } + binary('like', 'title', 'The%', () => field('pattern')); + binary('regexContains', 'title', 'Lord', () => field('pattern')); + binary('regexMatch', 'title', '^The.*', () => field('pattern')); + binary('regexFind', 'title', '[A-Z]\\w+', () => field('pattern')); + binary('regexFindAll', 'title', '[A-Z]\\w+', () => field('pattern')); + binary('stringContains', 'title', 'Ring', () => field('needle')); + binary('startsWith', 'title', 'The', () => field('prefix')); + binary('endsWith', 'title', 'Rings', () => field('suffix')); + binary('stringIndexOf', 'title', 'Ring', () => field('needle')); + binary('stringRepeat', 'title', 2, () => field('times')); + binary('split', 'csv', ',', () => field('delimiter')); + binary('join', 'tags', ', ', () => field('delimiter')); + for (const name of ['trim', 'ltrim', 'rtrim']) { + unary(name, 'title'); + fn('functions', name, { + 'static-field-name-literal': () => P[name]('title', '-'), + 'static-expression-expression': () => + P[name](field('title'), field('chars')), + 'method-literal': () => field('title')[name]('-'), + 'method-expression': () => field('title')[name](field('chars')), + }); + } + fn('functions', 'stringConcat', { + 'static-field-name-literal': () => P.stringConcat('title', ' by '), + 'static-field-name-variadic': () => + P.stringConcat('title', ' by ', field('author')), + 'static-expression-expression': () => + P.stringConcat(field('title'), field('author')), + 'method-literal': () => field('title').stringConcat(' by '), + 'method-variadic': () => field('title').stringConcat(' by ', field('author')), + }); + fn('functions', 'concat', { + 'static-field-name-literal': () => P.concat('title', '!'), + 'static-expression-variadic': () => + P.concat(field('title'), ' by ', field('author')), + 'method-literal': () => field('title').concat('!'), + 'method-list': () => field('tags').concat(['extra']), + 'method-variadic': () => field('title').concat(' by ', field('author')), + }); + for (const name of ['stringReplaceAll', 'stringReplaceOne']) { + fn('functions', name, { + 'static-field-name-literals': () => P[name]('title', 'a', 'b'), + 'static-expression-expressions': () => + P[name](field('title'), field('find'), field('replacement')), + 'method-literals': () => field('title')[name]('a', 'b'), + 'method-expressions': () => + field('title')[name](field('find'), field('replacement')), + }); + } + fn('functions', 'substring', { + 'static-field-name-position': () => P.substring('title', 2), + 'static-field-name-position-length': () => P.substring('title', 2, 3), + 'static-expression-expressions': () => + P.substring(field('title'), field('start'), field('count')), + 'method-position': () => field('title').substring(2), + 'method-position-length': () => field('title').substring(2, 3), + 'method-expressions': () => + field('title').substring(field('start'), field('count')), + }); + + // --------------------------------------------------------------------------- + // Functions: arrays + // --------------------------------------------------------------------------- + + fn('functions', 'array', { + 'static-literals': () => P.array([1, 'a', true, null]), + 'static-with-expressions': () => P.array([field('title'), 1]), + 'static-nested': () => P.array([[1, field('a')], {k: field('b')}]), + 'static-empty': () => P.array([]), + }); + fn('functions', 'arrayConcat', { + 'static-field-name-list': () => P.arrayConcat('tags', ['x', 'y']), + 'static-field-name-expression': () => + P.arrayConcat('tags', field('moreTags')), + 'static-expression-variadic': () => + P.arrayConcat(field('tags'), field('moreTags'), ['z']), + 'method-list': () => field('tags').arrayConcat(['x', 'y']), + 'method-expression': () => field('tags').arrayConcat(field('moreTags')), + 'method-variadic': () => + field('tags').arrayConcat(field('moreTags'), ['z']), + }); + binary('arrayContains', 'tags', 'magic', () => field('wanted')); + for (const name of ['arrayContainsAll', 'arrayContainsAny']) { + fn('functions', name, { + 'static-field-name-list': () => P[name]('tags', ['magic', 'epic']), + 'static-field-name-list-with-expression': () => + P[name]('tags', ['magic', field('wanted')]), + 'static-field-name-list-nested': () => P[name]('tags', ['magic', ['epic']]), + 'static-field-name-array-expression': () => + P[name]('tags', field('wanted')), + 'static-expression-list': () => P[name](field('tags'), ['magic', 'epic']), + 'method-list': () => field('tags')[name](['magic', 'epic']), + 'method-list-with-expression': () => + field('tags')[name](['magic', field('wanted')]), + 'method-array-expression': () => field('tags')[name](field('wanted')), + }); + } + for (const name of ['equalAny', 'notEqualAny']) { + fn('functions', name, { + 'static-field-name-list': () => P[name]('genre', ['Fantasy', 'Sci-Fi']), + 'static-field-name-list-with-expression': () => + P[name]('genre', ['Fantasy', field('favorite')]), + 'static-field-name-list-nested': () => + P[name]('genre', ['Fantasy', ['Sci-Fi']]), + 'static-field-name-array-expression': () => + P[name]('genre', field('genres')), + 'static-expression-list': () => + P[name](field('genre'), ['Fantasy', 'Sci-Fi']), + 'method-list': () => field('genre')[name](['Fantasy', 'Sci-Fi']), + 'method-list-with-expression': () => + field('genre')[name](['Fantasy', field('favorite')]), + 'method-array-expression': () => field('genre')[name](field('genres')), + }); + } + for (const name of [ + 'arrayLength', + 'arrayFirst', + 'arrayLast', + 'arrayReverse', + 'arraySum', + 'arrayMaximum', + 'arrayMinimum', + ]) { + unary(name, 'tags'); + } + for (const name of [ + 'arrayFirstN', + 'arrayLastN', + 'arrayMaximumN', + 'arrayMinimumN', + ]) { + binary(name, 'scores', 2, () => field('n')); + } + binary('arrayGet', 'tags', 1, () => field('index')); + binary('arrayIndexOf', 'tags', 'magic', () => field('wanted')); + binary('arrayLastIndexOf', 'tags', 'magic', () => field('wanted')); + binary('arrayIndexOfAll', 'tags', 'magic', () => field('wanted')); + fn('functions', 'arraySlice', { + 'static-field-name-offset': () => P.arraySlice('tags', 1), + 'static-field-name-offset-length': () => P.arraySlice('tags', 1, 2), + 'static-expression-expressions': () => + P.arraySlice(field('tags'), field('start'), field('count')), + 'method-offset': () => field('tags').arraySlice(1), + 'method-offset-length': () => field('tags').arraySlice(1, 2), + 'method-expressions': () => + field('tags').arraySlice(field('start'), field('count')), + }); + const isMagic = () => variable('tag').equal('magic'); + fn('functions', 'arrayFilter', { + 'static-field-name': () => P.arrayFilter('tags', 'tag', isMagic()), + 'static-expression': () => P.arrayFilter(field('tags'), 'tag', isMagic()), + method: () => field('tags').arrayFilter('tag', isMagic()), + }); + const upper = () => variable('tag').toUpper(); + fn('functions', 'arrayTransform', { + 'static-field-name': () => P.arrayTransform('tags', 'tag', upper()), + 'static-expression': () => P.arrayTransform(field('tags'), 'tag', upper()), + method: () => field('tags').arrayTransform('tag', upper()), + }); + const indexed = () => variable('tag').stringConcat(variable('i')); + fn('functions', 'arrayTransformWithIndex', { + 'static-field-name': () => + P.arrayTransformWithIndex('tags', 'tag', 'i', indexed()), + 'static-expression': () => + P.arrayTransformWithIndex(field('tags'), 'tag', 'i', indexed()), + method: () => field('tags').arrayTransformWithIndex('tag', 'i', indexed()), + }); + + // --------------------------------------------------------------------------- + // Functions: maps + // --------------------------------------------------------------------------- + + fn('functions', 'map', { + 'static-literals': () => P.map({a: 1, b: 'x'}), + 'static-with-expressions': () => P.map({a: field('title'), b: 1}), + 'static-nested': () => + P.map({inner: {k: field('y')}, list: [field('z'), 1]}), + 'static-empty': () => P.map({}), + }); + fn('functions', 'mapGet', { + 'static-field-name': () => P.mapGet('metadata', 'lang'), + 'static-expression': () => P.mapGet(field('metadata'), 'lang'), + method: () => field('metadata').mapGet('lang'), + }); + fn('functions', 'mapSet', { + 'static-field-name-literal': () => P.mapSet('metadata', 'lang', 'en'), + 'static-expression-variadic': () => + P.mapSet(field('metadata'), 'lang', 'en', 'pages', field('pageCount')), + 'method-literal': () => field('metadata').mapSet('lang', 'en'), + 'method-expression-key': () => + field('metadata').mapSet(field('key'), field('value')), + 'method-variadic': () => + field('metadata').mapSet('lang', 'en', 'pages', field('pageCount')), + }); + binary('mapRemove', 'metadata', 'lang', () => field('key')); + fn('functions', 'mapMerge', { + 'static-field-name-map': () => P.mapMerge('metadata', {extra: 1}), + 'static-expression-expression': () => + P.mapMerge(field('metadata'), field('overrides')), + 'static-field-name-variadic': () => + P.mapMerge('metadata', {extra: field('title')}, field('overrides')), + 'method-map': () => field('metadata').mapMerge({extra: 1}), + 'method-variadic': () => + field('metadata').mapMerge({extra: field('title')}, field('overrides')), + }); + for (const name of ['mapKeys', 'mapValues', 'mapEntries']) { + unary(name, 'metadata'); + } + binary('getField', 'metadata', 'lang', () => field('key')); + + // --------------------------------------------------------------------------- + // Functions: timestamps + // --------------------------------------------------------------------------- + + for (const name of [ + 'unixMicrosToTimestamp', + 'unixMillisToTimestamp', + 'unixSecondsToTimestamp', + 'timestampToUnixMicros', + 'timestampToUnixMillis', + 'timestampToUnixSeconds', + ]) { + unary(name, 'published'); + } + for (const name of ['timestampAdd', 'timestampSubtract']) { + fn('functions', name, { + 'static-field-name-literals': () => P[name]('published', 'day', 1), + 'static-expression-literals': () => + P[name](field('published'), 'hour', 2), + 'static-expression-expressions': () => + P[name](field('published'), field('unit'), field('amount')), + 'method-literals': () => field('published')[name]('minute', 30), + 'method-expressions': () => + field('published')[name](field('unit'), field('amount')), + }); + } + fn('functions', 'timestampTruncate', { + 'static-field-name-literal': () => P.timestampTruncate('published', 'day'), + 'static-field-name-literal-timezone': () => + P.timestampTruncate('published', 'week(monday)', 'America/New_York'), + 'static-expression-expression': () => + P.timestampTruncate(field('published'), field('granularity')), + 'static-expression-timezone-expression': () => + P.timestampTruncate(field('published'), 'month', field('tz')), + 'method-literal': () => field('published').timestampTruncate('year'), + 'method-literal-timezone': () => + field('published').timestampTruncate('day', 'UTC'), + 'method-expression-timezone-expression': () => + field('published').timestampTruncate(field('granularity'), field('tz')), + }); + fn('functions', 'timestampExtract', { + 'static-field-name-literal': () => P.timestampExtract('published', 'year'), + 'static-field-name-literal-timezone': () => + P.timestampExtract('published', 'dayofweek', 'America/New_York'), + 'static-expression-expression': () => + P.timestampExtract(field('published'), field('part')), + 'static-expression-timezone-expression': () => + P.timestampExtract(field('published'), 'month', field('tz')), + 'method-literal': () => field('published').timestampExtract('dayofyear'), + 'method-literal-timezone': () => + field('published').timestampExtract('hour', 'UTC'), + 'method-expression-timezone-expression': () => + field('published').timestampExtract(field('part'), field('tz')), + }); + fn('functions', 'timestampDiff', { + 'static-field-names': () => P.timestampDiff('end', 'start', 'day'), + 'static-field-name-expression': () => + P.timestampDiff('end', field('start'), 'second'), + 'static-expression-field-name': () => + P.timestampDiff(field('end'), 'start', 'hour'), + 'static-expressions-unit-expression': () => + P.timestampDiff(field('end'), field('start'), field('unit')), + 'method-field-name': () => field('end').timestampDiff('start', 'day'), + 'method-expressions': () => + field('end').timestampDiff(field('start'), field('unit')), + }); + fn('functions', 'currentTimestamp', {static: () => P.currentTimestamp()}); + + // --------------------------------------------------------------------------- + // Functions: types + // --------------------------------------------------------------------------- + + unary('type', 'value'); + fn('functions', 'isType', { + 'static-field-name': () => P.isType('value', 'string'), + 'static-expression': () => P.isType(field('value'), 'string'), + }); + for (const type of [ + 'null', + 'array', + 'boolean', + 'bytes', + 'timestamp', + 'geo_point', + 'number', + 'int32', + 'int64', + 'float64', + 'decimal128', + 'map', + 'reference', + 'string', + 'vector', + 'max_key', + 'min_key', + 'object_id', + 'regex', + 'request_timestamp', + ]) { + fn('functions', 'isType', { + [`method-${type.replace(/_/g, '-')}`]: () => field('value').isType(type), + }); + } + + // --------------------------------------------------------------------------- + // Functions: vectors and geo + // --------------------------------------------------------------------------- + + for (const name of ['cosineDistance', 'dotProduct', 'euclideanDistance']) { + fn('functions', name, { + 'static-field-name-number-array': () => + P[name]('embedding', [0.1, 0.2, 0.3]), + 'static-field-name-integer-array': () => P[name]('embedding', [1, 2, 3]), + 'static-field-name-vector-value': () => P[name]('embedding', vector), + 'static-field-name-expression': () => + P[name]('embedding', field('other')), + 'static-expression-number-array': () => + P[name](field('embedding'), [0.1, 0.2, 0.3]), + 'static-expression-expression': () => + P[name](field('embedding'), field('other')), + 'method-number-array': () => field('embedding')[name]([0.1, 0.2, 0.3]), + 'method-vector-value': () => field('embedding')[name](vector), + 'method-expression': () => field('embedding')[name](field('other')), + }); + } + unary('vectorLength', 'embedding'); + fn('functions', 'geoDistance', { + 'static-field-name-geo-point': () => P.geoDistance('location', geoPoint), + 'static-field-geo-point': () => P.geoDistance(field('location'), geoPoint), + 'static-field-name-expression': () => + P.geoDistance('location', field('other')), + 'method-geo-point': () => field('location').geoDistance(geoPoint), + 'method-expression': () => field('location').geoDistance(field('other')), + }); + + // --------------------------------------------------------------------------- + // Functions: references, documents and search + // --------------------------------------------------------------------------- + + unary('collectionId', '__name__'); + for (const name of ['documentId', 'parent']) { + fn('functions', name, { + 'static-path': () => P[name]('books/book1'), + 'static-reference': () => P[name](docRef()), + 'static-expression': () => P[name](field('__name__')), + method: () => field('__name__')[name](), + }); + } + fn('functions', 'currentDocument', {static: () => P.currentDocument()}); + fn('functions', 'documentMatches', { + 'static-literal': () => P.documentMatches('breakfast'), + 'static-expression': () => P.documentMatches(field('query')), + }); + fn('functions', 'score', {static: () => P.score()}); + + // --------------------------------------------------------------------------- + // Aggregates + // --------------------------------------------------------------------------- + + fn('aggregates', 'countAll', {static: () => P.countAll()}); + for (const name of [ + 'count', + 'sum', + 'average', + 'minimum', + 'maximum', + 'first', + 'last', + 'arrayAgg', + 'arrayAggDistinct', + 'countDistinct', + ]) { + unary(name, 'rating', {area: 'aggregates'}); + } + fn('aggregates', 'countIf', { + static: () => P.countIf(isGood()), + method: () => isGood().countIf(), + }); + + return cases; +}; diff --git a/packages/google_cloud_firestore/tool/pipeline_golden/generate.js b/packages/google_cloud_firestore/tool/pipeline_golden/generate.js new file mode 100644 index 00000000..b0900495 --- /dev/null +++ b/packages/google_cloud_firestore/tool/pipeline_golden/generate.js @@ -0,0 +1,259 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +/** + * Writes the `ExecutePipelineRequest` the Node.js Firestore SDK sends for + * every case in `cases.js` to `test/fixtures/pipeline_golden/.json`. + * + * Nothing touches the network: `Firestore.requestStream`, the single call the + * SDK makes to run `executePipeline`, is replaced with a stub that records + * the request and returns an empty response stream. Everything up to that + * point (`Pipeline.execute`, user data validation, `StructuredPipeline` + * serialization) is the SDK's own code. + * + * The recorded request is converted to proto3 JSON the same way google-gax + * does for the REST transport (`Type.fromObject` + `toProto3JSON`), which is + * also the encoding the Dart SDK sends. See README.md for the normalization + * applied on top. + */ + +const fs = require('fs'); +const path = require('path'); +const {PassThrough} = require('stream'); + +const sdkPackage = require.resolve('@google-cloud/firestore/package.json'); +const sdkDir = path.dirname(sdkPackage); +const gaxDir = path.dirname(require.resolve('google-gax', {paths: [sdkDir]})); +// Resolve protobufjs and the proto3 JSON serializer the same way the SDK and +// google-gax do, so the encoding matches the REST transport exactly. +const protobuf = require(require.resolve('protobufjs', {paths: [sdkDir]})); +const {toProto3JSON} = require( + require.resolve('proto3-json-serializer', {paths: [gaxDir]}), +); + +const sdk = require('@google-cloud/firestore'); +const buildCases = require('./cases'); + +const SDK_VERSION = require(sdkPackage).version; +const PROJECT_ID = 'test-project'; +const DATABASE_ID = '(default)'; +const OUT_DIR = path.resolve( + __dirname, + '..', + '..', + 'test', + 'fixtures', + 'pipeline_golden', +); + +/** + * Repeated and map fields that proto3 JSON omits when empty. + * + * protobufjs keeps some of them (for example `options: {}` on every stage), + * the Dart SDK drops them; both mean the same thing on the wire. Message + * fields such as an empty `mapValue` are kept: they carry a value. + */ +const OMIT_WHEN_EMPTY = new Set(['args', 'options', 'fields', 'values']); + +/** + * Names that are deliberately not exercised as `functions//...` or + * `aggregates//...`, and where they are covered instead. + */ +const COVERED_ELSEWHERE = { + field: 'values/field/*', + constant: 'values/constant/*', + variable: 'values/variable/*, functions/arrayFilter/*', + ascending: 'stages/sort/*', + descending: 'stages/sort/*', + subcollection: 'sources/subcollection/*', +}; + +/** `Expression` methods that are not Pipeline functions. */ +const NOT_FUNCTIONS = new Set(['constructor', 'as', 'asBoolean']); + +function isEmpty(value) { + if (Array.isArray(value)) return value.length === 0; + return ( + value !== null && typeof value === 'object' && Object.keys(value).length === 0 + ); +} + +/** Orders keys alphabetically, except that a function or stage `name` leads. */ +function compareKeys(a, b) { + if (a === b) return 0; + if (a === 'name') return -1; + if (b === 'name') return 1; + return a < b ? -1 : 1; +} + +/** Sorts object keys and drops empty repeated/map fields, recursively. */ +function canonicalize(value) { + if (Array.isArray(value)) return value.map(canonicalize); + if (value === null || typeof value !== 'object') return value; + const result = {}; + for (const key of Object.keys(value).sort(compareKeys)) { + const child = canonicalize(value[key]); + if (OMIT_WHEN_EMPTY.has(key) && isEmpty(child)) continue; + result[key] = child; + } + return result; +} + +/** + * Pretty-prints [value] as JSON, keeping any object or array that fits in + * [LINE_WIDTH] columns on one line, so a fixture reads like the request: + * `{"fieldReferenceValue": "rating"}` instead of three lines. + */ +const LINE_WIDTH = 80; + +function format(value, indent = '', column = indent.length) { + const inline = formatInline(value); + if (column + inline.length <= LINE_WIDTH || value === null) return inline; + if (typeof value !== 'object') return inline; + + const inner = indent + ' '; + if (Array.isArray(value)) { + const items = value.map(item => inner + format(item, inner)); + return `[\n${items.join(',\n')}\n${indent}]`; + } + const entries = Object.keys(value).map(key => { + const prefix = `${inner}${JSON.stringify(key)}: `; + return prefix + format(value[key], inner, prefix.length); + }); + return `{\n${entries.join(',\n')}\n${indent}}`; +} + +function formatInline(value) { + if (Array.isArray(value)) { + return `[${value.map(formatInline).join(', ')}]`; + } + if (value !== null && typeof value === 'object') { + const entries = Object.keys(value).map( + key => `${JSON.stringify(key)}: ${formatInline(value[key])}`, + ); + return `{${entries.join(', ')}}`; + } + return JSON.stringify(value); +} + +/** Fails when a Node function or method is not exercised by any case. */ +function checkCoverage(ids) { + const P = sdk.Pipelines; + const subjects = new Set(); + const methodSubjects = new Set(); + for (const id of ids) { + const [area, subject, variant] = id.split('/'); + if (area !== 'functions' && area !== 'aggregates') continue; + subjects.add(subject); + if (variant.startsWith('method')) methodSubjects.add(subject); + } + + const missing = []; + for (const name of Object.keys(P).sort()) { + if (typeof P[name] !== 'function' || /^[A-Z]/.test(name)) continue; + if (!subjects.has(name) && !(name in COVERED_ELSEWHERE)) { + missing.push(`top-level function ${name}()`); + } + } + + const methods = new Set([ + ...Object.getOwnPropertyNames(P.Expression.prototype), + ...Object.getOwnPropertyNames(P.BooleanExpression.prototype), + ...Object.getOwnPropertyNames(P.Field.prototype), + ]); + for (const name of [...methods].sort()) { + if (NOT_FUNCTIONS.has(name) || name.startsWith('_')) continue; + if (name === 'fieldName' || name === 'ascending' || name === 'descending') { + continue; + } + if (!methodSubjects.has(name)) missing.push(`Expression.${name}()`); + } + + if (missing.length > 0) { + throw new Error( + 'These Node Pipeline APIs have no golden case:\n ' + + missing.join('\n '), + ); + } +} + +async function main() { + const db = new sdk.Firestore({projectId: PROJECT_ID, databaseId: DATABASE_ID}); + const requestType = protobuf.Root.fromJSON( + require(path.join(sdkDir, 'build', 'protos', 'v1.json')), + ).lookupType('google.firestore.v1.ExecutePipelineRequest'); + + let captured; + db.requestStream = async (methodName, bidirectional, request) => { + if (methodName !== 'executePipeline') { + throw new Error(`Unexpected RPC ${methodName}`); + } + captured = request; + const stream = new PassThrough({objectMode: true}); + stream.end(); + return stream; + }; + + const cases = buildCases(sdk, db); + checkCoverage(cases.map(c => c.id)); + + const areas = new Map(); + for (const {id, pipeline, options} of cases) { + captured = undefined; + try { + await pipeline().execute(options); + } catch (e) { + throw new Error(`Case ${id} failed: ${e.stack || e}`); + } + if (!captured) throw new Error(`Case ${id} sent no request`); + + const json = canonicalize(toProto3JSON(requestType.fromObject(captured))); + const area = id.split('/')[0]; + if (!areas.has(area)) areas.set(area, {}); + areas.get(area)[id] = json; + } + + fs.mkdirSync(OUT_DIR, {recursive: true}); + for (const file of fs.readdirSync(OUT_DIR)) { + if (file.endsWith('.json')) fs.rmSync(path.join(OUT_DIR, file)); + } + for (const [area, requests] of [...areas].sort(([a], [b]) => + a < b ? -1 : 1, + )) { + const sorted = {}; + for (const id of Object.keys(requests).sort()) sorted[id] = requests[id]; + const fixture = { + generator: 'tool/pipeline_golden (npm run generate)', + sdk: `@google-cloud/firestore@${SDK_VERSION}`, + cases: sorted, + }; + fs.writeFileSync( + path.join(OUT_DIR, `${area}.json`), + format(fixture) + '\n', + ); + } + + await db.terminate(); + const counts = [...areas] + .map(([area, requests]) => `${area}: ${Object.keys(requests).length}`) + .join(', '); + console.log(`Wrote ${cases.length} cases (${counts}) to ${OUT_DIR}`); +} + +main().catch(e => { + console.error(e); + process.exitCode = 1; +}); diff --git a/packages/google_cloud_firestore/tool/pipeline_golden/package-lock.json b/packages/google_cloud_firestore/tool/pipeline_golden/package-lock.json new file mode 100644 index 00000000..252c4b83 --- /dev/null +++ b/packages/google_cloud_firestore/tool/pipeline_golden/package-lock.json @@ -0,0 +1,1358 @@ +{ + "name": "pipeline-golden", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pipeline-golden", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/firestore": "9.3.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-9.3.1.tgz", + "integrity": "sha512-ofGTiSpfXrHrrS3KaDOmsupSzCHr4WVl1+s6Ke0F8rPBRyb/OuZSC8u1NjCKtt/fO7A1GvqASQGhdAwVsnGpQA==", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/firestore-api": "^0.2.0", + "@opentelemetry/api": "^1.9.0", + "fast-deep-equal": "^3.1.3", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^6.0.1", + "protobufjs": "^7.5.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@google-cloud/firestore-api": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore-api/-/firestore-api-0.2.0.tgz", + "integrity": "sha512-wfHJPZmLqzpy6FN6U+7HkkP/AKhGsbHSpEy/qL2TyI3/pKRAvpamQxZ2qvps5hMRgAjfuHCim+oeJj9RveCaqg==", + "license": "Apache-2.0", + "dependencies": { + "google-gax": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/gcp-metadata": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.4.tgz", + "integrity": "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "7.1.3", + "google-logging-utils": "1.1.3", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/google-gax": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.8.tgz", + "integrity": "sha512-M4vpZcXQIC1gqIVGQ7eaU3jXQA6zecStyTXu514TYfThlgSurYJOxHZo9fzU6hAgwPWvuEynAVHWyaIk80VeEA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "10.5.0", + "google-logging-utils": "1.1.3", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "3.0.4", + "protobufjs": "^7.5.4", + "retry-request": "^8.0.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/proto3-json-serializer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.4.tgz", + "integrity": "sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/retry-request": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.4.tgz", + "integrity": "sha512-pI6/7eabUYkZxamkOq0g0uMxKLLGnjzhefY+vL8bVXag5rto4OU2YBTPytWLuHH7aEKD6fn7kJycQfid0Mwnkw==", + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/firestore-api/node_modules/teeny-request": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.4.tgz", + "integrity": "sha512-R1Cg4Vu0UULeDfHL/kjABLaTW++9yD/B6n2g48y5dJ04hsEaxcfmAqbNDzNsbqAYJyIpZafjklLG9YxRu9uzOg==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.5.tgz", + "integrity": "sha512-7VZM+SVdEcUUqSQeNI3zM8Qs/BhQKZndPo2h5VkYkAM8Iz0wJIa8mKV5ekQGqG8UUsnkQ0NMxIxwkIHYvj0qOw==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "license": "MIT" + }, + "node_modules/gaxios": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-9.0.4.tgz", + "integrity": "sha512-p6WvbXOhfwnVtv/MyPq5kBiVhfV/qglMIxwwTVNEI1Ha1HmOgz5uHYHtYO6SfGckFaXe8blA11IJJ3wqXmvmBA==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.1.3", + "google-logging-utils": "^2.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.1.0.tgz", + "integrity": "sha512-YIBq5KNoHQrZ9PM4PeodDBqqBGlSTIxh1zibN8u3zNIO9UJg8gvKuCpdKV7fWXhsZ2zRkU6FhGrgKNgLJ2IRgw==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "^9.0.0", + "google-logging-utils": "^2.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-gax": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-6.10.0.tgz", + "integrity": "sha512-P9+6mNoC97uSUj55MptrxNj9muoDwc05mcY49R8jyg3kqzkPK+uWHApLuAHsNPKh3dtk1M1IuVZIto8e/fvpQA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "@opentelemetry/api": "^1.9.0", + "duplexify": "^4.1.3", + "google-auth-library": "^11.0.0", + "google-logging-utils": "^2.0.0", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^4.0.0", + "protobufjs": "^7.5.4", + "retry-request": "^9.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/google-logging-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-2.0.1.tgz", + "integrity": "sha512-HMhaQghlOTvbcb3c4T5jmmOMtG3JUF1iOQMezaJXL86CDS+Tm2vHd0IeLFRAx3+ewd+bo9E1HFHoy17X5aJa9A==", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + } + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/proto3-json-serializer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-4.0.2.tgz", + "integrity": "sha512-VUQlb/J2WjOG8BKhdLwJUz+MKxSMtBGYh+TaNmRtiYVhlIzXjzeAGIx7gtc3lXlA2iURzfF2+cXaQvC2ofwzpg==", + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.5.4" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry-request": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-9.0.1.tgz", + "integrity": "sha512-4kGHEBl0ME6gR+8QB1sPMyg58jUxLUcCZNeqLDrfqzk0eWQN8XJFmeu7fmZCjlZCRW0S2u64Ik1HkO/0sWgCYQ==", + "license": "MIT", + "dependencies": { + "extend": "^3.0.2", + "teeny-request": "^11.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, + "node_modules/teeny-request": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-11.0.1.tgz", + "integrity": "sha512-bNr5j2YjSdajgCVsp+8JVjRf1uHIbpNISLeKp/7V1NnVMX3Gh6H/kECNGlm2Q3I68ACODQ0iv6aZ3Rvm8WgLGA==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/packages/google_cloud_firestore/tool/pipeline_golden/package.json b/packages/google_cloud_firestore/tool/pipeline_golden/package.json new file mode 100644 index 00000000..8d2eff9c --- /dev/null +++ b/packages/google_cloud_firestore/tool/pipeline_golden/package.json @@ -0,0 +1,15 @@ +{ + "name": "pipeline-golden", + "private": true, + "description": "Generates golden ExecutePipelineRequest fixtures for google_cloud_firestore from the Node.js Firestore SDK.", + "license": "Apache-2.0", + "engines": { + "node": ">=22" + }, + "scripts": { + "generate": "node generate.js" + }, + "dependencies": { + "@google-cloud/firestore": "9.3.1" + } +}