diff --git a/.gitattributes b/.gitattributes index 6a04e7492..7f54ebe53 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,6 +4,7 @@ tests export-ignore benchmark export-ignore docs export-ignore examples export-ignore +generator export-ignore mongo-orchestration export-ignore stubs export-ignore tools export-ignore @@ -14,6 +15,13 @@ phpunit.evergreen.xml export-ignore phpunit.xml.dist export-ignore psalm.xml.dist export-ignore psalm-baseline.xml export-ignore +rector.php export-ignore # Prevent generated build files from showing diffs in pull requests .evergreen/config/generated/** linguist-generated=true +/src/Builder/Accumulator/*.php linguist-generated=true +/src/Builder/Expression/*.php linguist-generated=true +/src/Builder/Query/*.php linguist-generated=true +/src/Builder/Projection/*.php linguist-generated=true +/src/Builder/Stage/*.php linguist-generated=true +/tests/Builder/*/Pipelines.php linguist-generated=true diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index ba987c7e5..da58ba8a5 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -11,6 +11,9 @@ inputs: description: "INI values to pass along to setup-php action" required: false default: "" + working-directory: + description: "The directory where composer.json is located, if it is not in the repository root." + required: false runs: using: composite @@ -49,3 +52,4 @@ runs: # Revert when psalm supports PHP 8.4 # composer-options: "--no-suggest" composer-options: "--no-suggest ${{ inputs.php-version == '8.4' && '--ignore-platform-req=php+' || '' }}" + working-directory: "${{ inputs.working-directory }}" diff --git a/.github/workflows/generator.yml b/.github/workflows/generator.yml new file mode 100644 index 000000000..59d8eb186 --- /dev/null +++ b/.github/workflows/generator.yml @@ -0,0 +1,42 @@ +name: "Generator" + +on: + merge_group: + pull_request: + branches: + - "v*.*" + - "master" + - "feature/*" + push: + branches: + - "v*.*" + - "master" + - "feature/*" + +env: + PHP_VERSION: "8.2" + # TODO: change to "stable" once 1.20.0 is released + # DRIVER_VERSION: "stable" + DRIVER_VERSION: "mongodb/mongo-php-driver@v1.20" + +jobs: + psalm: + name: "Diff check" + runs-on: "ubuntu-22.04" + + steps: + - name: "Checkout" + uses: "actions/checkout@v4" + + - name: "Setup" + uses: "./.github/actions/setup" + with: + php-version: ${{ env.PHP_VERSION }} + driver-version: ${{ env.DRIVER_VERSION }} + working-directory: "generator" + + - name: "Run Generator" + run: "generator/generate" + + - name: "Check file diff" + run: git add . -N && git diff --exit-code diff --git a/composer.json b/composer.json index a6dd35b5a..9f2f2020f 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,9 @@ "symfony/phpunit-bridge": "^5.2", "vimeo/psalm": "^5.13" }, + "replace": { + "mongodb/builder": "*" + }, "autoload": { "psr-4": { "MongoDB\\": "src/" }, "files": [ "src/functions.php" ] diff --git a/generator/README.md b/generator/README.md new file mode 100644 index 000000000..5b6449543 --- /dev/null +++ b/generator/README.md @@ -0,0 +1,34 @@ +# Code Generator for MongoDB PHP Library + +This subproject is used to generate the code that is committed to the repository. +The `generator` directory is not included in `mongodb/mongodb` package and is not installed by Composer. + +## Contributing + +Updating the generated code can be done only by modifying the code generator, or its configuration. + +To run the generator, you need to have PHP 8.1+ installed and Composer. + +1. Move to the `generator` directory: `cd generator` +1. Install dependencies: `composer install` +1. Run the generator: `./generate` + +## Configuration + +The `generator/config/*.yaml` files contains the list of operators and stages that are supported by the library. + +### Test pipelines + +Each operator can contain a `tests` section with a list if pipelines. To represent specific BSON objects, +it is necessary to use Yaml tags: + +| BSON Type | Example | +|-------------|--------------------------------------------------------| +| Regex | `!bson_regex '^abc'`
`!bson_regex ['^abc', 'i']` | +| Int64 | `!bson_int64 '123456789'` | +| Decimal128 | `!bson_decimal128 '0.9'` | +| UTCDateTime | `!bson_utcdatetime 0` | +| Binary | `!bson_binary 'IA=='` | + +To add new test cases to operators, you can get inspiration from the official MongoDB documentation and use +the `generator/js2yaml.html` web page to manually convert a pipeline array from JS to Yaml. diff --git a/generator/composer.json b/generator/composer.json new file mode 100644 index 000000000..2deb4ae95 --- /dev/null +++ b/generator/composer.json @@ -0,0 +1,32 @@ +{ + "name": "mongodb/code-generator", + "type": "project", + "repositories": [ + { + "type": "path", + "url": "../", + "symlink": true + } + ], + "replace": { + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*" + }, + "require": { + "mongodb/mongodb": "@dev", + "nette/php-generator": "^4.1.5", + "nikic/php-parser": "^5", + "symfony/console": "^7", + "symfony/finder": "^7", + "symfony/yaml": "^7" + }, + "license": "Apache-2.0", + "autoload": { + "psr-4": { + "MongoDB\\CodeGenerator\\": "src/" + } + }, + "config": { + "sort-packages": true + } +} diff --git a/generator/config/accumulator/accumulator.yaml b/generator/config/accumulator/accumulator.yaml new file mode 100644 index 000000000..e90fbbf52 --- /dev/null +++ b/generator/config/accumulator/accumulator.yaml @@ -0,0 +1,133 @@ +# $schema: ../schema.json +name: $accumulator +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/' +type: + - accumulator +encode: object +description: | + Defines a custom accumulator function. + New in MongoDB 4.4. +arguments: + - + name: init + type: + - javascript + description: | + Function used to initialize the state. The init function receives its arguments from the initArgs array expression. You can specify the function definition as either BSON type Code or String. + - + name: initArgs + type: + - resolvesToArray + optional: true + description: | + Arguments passed to the init function. + - + name: accumulate + type: + - javascript + description: | + Function used to accumulate documents. The accumulate function receives its arguments from the current state and accumulateArgs array expression. The result of the accumulate function becomes the new state. You can specify the function definition as either BSON type Code or String. + - + name: accumulateArgs + type: + - resolvesToArray + description: | + Arguments passed to the accumulate function. You can use accumulateArgs to specify what field value(s) to pass to the accumulate function. + - + name: merge + type: + - javascript + description: | + Function used to merge two internal states. merge must be either a String or Code BSON type. merge returns the combined result of the two merged states. For information on when the merge function is called, see Merge Two States with $merge. + - + name: finalize + type: + - javascript + optional: true + description: | + Function used to update the result of the accumulation. + - + name: lang + type: + - string + description: | + The language used in the $accumulator code. + +tests: + - + name: 'Use $accumulator to Implement the $avg Operator' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use--accumulator-to-implement-the--avg-operator' + pipeline: + - + $group: + _id: '$author' + avgCopies: + $accumulator: + init: + $code: |- + function() { + return { count: 0, sum: 0 } + } + accumulate: + $code: |- + function(state, numCopies) { + return { count: state.count + 1, sum: state.sum + numCopies } + } + accumulateArgs: [ "$copies" ], + merge: + $code: |- + function(state1, state2) { + return { + count: state1.count + state2.count, + sum: state1.sum + state2.sum + } + } + finalize: + $code: |- + function(state) { + return (state.sum / state.count) + } + lang: 'js' + + - + name: 'Use initArgs to Vary the Initial State by Group' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/accumulator/#use-initargs-to-vary-the-initial-state-by-group' + pipeline: + - + $group: + _id: + city: '$city' + restaurants: + $accumulator: + init: + $code: |- + function(city, userProfileCity) { + return { max: city === userProfileCity ? 3 : 1, restaurants: [] } + } + initArgs: + - '$city' + - 'Bettles' + accumulate: + $code: |- + function(state, restaurantName) { + if (state.restaurants.length < state.max) { + state.restaurants.push(restaurantName); + } + return state; + } + accumulateArgs: + - '$name' + merge: + $code: |- + function(state1, state2) { + return { + max: state1.max, + restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) + } + } + finalize: + $code: |- + function(state) { + return state.restaurants + } + lang: 'js' diff --git a/generator/config/accumulator/addToSet.yaml b/generator/config/accumulator/addToSet.yaml new file mode 100644 index 000000000..9566899eb --- /dev/null +++ b/generator/config/accumulator/addToSet.yaml @@ -0,0 +1,47 @@ +# $schema: ../schema.json +name: $addToSet +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addToSet/' +type: + - accumulator + - window +encode: single +description: | + Returns an array of unique expression values for each group. Order of the array elements is undefined. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - expression + +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addToSet/#use-in--group-stage' + pipeline: + - $group: + _id: + day: + $dayOfYear: + date: '$date' + year: + $year: + date: '$date' + itemsSold: + $addToSet: '$item' + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addToSet/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + cakeTypesForState: + $addToSet: '$type' + window: + documents: + - 'unbounded' + - 'current' diff --git a/generator/config/accumulator/avg.yaml b/generator/config/accumulator/avg.yaml new file mode 100644 index 000000000..3777bbf98 --- /dev/null +++ b/generator/config/accumulator/avg.yaml @@ -0,0 +1,45 @@ +# $schema: ../schema.json +name: $avg +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg/' +type: + - accumulator + - window +encode: single +description: | + Returns an average of numerical values. Ignores non-numeric values. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - resolvesToNumber +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg/#use-in--group-stage' + pipeline: + - $group: + _id: '$item' + avgAmount: + $avg: + $multiply: + - '$price' + - '$quantity' + avgQuantity: + $avg: '$quantity' + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + averageQuantityForState: + $avg: '$quantity' + window: + documents: + - 'unbounded' + - 'current' diff --git a/generator/config/accumulator/bottom.yaml b/generator/config/accumulator/bottom.yaml new file mode 100644 index 000000000..4ebb897ea --- /dev/null +++ b/generator/config/accumulator/bottom.yaml @@ -0,0 +1,55 @@ +# $schema: ../schema.json +name: $bottom +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bottom/' +type: + - accumulator + - window +encode: object +description: | + Returns the bottom element within a group according to the specified sort order. + New in MongoDB 5.2: Available in the $group and $setWindowFields stages. +arguments: + - + name: sortBy + type: + - object # SortSpec + description: | + Specifies the order of results, with syntax similar to $sort. + - + name: output + type: + - expression + description: | + Represents the output for each element in the group and can be any expression. +tests: + - + name: 'Find the Bottom Score' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bottom/#find-the-bottom-score' + pipeline: + - + $match: + gameId: 'G1' + - + $group: + _id: '$gameId' + playerId: + $bottom: + output: + - '$playerId' + - '$score' + sortBy: + score: -1 + - + name: 'Finding the Bottom Score Across Multiple Games' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bottom/#finding-the-bottom-score-across-multiple-games' + pipeline: + - + $group: + _id: '$gameId' + playerId: + $bottom: + output: + - '$playerId' + - '$score' + sortBy: + score: -1 diff --git a/generator/config/accumulator/bottomN.yaml b/generator/config/accumulator/bottomN.yaml new file mode 100644 index 000000000..30bca678c --- /dev/null +++ b/generator/config/accumulator/bottomN.yaml @@ -0,0 +1,85 @@ +# $schema: ../schema.json +name: $bottomN +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bottomN/' +type: + - accumulator + - window +encode: object +description: | + Returns an aggregation of the bottom n elements within a group, according to the specified sort order. If the group contains fewer than n elements, $bottomN returns all elements in the group. + New in MongoDB 5.2. + Available in the $group and $setWindowFields stages. +arguments: + - + name: 'n' + type: + - resolvesToInt + description: | + Limits the number of results per group and has to be a positive integral expression that is either a constant or depends on the _id value for $group. + - + name: sortBy + type: + - object # SortSpec + description: | + Specifies the order of results, with syntax similar to $sort. + - + name: output + type: + - expression + description: | + Represents the output for each element in the group and can be any expression. +tests: + - + name: 'Find the Three Lowest Scores' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bottomN/#find-the-three-lowest-scores' + pipeline: + - + $match: + gameId: 'G1' + - + $group: + _id: '$gameId' + playerId: + $bottomN: + output: + - '$playerId' + - '$score' + sortBy: + score: -1 + n: 3 + - + name: 'Finding the Three Lowest Score Documents Across Multiple Games' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bottomN/#finding-the-three-lowest-score-documents-across-multiple-games' + pipeline: + - + $group: + _id: '$gameId' + playerId: + $bottomN: + output: + - '$playerId' + - '$score' + sortBy: + score: -1 + n: 3 + - + name: 'Computing n Based on the Group Key for $group' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bottomN/#computing-n-based-on-the-group-key-for--group' + pipeline: + - + $group: + _id: + gameId: '$gameId' + gamescores: + $bottomN: + output: '$score' + n: + $cond: + if: + $eq: + - '$gameId' + - 'G2' + then: 1 + else: 3 + sortBy: + score: -1 diff --git a/generator/config/accumulator/count.yaml b/generator/config/accumulator/count.yaml new file mode 100644 index 000000000..d9819056d --- /dev/null +++ b/generator/config/accumulator/count.yaml @@ -0,0 +1,37 @@ +# $schema: ../schema.json +name: $count +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/count-accumulator/' +type: + - accumulator + - window +encode: object +description: | + Returns the number of documents in the group or window. + Distinct from the $count pipeline stage. + New in MongoDB 5.0. +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/count-accumulator/#use-in--group-stage' + pipeline: + - + $group: + _id: '$state' + countNumberOfDocumentsForState: + $count: {} + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/count-accumulator/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + countNumberOfDocumentsForState: + $count: {} + window: + documents: + - 'unbounded' + - 'current' diff --git a/generator/config/accumulator/covariancePop.yaml b/generator/config/accumulator/covariancePop.yaml new file mode 100644 index 000000000..b43a24022 --- /dev/null +++ b/generator/config/accumulator/covariancePop.yaml @@ -0,0 +1,41 @@ +# $schema: ../schema.json +name: $covariancePop +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/covariancePop/' +type: + - window +encode: array +description: | + Returns the population covariance of two numeric expressions. + New in MongoDB 5.0. +arguments: + - + name: expression1 + type: + - resolvesToNumber + - + name: expression2 + type: + - resolvesToNumber +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/covariancePop/#example' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + covariancePopForState: + $covariancePop: + - + # Example uses the short form, the builder always generates the verbose form + # $year: '$orderDate' + $year: + date: '$orderDate' + - '$quantity' + window: + documents: + - 'unbounded' + - 'current' diff --git a/generator/config/accumulator/covarianceSamp.yaml b/generator/config/accumulator/covarianceSamp.yaml new file mode 100644 index 000000000..b6cc529af --- /dev/null +++ b/generator/config/accumulator/covarianceSamp.yaml @@ -0,0 +1,41 @@ +# $schema: ../schema.json +name: $covarianceSamp +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/covarianceSamp/' +type: + - window +encode: array +description: | + Returns the sample covariance of two numeric expressions. + New in MongoDB 5.0. +arguments: + - + name: expression1 + type: + - resolvesToNumber + - + name: expression2 + type: + - resolvesToNumber +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/covarianceSamp/#example' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + covarianceSampForState: + $covarianceSamp: + - + # Example uses the short form, the builder always generates the verbose form + # $year: '$orderDate' + $year: + date: '$orderDate' + - '$quantity' + window: + documents: + - 'unbounded' + - 'current' diff --git a/generator/config/accumulator/denseRank.yaml b/generator/config/accumulator/denseRank.yaml new file mode 100644 index 000000000..0c50dd901 --- /dev/null +++ b/generator/config/accumulator/denseRank.yaml @@ -0,0 +1,34 @@ +# $schema: ../schema.json +name: $denseRank +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/denseRank/' +type: + - window +encode: object +description: | + Returns the document position (known as the rank) relative to other documents in the $setWindowFields stage partition. There are no gaps in the ranks. Ties receive the same rank. + New in MongoDB 5.0. +tests: + - + name: 'Dense Rank Partitions by an Integer Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/denseRank/#dense-rank-partitions-by-an-integer-field' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + quantity: -1 + output: + denseRankQuantityForState: + $denseRank: {} + - + name: 'Dense Rank Partitions by a Date Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/denseRank/#dense-rank-partitions-by-a-date-field' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + denseRankOrderDateForState: + $denseRank: {} diff --git a/generator/config/accumulator/derivative.yaml b/generator/config/accumulator/derivative.yaml new file mode 100644 index 000000000..5745e9380 --- /dev/null +++ b/generator/config/accumulator/derivative.yaml @@ -0,0 +1,47 @@ +# $schema: ../schema.json +name: $derivative +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/derivative/' +type: + - window +encode: object +description: | + Returns the average rate of change within the specified window. + New in MongoDB 5.0. +arguments: + - + name: input + type: + - resolvesToNumber + - resolvesToDate + - + name: unit + type: + - timeUnit + optional: true + description: | + A string that specifies the time unit. Use one of these strings: "week", "day","hour", "minute", "second", "millisecond". + If the sortBy field is not a date, you must omit a unit. If you specify a unit, you must specify a date in the sortBy field. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/derivative/#example' + pipeline: + - + $setWindowFields: + partitionBy: '$truckID' + sortBy: + timeStamp: 1 + output: + truckAverageSpeed: + $derivative: + input: '$miles' + unit: 'hour' + window: + range: + - -30 + - 0 + unit: 'second' + - + $match: + truckAverageSpeed: + $gt: 50 diff --git a/generator/config/accumulator/documentNumber.yaml b/generator/config/accumulator/documentNumber.yaml new file mode 100644 index 000000000..b810ccd44 --- /dev/null +++ b/generator/config/accumulator/documentNumber.yaml @@ -0,0 +1,22 @@ +# $schema: ../schema.json +name: $documentNumber +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/documentNumber/' +type: + - window +encode: object +description: | + Returns the position of a document (known as the document number) in the $setWindowFields stage partition. Ties result in different adjacent document numbers. + New in MongoDB 5.0. +tests: + - + name: 'Document Number for Each State' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/documentNumber/#document-number-for-each-state' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + quantity: -1 + output: + documentNumberForState: + $documentNumber: {} diff --git a/generator/config/accumulator/expMovingAvg.yaml b/generator/config/accumulator/expMovingAvg.yaml new file mode 100644 index 000000000..3009dd115 --- /dev/null +++ b/generator/config/accumulator/expMovingAvg.yaml @@ -0,0 +1,60 @@ +# $schema: ../schema.json +name: $expMovingAvg +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/expMovingAvg/' +type: + - window +encode: object +description: | + Returns the exponential moving average for the numeric expression. + New in MongoDB 5.0. +arguments: + - + name: input + type: + - resolvesToNumber + - + name: 'N' + type: + - int + optional: true + description: | + An integer that specifies the number of historical documents that have a significant mathematical weight in the exponential moving average calculation, with the most recent documents contributing the most weight. + You must specify either N or alpha. You cannot specify both. + The N value is used in this formula to calculate the current result based on the expression value from the current document being read and the previous result of the calculation: + - + name: alpha + type: + - double + optional: true + description: | + A double that specifies the exponential decay value to use in the exponential moving average calculation. A higher alpha value assigns a lower mathematical significance to previous results from the calculation. + You must specify either N or alpha. You cannot specify both. +tests: + - + name: 'Exponential Moving Average Using N' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/expMovingAvg/#exponential-moving-average-using-n' + pipeline: + - + $setWindowFields: + partitionBy: '$stock' + sortBy: + date: 1 + output: + expMovingAvgForStock: + $expMovingAvg: + input: '$price' + N: 2 + - + name: 'Exponential Moving Average Using alpha' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/expMovingAvg/#exponential-moving-average-using-alpha' + pipeline: + - + $setWindowFields: + partitionBy: '$stock' + sortBy: + date: 1 + output: + expMovingAvgForStock: + $expMovingAvg: + input: '$price' + alpha: 0.75 diff --git a/generator/config/accumulator/first.yaml b/generator/config/accumulator/first.yaml new file mode 100644 index 000000000..d82f831a0 --- /dev/null +++ b/generator/config/accumulator/first.yaml @@ -0,0 +1,45 @@ +# $schema: ../schema.json +name: $first +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/first/' +type: + - accumulator + - window +encode: single +description: | + Returns the result of an expression for the first document in a group or window. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/first/#use-in--group-stage' + pipeline: + - + $sort: + item: 1 + date: 1 + - + $group: + _id: '$item' + firstSale: + $first: '$date' + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/first/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + firstOrderTypeForState: + $first: '$type' + window: + documents: + - 'unbounded' + - 'current' diff --git a/generator/config/accumulator/firstN.yaml b/generator/config/accumulator/firstN.yaml new file mode 100644 index 000000000..cb7a6e96c --- /dev/null +++ b/generator/config/accumulator/firstN.yaml @@ -0,0 +1,122 @@ +# $schema: ../schema.json +name: $firstN +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN/' +type: + - accumulator + - window +encode: object +description: | + Returns an aggregation of the first n elements within a group. + The elements returned are meaningful only if in a specified sort order. + If the group contains fewer than n elements, $firstN returns all elements in the group. +arguments: + - + name: input + type: + - expression + description: | + An expression that resolves to the array from which to return n elements. + - + name: 'n' + type: + - resolvesToInt + description: | + A positive integral expression that is either a constant or depends on the _id value for $group. +tests: + - + name: 'Null and Missing Values' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN/#null-and-missing-values' + pipeline: + - + $documents: + - + playerId: 'PlayerA' + gameId: 'G1' + score: 1 + - + playerId: 'PlayerB' + gameId: 'G1' + score: 2 + - + playerId: 'PlayerC' + gameId: 'G1' + score: 3 + - + playerId: 'PlayerD' + gameId: 'G1' + - + playerId: 'PlayerE' + gameId: 'G1' + score: ~ + - + $group: + _id: '$gameId' + firstFiveScores: + $firstN: + input: '$score' + n: 5 + - + name: 'Find the First Three Player Scores for a Single Game' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN/#find-the-first-three-player-scores-for-a-single-game' + pipeline: + - + $match: + gameId: 'G1' + - + $group: + _id: '$gameId' + firstThreeScores: + $firstN: + input: + - '$playerId' + - '$score' + n: 3 + - + name: 'Finding the First Three Player Scores Across Multiple Games' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN/#finding-the-first-three-player-scores-across-multiple-games' + pipeline: + - + $group: + _id: '$gameId' + playerId: + $firstN: + input: + - '$playerId' + - '$score' + n: 3 + - + name: 'Using $sort With $firstN' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN/#using--sort-with--firstn' + pipeline: + - + $sort: + score: -1 + - + $group: + _id: '$gameId' + playerId: + $firstN: + input: + - '$playerId' + - '$score' + n: 3 + - + name: 'Computing n Based on the Group Key for $group' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN/#computing-n-based-on-the-group-key-for--group' + pipeline: + - + $group: + _id: + gameId: '$gameId' + gamescores: + $firstN: + input: '$score' + n: + $cond: + if: + $eq: + - '$gameId' + - 'G2' + then: 1 + else: 3 + diff --git a/generator/config/accumulator/integral.yaml b/generator/config/accumulator/integral.yaml new file mode 100644 index 000000000..efc803597 --- /dev/null +++ b/generator/config/accumulator/integral.yaml @@ -0,0 +1,43 @@ +# $schema: ../schema.json +name: $integral +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/integral/' +type: + - window +encode: object +description: | + Returns the approximation of the area under a curve. + New in MongoDB 5.0. +arguments: + - + name: input + type: + - resolvesToNumber + - resolvesToDate + - + name: unit + type: + - timeUnit + optional: true + description: | + A string that specifies the time unit. Use one of these strings: "week", "day","hour", "minute", "second", "millisecond". + If the sortBy field is not a date, you must omit a unit. If you specify a unit, you must specify a date in the sortBy field. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/integral/#example' + pipeline: + - + $setWindowFields: + partitionBy: '$powerMeterID' + sortBy: + timeStamp: 1 + output: + powerMeterKilowattHours: + $integral: + input: '$kilowatts' + unit: 'hour' + window: + range: + - 'unbounded' + - 'current' + unit: 'hour' diff --git a/generator/config/accumulator/last.yaml b/generator/config/accumulator/last.yaml new file mode 100644 index 000000000..969c05524 --- /dev/null +++ b/generator/config/accumulator/last.yaml @@ -0,0 +1,45 @@ +# $schema: ../schema.json +name: $last +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/last/' +type: + - accumulator + - window +encode: single +description: | + Returns the result of an expression for the last document in a group or window. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/last/' + pipeline: + - + $sort: + item: 1 + date: 1 + - + $group: + _id: '$item' + lastSalesDate: + $last: '$date' + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/last/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + lastOrderTypeForState: + $last: '$type' + window: + documents: + - 'current' + - 'unbounded' diff --git a/generator/config/accumulator/lastN.yaml b/generator/config/accumulator/lastN.yaml new file mode 100644 index 000000000..13c9b72bd --- /dev/null +++ b/generator/config/accumulator/lastN.yaml @@ -0,0 +1,89 @@ +# $schema: ../schema.json +name: $lastN +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN/' +type: + - accumulator + - window +encode: object +description: | + Returns an aggregation of the last n elements within a group. + The elements returned are meaningful only if in a specified sort order. + If the group contains fewer than n elements, $lastN returns all elements in the group. +arguments: + - + name: input + type: + - resolvesToArray + description: | + An expression that resolves to the array from which to return n elements. + - + name: 'n' + type: + - resolvesToInt + description: | + An expression that resolves to a positive integer. The integer specifies the number of array elements that $firstN returns. +tests: + - + name: 'Find the Last Three Player Scores for a Single Game' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN/#find-the-last-three-player-scores-for-a-single-game' + pipeline: + - + $match: + gameId: 'G1' + - + $group: + _id: '$gameId' + lastThreeScores: + $lastN: + input: + - '$playerId' + - '$score' + n: 3 + - + name: 'Finding the Last Three Player Scores Across Multiple Games' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN/#finding-the-last-three-player-scores-across-multiple-games' + pipeline: + - + $group: + _id: '$gameId' + playerId: + $lastN: + input: + - '$playerId' + - '$score' + n: 3 + - + name: 'Using $sort With $lastN' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN/#using--sort-with--lastn' + pipeline: + - + $sort: + score: -1 + - + $group: + _id: '$gameId' + playerId: + $lastN: + input: + - '$playerId' + - '$score' + n: 3 + - + name: 'Computing n Based on the Group Key for $group' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN/#computing-n-based-on-the-group-key-for--group' + pipeline: + - + $group: + _id: + gameId: '$gameId' + gamescores: + $lastN: + input: '$score' + n: + $cond: + if: + $eq: + - '$gameId' + - 'G2' + then: 1 + else: 3 diff --git a/generator/config/accumulator/linearFill.yaml b/generator/config/accumulator/linearFill.yaml new file mode 100644 index 000000000..034e6ab9e --- /dev/null +++ b/generator/config/accumulator/linearFill.yaml @@ -0,0 +1,40 @@ +# $schema: ../schema.json +name: $linearFill +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/linearFill/' +type: + - window +encode: single +description: | + Fills null and missing fields in a window using linear interpolation based on surrounding field values. + Available in the $setWindowFields stage. + New in MongoDB 5.3. +arguments: + - + name: expression + type: + - resolvesToNumber +tests: + - + name: 'Fill Missing Values with Linear Interpolation' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/linearFill/#fill-missing-values-with-linear-interpolation' + pipeline: + - + $setWindowFields: + sortBy: + time: 1 + output: + price: + $linearFill: '$price' + - + name: 'Use Multiple Fill Methods in a Single Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/linearFill/#use-multiple-fill-methods-in-a-single-stage' + pipeline: + - + $setWindowFields: + sortBy: + time: 1 + output: + linearFillPrice: + $linearFill: '$price' + locfPrice: + $locf: '$price' diff --git a/generator/config/accumulator/locf.yaml b/generator/config/accumulator/locf.yaml new file mode 100644 index 000000000..63979bca4 --- /dev/null +++ b/generator/config/accumulator/locf.yaml @@ -0,0 +1,27 @@ +# $schema: ../schema.json +name: $locf +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/locf/' +type: + - window +encode: single +description: | + Last observation carried forward. Sets values for null and missing fields in a window to the last non-null value for the field. + Available in the $setWindowFields stage. + New in MongoDB 5.2. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Fill Missing Values with the Last Observed Value' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/locf/#fill-missing-values-with-the-last-observed-value' + pipeline: + - + $setWindowFields: + sortBy: + time: 1 + output: + price: + $locf: '$price' diff --git a/generator/config/accumulator/max.yaml b/generator/config/accumulator/max.yaml new file mode 100644 index 000000000..165cefc43 --- /dev/null +++ b/generator/config/accumulator/max.yaml @@ -0,0 +1,46 @@ +# $schema: ../schema.json +name: $max +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/max/' +type: + - accumulator + - window +encode: single +description: | + Returns the maximum value that results from applying an expression to each document. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/max/#use-in--group-stage' + pipeline: + - + $group: + _id: '$item' + maxTotalAmount: + $max: + $multiply: + - '$price' + - '$quantity' + maxQuantity: + $max: '$quantity' + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/max/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + maximumQuantityForState: + $max: '$quantity' + window: + documents: + - 'unbounded' + - 'current' diff --git a/generator/config/accumulator/maxN.yaml b/generator/config/accumulator/maxN.yaml new file mode 100644 index 000000000..4014782a8 --- /dev/null +++ b/generator/config/accumulator/maxN.yaml @@ -0,0 +1,73 @@ +# $schema: ../schema.json +name: $maxN +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/maxN/' +type: + - accumulator + - window +encode: object +description: | + Returns the n largest values in an array. Distinct from the $maxN accumulator. +arguments: + - + name: input + type: + - resolvesToArray + description: | + An expression that resolves to the array from which to return the maximal n elements. + - + name: 'n' + type: + - resolvesToInt + description: | + An expression that resolves to a positive integer. The integer specifies the number of array elements that $maxN returns. +tests: + - + name: 'Find the Maximum Three Scores for a Single Game' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/maxN/#find-the-maximum-three-scores-for-a-single-game' + pipeline: + - + $match: + gameId: 'G1' + - + $group: + _id: '$gameId' + maxThreeScores: + $maxN: + input: + - '$score' + - '$playerId' + n: 3 + - + name: 'Finding the Maximum Three Scores Across Multiple Games' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/maxN/#finding-the-maximum-three-scores-across-multiple-games' + pipeline: + - + $group: + _id: '$gameId' + maxScores: + $maxN: + input: + - '$score' + - '$playerId' + n: 3 + - + name: 'Computing n Based on the Group Key for $group' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/maxN/#computing-n-based-on-the-group-key-for--group' + pipeline: + - + $group: + _id: + gameId: '$gameId' + gamescores: + $maxN: + input: + - '$score' + - '$playerId' + n: + $cond: + if: + $eq: + - '$gameId' + - 'G2' + then: 1 + else: 3 diff --git a/generator/config/accumulator/median.yaml b/generator/config/accumulator/median.yaml new file mode 100644 index 000000000..708ea3502 --- /dev/null +++ b/generator/config/accumulator/median.yaml @@ -0,0 +1,61 @@ +# $schema: ../schema.json +name: $median +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/median/' +type: + - accumulator + - window +encode: object +description: | + Returns an approximation of the median, the 50th percentile, as a scalar value. + New in MongoDB 7.0. + This operator is available as an accumulator in these stages: + $group + $setWindowFields + It is also available as an aggregation expression. +arguments: + - + name: input + type: + - resolvesToNumber + description: | + $median calculates the 50th percentile value of this data. input must be a field name or an expression that evaluates to a numeric type. If the expression cannot be converted to a numeric type, the $median calculation ignores it. + - + name: method + type: + - string # AccumulatorPercentile + description: | + The method that mongod uses to calculate the 50th percentile value. The method must be 'approximate'. +tests: + - + name: 'Use $median as an Accumulator' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/median/#use-operatorname-as-an-accumulator' + pipeline: + - + $group: + _id: ~ + test01_median: + $median: + input: '$test01' + method: 'approximate' + - + name: 'Use $median in a $setWindowField Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/median/#use-operatorname-in-a--setwindowfield-stage' + pipeline: + - + $setWindowFields: + sortBy: + test01: 1 + output: + test01_median: + $median: + input: '$test01' + method: 'approximate' + window: + range: + - -3 + - 3 + - + $project: + _id: 0 + studentId: 1 + test01_median: 1 diff --git a/generator/config/accumulator/mergeObjects.yaml b/generator/config/accumulator/mergeObjects.yaml new file mode 100644 index 000000000..d68728001 --- /dev/null +++ b/generator/config/accumulator/mergeObjects.yaml @@ -0,0 +1,25 @@ +# $schema: ../schema.json +name: $mergeObjects +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/mergeObjects/' +type: + - accumulator +encode: single +description: | + Combines multiple documents into a single document. +arguments: + - + name: document + type: + - resolvesToObject + description: | + Any valid expression that resolves to a document. +tests: + - + name: '$mergeObjects as an Accumulator' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/mergeObjects/#-mergeobjects-as-an-accumulator' + pipeline: + - + $group: + _id: '$item' + mergedSales: + $mergeObjects: '$quantity' diff --git a/generator/config/accumulator/min.yaml b/generator/config/accumulator/min.yaml new file mode 100644 index 000000000..226d56ec8 --- /dev/null +++ b/generator/config/accumulator/min.yaml @@ -0,0 +1,41 @@ +# $schema: ../schema.json +name: $min +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/min/' +type: + - accumulator + - window +encode: single +description: | + Returns the minimum value that results from applying an expression to each document. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/min/#use-in--group-stage' + pipeline: + - + $group: + _id: '$item' + minQuantity: + $min: '$quantity' + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/min/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + minimumQuantityForState: + $min: '$quantity' + window: + documents: + - 'unbounded' + - 'current' diff --git a/generator/config/accumulator/minN.yaml b/generator/config/accumulator/minN.yaml new file mode 100644 index 000000000..24719a22a --- /dev/null +++ b/generator/config/accumulator/minN.yaml @@ -0,0 +1,73 @@ +# $schema: ../schema.json +name: $minN +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/minN/' +type: + - accumulator + - window +encode: object +description: | + Returns the n smallest values in an array. Distinct from the $minN accumulator. +arguments: + - + name: input + type: + - resolvesToArray + description: | + An expression that resolves to the array from which to return the maximal n elements. + - + name: 'n' + type: + - resolvesToInt + description: | + An expression that resolves to a positive integer. The integer specifies the number of array elements that $maxN returns. +tests: + - + name: 'Find the Minimum Three Scores for a Single Game' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/minN/#find-the-minimum-three-scores-for-a-single-game' + pipeline: + - + $match: + gameId: 'G1' + - + $group: + _id: '$gameId' + minScores: + $minN: + input: + - '$score' + - '$playerId' + n: 3 + - + name: 'Finding the Minimum Three Documents Across Multiple Games' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/minN/#finding-the-minimum-three-documents-across-multiple-games' + pipeline: + - + $group: + _id: '$gameId' + minScores: + $minN: + input: + - '$score' + - '$playerId' + n: 3 + - + name: 'Computing n Based on the Group Key for $group' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/minN/#computing-n-based-on-the-group-key-for--group' + pipeline: + - + $group: + _id: + gameId: '$gameId' + gamescores: + $minN: + input: + - '$score' + - '$playerId' + n: + $cond: + if: + $eq: + - '$gameId' + - 'G2' + then: 1 + else: 3 diff --git a/generator/config/accumulator/percentile.yaml b/generator/config/accumulator/percentile.yaml new file mode 100644 index 000000000..b2a6147d4 --- /dev/null +++ b/generator/config/accumulator/percentile.yaml @@ -0,0 +1,102 @@ +# $schema: ../schema.json +name: $percentile +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/percentile/' +type: + - accumulator + - window +encode: object +description: | + Returns an array of scalar values that correspond to specified percentile values. + New in MongoDB 7.0. + + This operator is available as an accumulator in these stages: + $group + + $setWindowFields + + It is also available as an aggregation expression. +arguments: + - + name: input + type: + - resolvesToNumber + description: | + $percentile calculates the percentile values of this data. input must be a field name or an expression that evaluates to a numeric type. If the expression cannot be converted to a numeric type, the $percentile calculation ignores it. + - + name: p + type: + - resolvesToArray # of resolvesToNumber + description: | + $percentile calculates a percentile value for each element in p. The elements represent percentages and must evaluate to numeric values in the range 0.0 to 1.0, inclusive. + $percentile returns results in the same order as the elements in p. + - + name: method + type: + - string # AccumulatorPercentile + description: | + The method that mongod uses to calculate the percentile value. The method must be 'approximate'. +tests: + - + name: 'Calculate a Single Value as an Accumulator' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/percentile/#calculate-a-single-value-as-an-accumulator' + pipeline: + - + $group: + _id: ~ + test01_percentiles: + $percentile: + input: '$test01' + p: + - 0.95 + method: 'approximate' + - + name: 'Calculate Multiple Values as an Accumulator' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/percentile/#calculate-multiple-values-as-an-accumulator' + pipeline: + - + $group: + _id: ~ + test01_percentiles: + $percentile: + input: '$test01' + p: [0.5, 0.75, 0.9, 0.95] + method: 'approximate' + test02_percentiles: + $percentile: + input: '$test02' + p: [0.5, 0.75, 0.9, 0.95] + method: 'approximate' + test03_percentiles: + $percentile: + input: '$test03' + p: [0.5, 0.75, 0.9, 0.95] + method: 'approximate' + test03_percent_alt: + $percentile: + input: '$test03' + p: [0.9, 0.5, 0.75, 0.95] + method: 'approximate' + - + name: 'Use $percentile in a $setWindowField Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/percentile/#use-operatorname-in-a--setwindowfield-stage' + pipeline: + - + $setWindowFields: + sortBy: + test01: 1 + output: + test01_95percentile: + $percentile: + input: '$test01' + p: + - 0.95 + method: 'approximate' + window: + range: + - -3 + - 3 + - + $project: + _id: 0 + studentId: 1 + test01_95percentile: 1 diff --git a/generator/config/accumulator/push.yaml b/generator/config/accumulator/push.yaml new file mode 100644 index 000000000..3fc367c59 --- /dev/null +++ b/generator/config/accumulator/push.yaml @@ -0,0 +1,55 @@ +# $schema: ../schema.json +name: $push +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/push/' +type: + - accumulator + - window +encode: single +description: | + Returns an array of values that result from applying an expression to each document. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/push/#use-in--group-stage' + pipeline: + - + $sort: + date: 1 + item: 1 + - + $group: + _id: + day: + # Example uses the short form, the builder always generates the verbose form + # $dayOfYear: '$date' + $dayOfYear: + date: '$date' + year: + # Example uses the short form, the builder always generates the verbose form + # $year: '$date' + $year: + date: '$date' + itemsSold: + $push: + item: '$item' + quantity: '$quantity' + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/push/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + quantitiesForState: + $push: '$quantity' + window: + documents: ['unbounded', 'current'] diff --git a/generator/config/accumulator/rank.yaml b/generator/config/accumulator/rank.yaml new file mode 100644 index 000000000..8b8fd041b --- /dev/null +++ b/generator/config/accumulator/rank.yaml @@ -0,0 +1,34 @@ +# $schema: ../schema.json +name: $rank +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/rank/' +type: + - window +encode: object +description: | + Returns the document position (known as the rank) relative to other documents in the $setWindowFields stage partition. + New in MongoDB 5.0. +tests: + - + name: 'Rank Partitions by an Integer Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/rank/#rank-partitions-by-an-integer-field' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + quantity: -1 + output: + rankQuantityForState: + $rank: {} + - + name: 'Rank Partitions by a Date Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/rank/#rank-partitions-by-a-date-field' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + rankOrderDateForState: + $rank: {} diff --git a/generator/config/accumulator/shift.yaml b/generator/config/accumulator/shift.yaml new file mode 100644 index 000000000..f4984f056 --- /dev/null +++ b/generator/config/accumulator/shift.yaml @@ -0,0 +1,65 @@ +# $schema: ../schema.json +name: $shift +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/shift/' +type: + - window +encode: object +description: | + Returns the value from an expression applied to a document in a specified position relative to the current document in the $setWindowFields stage partition. + New in MongoDB 5.0. +arguments: + - + name: output + type: + - expression + description: | + Specifies an expression to evaluate and return in the output. + - + name: by + type: + - int + description: | + Specifies an integer with a numeric document position relative to the current document in the output. + For example: + 1 specifies the document position after the current document. + -1 specifies the document position before the current document. + -2 specifies the document position that is two positions before the current document. + - + name: default + type: + - expression + description: | + Specifies an optional default expression to evaluate if the document position is outside of the implicit $setWindowFields stage window. The implicit window contains all the documents in the partition. + The default expression must evaluate to a constant value. + If you do not specify a default expression, $shift returns null for documents whose positions are outside of the implicit $setWindowFields stage window. +tests: + - + name: 'Shift Using a Positive Integer' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/shift/#shift-using-a-positive-integer' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + quantity: -1 + output: + shiftQuantityForState: + $shift: + output: '$quantity' + by: 1 + default: 'Not available' + - + name: 'Shift Using a Negative Integer' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/shift/#shift-using-a-negative-integer' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + quantity: -1 + output: + shiftQuantityForState: + $shift: + output: '$quantity' + by: -1 + default: 'Not available' diff --git a/generator/config/accumulator/stdDevPop.yaml b/generator/config/accumulator/stdDevPop.yaml new file mode 100644 index 000000000..8916456d4 --- /dev/null +++ b/generator/config/accumulator/stdDevPop.yaml @@ -0,0 +1,40 @@ +# $schema: ../schema.json +name: $stdDevPop +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/stdDevPop/' +type: + - accumulator + - window +encode: single +description: | + Calculates the population standard deviation of the input values. Use if the values encompass the entire population of data you want to represent and do not wish to generalize about a larger population. $stdDevPop ignores non-numeric values. + If the values represent only a sample of a population of data from which to generalize about the population, use $stdDevSamp instead. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - resolvesToNumber +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/stdDevPop/#use-in--group-stage' + pipeline: + - + $group: + _id: '$quiz' + stdDev: + $stdDevPop: '$score' + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/stdDevPop/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + stdDevPopQuantityForState: + $stdDevPop: '$quantity' + window: + documents: ['unbounded', 'current'] diff --git a/generator/config/accumulator/stdDevSamp.yaml b/generator/config/accumulator/stdDevSamp.yaml new file mode 100644 index 000000000..94ac33d15 --- /dev/null +++ b/generator/config/accumulator/stdDevSamp.yaml @@ -0,0 +1,43 @@ +# $schema: ../schema.json +name: $stdDevSamp +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/stdDevSamp/' +type: + - accumulator + - window +encode: single +description: | + Calculates the sample standard deviation of the input values. Use if the values encompass a sample of a population of data from which to generalize about the population. $stdDevSamp ignores non-numeric values. + If the values represent the entire population of data or you do not wish to generalize about a larger population, use $stdDevPop instead. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - resolvesToNumber +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/stdDevSamp/#use-in--group-stage' + pipeline: + - + $sample: + size: 100 + - + $group: + _id: ~ + ageStdDev: + $stdDevSamp: '$age' + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/stdDevSamp/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + stdDevSampQuantityForState: + $stdDevSamp: '$quantity' + window: + documents: ['unbounded', 'current'] diff --git a/generator/config/accumulator/sum.yaml b/generator/config/accumulator/sum.yaml new file mode 100644 index 000000000..c40417ef4 --- /dev/null +++ b/generator/config/accumulator/sum.yaml @@ -0,0 +1,56 @@ +# $schema: ../schema.json +name: $sum +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sum/' +type: + - accumulator + - window +encode: single +description: | + Returns a sum of numerical values. Ignores non-numeric values. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - resolvesToNumber +tests: + - + name: 'Use in $group Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sum/#use-in--group-stage' + pipeline: + - + $group: + _id: + day: + # Example uses the short form, the builder always generates the verbose form + # $dayOfYear: '$date' + $dayOfYear: + date: '$date' + year: + # Example uses the short form, the builder always generates the verbose form + # $year: '$date' + $year: + date: '$date' + totalAmount: + $sum: + $multiply: + - '$price' + - '$quantity' + count: + $sum: 1 + - + name: 'Use in $setWindowFields Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sum/#use-in--setwindowfields-stage' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + sumQuantityForState: + $sum: '$quantity' + window: + documents: + - 'unbounded' + - 'current' diff --git a/generator/config/accumulator/top.yaml b/generator/config/accumulator/top.yaml new file mode 100644 index 000000000..2f4deefa2 --- /dev/null +++ b/generator/config/accumulator/top.yaml @@ -0,0 +1,56 @@ +# $schema: ../schema.json +name: $top +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/top/' +type: + - accumulator +encode: object +description: | + Returns the top element within a group according to the specified sort order. + New in MongoDB 5.2. + + Available in the $group and $setWindowFields stages. +arguments: + - + name: sortBy + type: + - object # SortSpec + description: | + Specifies the order of results, with syntax similar to $sort. + - + name: output + type: + - expression + description: | + Represents the output for each element in the group and can be any expression. +tests: + - + name: 'Find the Top Score' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/top/#find-the-top-score' + pipeline: + - + $match: + gameId: 'G1' + - + $group: + _id: '$gameId' + playerId: + $top: + output: + - '$playerId' + - '$score' + sortBy: + score: -1 + - + name: 'Find the Top Score Across Multiple Games' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/top/#find-the-top-score-across-multiple-games' + pipeline: + - + $group: + _id: '$gameId' + playerId: + $top: + output: + - '$playerId' + - '$score' + sortBy: + score: -1 diff --git a/generator/config/accumulator/topN.yaml b/generator/config/accumulator/topN.yaml new file mode 100644 index 000000000..c8073e09f --- /dev/null +++ b/generator/config/accumulator/topN.yaml @@ -0,0 +1,85 @@ +# $schema: ../schema.json +name: $topN +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/topN/' +type: + - accumulator +encode: object +description: | + Returns an aggregation of the top n fields within a group, according to the specified sort order. + New in MongoDB 5.2. + + Available in the $group and $setWindowFields stages. +arguments: + - + name: 'n' + type: + - resolvesToInt + description: | + limits the number of results per group and has to be a positive integral expression that is either a constant or depends on the _id value for $group. + - + name: sortBy + type: + - object # SortSpec + description: | + Specifies the order of results, with syntax similar to $sort. + - + name: output + type: + - expression + description: | + Represents the output for each element in the group and can be any expression. +tests: + - + name: 'Find the Three Highest Scores' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/topN/#find-the-three-highest-scores' + pipeline: + - + $match: + gameId: 'G1' + - + $group: + _id: '$gameId' + playerId: + $topN: + output: + - '$playerId' + - '$score' + sortBy: + score: -1 + n: 3 + - + name: 'Finding the Three Highest Score Documents Across Multiple Games' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/topN/#finding-the-three-highest-score-documents-across-multiple-games' + pipeline: + - + $group: + _id: '$gameId' + playerId: + $topN: + output: + - '$playerId' + - '$score' + sortBy: + score: -1 + n: 3 + - + name: 'Computing n Based on the Group Key for $group' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/topN/#computing-n-based-on-the-group-key-for--group' + pipeline: + - + $group: + _id: + gameId: '$gameId' + gamescores: + $topN: + output: '$score' + n: + $cond: + if: + $eq: + - '$gameId' + - 'G2' + then: 1 + else: 3 + sortBy: + score: -1 diff --git a/generator/config/definitions.php b/generator/config/definitions.php new file mode 100644 index 000000000..a4f952d39 --- /dev/null +++ b/generator/config/definitions.php @@ -0,0 +1,61 @@ + __DIR__ . '/stage', + 'namespace' => 'MongoDB\\Builder\\Stage', + 'classNameSuffix' => 'Stage', + 'generators' => [ + OperatorClassGenerator::class, + OperatorFactoryGenerator::class, + OperatorTestGenerator::class, + FluentStageFactoryGenerator::class, + ], + ], + + // Aggregation Pipeline Accumulator and Window Operators + [ + 'configFiles' => __DIR__ . '/accumulator', + 'namespace' => 'MongoDB\\Builder\\Accumulator', + 'classNameSuffix' => 'Accumulator', + 'generators' => [ + OperatorClassGenerator::class, + OperatorFactoryGenerator::class, + OperatorTestGenerator::class, + ], + ], + + // Aggregation Pipeline Expression + [ + 'configFiles' => __DIR__ . '/expression', + 'namespace' => 'MongoDB\\Builder\\Expression', + 'classNameSuffix' => 'Operator', + 'generators' => [ + OperatorClassGenerator::class, + OperatorFactoryGenerator::class, + OperatorTestGenerator::class, + ], + ], + + // Query Operators + [ + 'configFiles' => __DIR__ . '/query', + 'namespace' => 'MongoDB\\Builder\\Query', + 'classNameSuffix' => 'Operator', + 'generators' => [ + OperatorClassGenerator::class, + OperatorFactoryGenerator::class, + OperatorTestGenerator::class, + ], + ], +]; diff --git a/generator/config/expression/abs.yaml b/generator/config/expression/abs.yaml new file mode 100644 index 000000000..fe29e44e3 --- /dev/null +++ b/generator/config/expression/abs.yaml @@ -0,0 +1,25 @@ +# $schema: ../schema.json +name: $abs +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/abs/' +type: + - resolvesToNumber +encode: single +description: | + Returns the absolute value of a number. +arguments: + - + name: value + type: + - resolvesToNumber +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/abs/#example' + pipeline: + - + $project: + delta: + $abs: + $subtract: + - '$startTemp' + - '$endTemp' diff --git a/generator/config/expression/acos.yaml b/generator/config/expression/acos.yaml new file mode 100644 index 000000000..7deca736d --- /dev/null +++ b/generator/config/expression/acos.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $acos +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/acos/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the inverse cosine (arc cosine) of a value in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $acos takes any valid expression that resolves to a number between -1 and 1, e.g. -1 <= value <= 1. + $acos returns values in radians. Use $radiansToDegrees operator to convert the output value from radians to degrees. + By default $acos returns values as a double. $acos can also return values as a 128-bit decimal as long as the expression resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/acos/#example' + pipeline: + - + $addFields: + angle_a: + $radiansToDegrees: + $acos: + $divide: + - '$side_b' + - '$hypotenuse' diff --git a/generator/config/expression/acosh.yaml b/generator/config/expression/acosh.yaml new file mode 100644 index 000000000..ce575e317 --- /dev/null +++ b/generator/config/expression/acosh.yaml @@ -0,0 +1,28 @@ +# $schema: ../schema.json +name: $acosh +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/acosh/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the inverse hyperbolic cosine (hyperbolic arc cosine) of a value in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $acosh takes any valid expression that resolves to a number between 1 and +Infinity, e.g. 1 <= value <= +Infinity. + $acosh returns values in radians. Use $radiansToDegrees operator to convert the output value from radians to degrees. + By default $acosh returns values as a double. $acosh can also return values as a 128-bit decimal as long as the expression resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/acosh/#example' + pipeline: + - + $addFields: + y-coordinate: + $radiansToDegrees: + $acosh: '$x-coordinate' diff --git a/generator/config/expression/add.yaml b/generator/config/expression/add.yaml new file mode 100644 index 000000000..fa23253d0 --- /dev/null +++ b/generator/config/expression/add.yaml @@ -0,0 +1,44 @@ +# $schema: ../schema.json +name: $add +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/add/' +type: + - resolvesToInt + - resolvesToLong + - resolvesToDouble + - resolvesToDecimal + - resolvesToDate +encode: single +description: | + Adds numbers to return the sum, or adds numbers and a date to return a new date. If adding numbers and a date, treats the numbers as milliseconds. Accepts any number of argument expressions, but at most, one expression can resolve to a date. +arguments: + - + name: expression + type: + - resolvesToNumber + - resolvesToDate + variadic: array + description: | + The arguments can be any valid expression as long as they resolve to either all numbers or to numbers and a date. +tests: + - + name: 'Add Numbers' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/add/#add-numbers' + pipeline: + - + $project: + item: 1 + total: + $add: + - '$price' + - '$fee' + - + name: 'Perform Addition on a Date' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/add/#perform-addition-on-a-date' + pipeline: + - + $project: + item: 1 + billing_date: + $add: + - '$date' + - 259200000 diff --git a/generator/config/expression/allElementsTrue.yaml b/generator/config/expression/allElementsTrue.yaml new file mode 100644 index 000000000..7301f8d68 --- /dev/null +++ b/generator/config/expression/allElementsTrue.yaml @@ -0,0 +1,25 @@ +# $schema: ../schema.json +name: $allElementsTrue +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/allElementsTrue/' +type: + - resolvesToBool +encode: array +description: | + Returns true if no element of a set evaluates to false, otherwise, returns false. Accepts a single argument expression. +arguments: + - + name: expression + type: + - resolvesToArray +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/allElementsTrue/#example' + pipeline: + - + $project: + responses: 1 + isAllTrue: + $allElementsTrue: + - '$responses' + _id: 0 diff --git a/generator/config/expression/and.yaml b/generator/config/expression/and.yaml new file mode 100644 index 000000000..96057d249 --- /dev/null +++ b/generator/config/expression/and.yaml @@ -0,0 +1,37 @@ +# $schema: ../schema.json +name: $and +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/and/' +type: + - resolvesToBool +encode: single +description: | + Returns true only when all its expressions evaluate to true. Accepts any number of argument expressions. +arguments: + - + name: expression + type: + - expression + - resolvesToBool + - resolvesToNumber + - resolvesToString + - resolvesToNull + variadic: array +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/and/#example' + pipeline: + - + $project: + item: 1 + qty: 1 + result: + $and: + - + $gt: + - '$qty' + - 100 + - + $lt: + - '$qty' + - 250 diff --git a/generator/config/expression/anyElementTrue.yaml b/generator/config/expression/anyElementTrue.yaml new file mode 100644 index 000000000..50fe665b6 --- /dev/null +++ b/generator/config/expression/anyElementTrue.yaml @@ -0,0 +1,25 @@ +# $schema: ../schema.json +name: $anyElementTrue +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/anyElementTrue/' +type: + - resolvesToBool +encode: array +description: | + Returns true if any elements of a set evaluate to true; otherwise, returns false. Accepts a single argument expression. +arguments: + - + name: expression + type: + - resolvesToArray +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/anyElementTrue/#example' + pipeline: + - + $project: + responses: 1 + isAnyTrue: + $anyElementTrue: + - '$responses' + _id: 0 diff --git a/generator/config/expression/arrayElemAt.yaml b/generator/config/expression/arrayElemAt.yaml new file mode 100644 index 000000000..09fe9dae1 --- /dev/null +++ b/generator/config/expression/arrayElemAt.yaml @@ -0,0 +1,33 @@ +# $schema: ../schema.json +name: $arrayElemAt +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/arrayElemAt/' +type: + - resolvesToAny +encode: array +description: | + Returns the element at the specified array index. +arguments: + - + name: array + type: + - resolvesToArray + - + name: idx + type: + - resolvesToInt +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/arrayElemAt/#example' + pipeline: + - + $project: + name: 1 + first: + $arrayElemAt: + - '$favorites' + - 0 + last: + $arrayElemAt: + - '$favorites' + - -1 diff --git a/generator/config/expression/arrayToObject.yaml b/generator/config/expression/arrayToObject.yaml new file mode 100644 index 000000000..87026f7a7 --- /dev/null +++ b/generator/config/expression/arrayToObject.yaml @@ -0,0 +1,50 @@ +# $schema: ../schema.json +name: $arrayToObject +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/arrayToObject/' +type: + - resolvesToObject +encode: array +description: | + Converts an array of key value pairs to a document. +arguments: + - + name: array + type: + - resolvesToArray +tests: + - + name: '$arrayToObject Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/arrayToObject/#-arraytoobject--example' + pipeline: + - + $project: + item: 1 + dimensions: + # Example uses the short form, the builder always generates the verbose form + # $arrayToObject: '$dimensions' + $arrayToObject: + - '$dimensions' + - + name: '$objectToArray and $arrayToObject Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/arrayToObject/#-objecttoarray----arraytoobject-example' + pipeline: + - + $addFields: + instock: + $objectToArray: '$instock' + - + $addFields: + instock: + $concatArrays: + - '$instock' + - + - + k: 'total' + v: + $sum: + - '$instock.v' + - + $addFields: + instock: + $arrayToObject: + - '$instock' diff --git a/generator/config/expression/asin.yaml b/generator/config/expression/asin.yaml new file mode 100644 index 000000000..43e2832a2 --- /dev/null +++ b/generator/config/expression/asin.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $asin +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/asin/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the inverse sin (arc sine) of a value in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $asin takes any valid expression that resolves to a number between -1 and 1, e.g. -1 <= value <= 1. + $asin returns values in radians. Use $radiansToDegrees operator to convert the output value from radians to degrees. + By default $asin returns values as a double. $asin can also return values as a 128-bit decimal as long as the expression resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/asin/#example' + pipeline: + - + $addFields: + angle_a: + $radiansToDegrees: + $asin: + $divide: + - '$side_a' + - '$hypotenuse' diff --git a/generator/config/expression/asinh.yaml b/generator/config/expression/asinh.yaml new file mode 100644 index 000000000..6d45c14fa --- /dev/null +++ b/generator/config/expression/asinh.yaml @@ -0,0 +1,28 @@ +# $schema: ../schema.json +name: $asinh +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/asinh/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the inverse hyperbolic sine (hyperbolic arc sine) of a value in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $asinh takes any valid expression that resolves to a number. + $asinh returns values in radians. Use $radiansToDegrees operator to convert the output value from radians to degrees. + By default $asinh returns values as a double. $asinh can also return values as a 128-bit decimal as long as the expression resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/asinh/#example' + pipeline: + - + $addFields: + y-coordinate: + $radiansToDegrees: + $asinh: '$x-coordinate' diff --git a/generator/config/expression/atan.yaml b/generator/config/expression/atan.yaml new file mode 100644 index 000000000..a8bb1674f --- /dev/null +++ b/generator/config/expression/atan.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $atan +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/atan/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the inverse tangent (arc tangent) of a value in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $atan takes any valid expression that resolves to a number. + $atan returns values in radians. Use $radiansToDegrees operator to convert the output value from radians to degrees. + By default $atan returns values as a double. $atan can also return values as a 128-bit decimal as long as the expression resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/atan/#example' + pipeline: + - + $addFields: + angle_a: + $radiansToDegrees: + $atan: + $divide: + - '$side_b' + - '$side_a' diff --git a/generator/config/expression/atan2.yaml b/generator/config/expression/atan2.yaml new file mode 100644 index 000000000..1abc55e6a --- /dev/null +++ b/generator/config/expression/atan2.yaml @@ -0,0 +1,34 @@ +# $schema: ../schema.json +name: $atan2 +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/atan2/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: array +description: | + Returns the inverse tangent (arc tangent) of y / x in radians, where y and x are the first and second values passed to the expression respectively. +arguments: + - + name: 'y' + type: + - resolvesToNumber + description: | + $atan2 takes any valid expression that resolves to a number. + $atan2 returns values in radians. Use $radiansToDegrees operator to convert the output value from radians to degrees. + By default $atan returns values as a double. $atan2 can also return values as a 128-bit decimal as long as the expression resolves to a 128-bit decimal value. + - + name: x + type: + - resolvesToNumber +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/atan2/#example' + pipeline: + - + $addFields: + angle_a: + $radiansToDegrees: + $atan2: + - '$side_b' + - '$side_a' diff --git a/generator/config/expression/atanh.yaml b/generator/config/expression/atanh.yaml new file mode 100644 index 000000000..501fba2bf --- /dev/null +++ b/generator/config/expression/atanh.yaml @@ -0,0 +1,28 @@ +# $schema: ../schema.json +name: $atanh +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/atanh/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the inverse hyperbolic tangent (hyperbolic arc tangent) of a value in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $atanh takes any valid expression that resolves to a number between -1 and 1, e.g. -1 <= value <= 1. + $atanh returns values in radians. Use $radiansToDegrees operator to convert the output value from radians to degrees. + By default $atanh returns values as a double. $atanh can also return values as a 128-bit decimal as long as the expression resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/atanh/#example' + pipeline: + - + $addFields: + y-coordinate: + $radiansToDegrees: + $atanh: '$x-coordinate' diff --git a/generator/config/expression/avg.yaml b/generator/config/expression/avg.yaml new file mode 100644 index 000000000..3bb771936 --- /dev/null +++ b/generator/config/expression/avg.yaml @@ -0,0 +1,35 @@ +# $schema: ../schema.json +name: $avg +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg/' +type: + - resolvesToNumber +encode: single +description: | + Returns an average of numerical values. Ignores non-numeric values. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - resolvesToNumber + variadic: array +tests: + - + name: 'Use in $project Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/avg/#use-in--project-stage' + pipeline: + - + $project: + quizAvg: + # Example uses the short form, the builder always generates the verbose form + # $avg: '$quizzes' + $avg: + - '$quizzes' + labAvg: + # $avg: '$labs' + $avg: + - '$labs' + examAvg: + $avg: + - '$final' + - '$midterm' diff --git a/generator/config/expression/binarySize.yaml b/generator/config/expression/binarySize.yaml new file mode 100644 index 000000000..eb0146f8c --- /dev/null +++ b/generator/config/expression/binarySize.yaml @@ -0,0 +1,25 @@ +# $schema: ../schema.json +name: $binarySize +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/binarySize/' +type: + - resolvesToInt +encode: single +description: | + Returns the size of a given string or binary data value's content in bytes. +arguments: + - + name: expression + type: + - resolvesToString + - resolvesToBinData + - resolvesToNull +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/binarySize/#example' + pipeline: + - + $project: + name: '$name' + imageSize: + $binarySize: '$binary' diff --git a/generator/config/expression/bitAnd.yaml b/generator/config/expression/bitAnd.yaml new file mode 100644 index 000000000..271cc0973 --- /dev/null +++ b/generator/config/expression/bitAnd.yaml @@ -0,0 +1,38 @@ +# $schema: ../schema.json +name: $bitAnd +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bitAnd/' +type: + - resolvesToInt + - resolvesToLong +encode: single +description: | + Returns the result of a bitwise and operation on an array of int or long values. + New in MongoDB 6.3. +arguments: + - + name: expression + type: + - resolvesToInt + - resolvesToLong + variadic: array +tests: + - + name: 'Bitwise AND with Two Integers' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bitAnd/#bitwise-and-with-two-integers' + pipeline: + - + $project: + result: + $bitAnd: + - '$a' + - '$b' + - + name: 'Bitwise AND with a Long and Integer' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bitAnd/#bitwise-and-with-a-long-and-integer' + pipeline: + - + $project: + result: + $bitAnd: + - '$a' + - { "$numberLong": "63" } diff --git a/generator/config/expression/bitNot.yaml b/generator/config/expression/bitNot.yaml new file mode 100644 index 000000000..5211fa42a --- /dev/null +++ b/generator/config/expression/bitNot.yaml @@ -0,0 +1,25 @@ +# $schema: ../schema.json +name: $bitNot +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bitNot/' +type: + - resolvesToInt + - resolvesToLong +encode: single +description: | + Returns the result of a bitwise not operation on a single argument or an array that contains a single int or long value. + New in MongoDB 6.3. +arguments: + - + name: expression + type: + - resolvesToInt + - resolvesToLong +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bitNot/#example' + pipeline: + - + $project: + result: + $bitNot: '$a' diff --git a/generator/config/expression/bitOr.yaml b/generator/config/expression/bitOr.yaml new file mode 100644 index 000000000..084ac224c --- /dev/null +++ b/generator/config/expression/bitOr.yaml @@ -0,0 +1,38 @@ +# $schema: ../schema.json +name: $bitOr +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bitOr/' +type: + - resolvesToInt + - resolvesToLong +encode: single +description: | + Returns the result of a bitwise or operation on an array of int or long values. + New in MongoDB 6.3. +arguments: + - + name: expression + type: + - resolvesToInt + - resolvesToLong + variadic: array +tests: + - + name: 'Bitwise OR with Two Integers' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bitOr/#bitwise-or-with-two-integers' + pipeline: + - + $project: + result: + $bitOr: + - '$a' + - '$b' + - + name: 'Bitwise OR with a Long and Integer' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bitOr/#bitwise-or-with-a-long-and-integer' + pipeline: + - + $project: + result: + $bitOr: + - '$a' + - { "$numberLong": "63" } diff --git a/generator/config/expression/bitXor.yaml b/generator/config/expression/bitXor.yaml new file mode 100644 index 000000000..f4acc4df4 --- /dev/null +++ b/generator/config/expression/bitXor.yaml @@ -0,0 +1,28 @@ +# $schema: ../schema.json +name: $bitXor +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bitXor/' +type: + - resolvesToInt + - resolvesToLong +encode: single +description: | + Returns the result of a bitwise xor (exclusive or) operation on an array of int and long values. + New in MongoDB 6.3. +arguments: + - + name: expression + type: + - resolvesToInt + - resolvesToLong + variadic: array +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bitXor/#example' + pipeline: + - + $project: + result: + $bitXor: + - '$a' + - '$b' diff --git a/generator/config/expression/bsonSize.yaml b/generator/config/expression/bsonSize.yaml new file mode 100644 index 000000000..712188c52 --- /dev/null +++ b/generator/config/expression/bsonSize.yaml @@ -0,0 +1,48 @@ +# $schema: ../schema.json +name: $bsonSize +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bsonSize/' +type: + - resolvesToInt +encode: single +description: | + Returns the size in bytes of a given document (i.e. BSON type Object) when encoded as BSON. +arguments: + - + name: object + type: + - resolvesToObject + - resolvesToNull +tests: + - + name: 'Return Sizes of Documents' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bsonSize/#return-sizes-of-documents' + pipeline: + - + $project: + name: 1 + object_size: + $bsonSize: '$$ROOT' + - + name: 'Return Combined Size of All Documents in a Collection' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bsonSize/#return-combined-size-of-all-documents-in-a-collection' + pipeline: + - + $group: + _id: ~ + combined_object_size: + $sum: + $bsonSize: '$$ROOT' + - + name: 'Return Document with Largest Specified Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bsonSize/#return-document-with-largest-specified-field' + pipeline: + - + $project: + name: '$name' + task_object_size: + $bsonSize: '$current_task' + - + $sort: + task_object_size: -1 + - + $limit: 1 diff --git a/generator/config/expression/case.yaml b/generator/config/expression/case.yaml new file mode 100644 index 000000000..b86aef878 --- /dev/null +++ b/generator/config/expression/case.yaml @@ -0,0 +1,21 @@ +# $schema: ../schema.json +name: $case +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/switch/' +type: + - switchBranch +encode: flat_object +description: | + Represents a single case in a $switch expression +arguments: + - + name: case + type: + - resolvesToBool + description: | + Can be any valid expression that resolves to a boolean. If the result is not a boolean, it is coerced to a boolean value. More information about how MongoDB evaluates expressions as either true or false can be found here. + - + name: then + type: + - expression + description: | + Can be any valid expression. diff --git a/generator/config/expression/ceil.yaml b/generator/config/expression/ceil.yaml new file mode 100644 index 000000000..73c31ddb7 --- /dev/null +++ b/generator/config/expression/ceil.yaml @@ -0,0 +1,25 @@ +# $schema: ../schema.json +name: $ceil +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/ceil/' +type: + - resolvesToInt +encode: single +description: | + Returns the smallest integer greater than or equal to the specified number. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + If the argument resolves to a value of null or refers to a field that is missing, $ceil returns null. If the argument resolves to NaN, $ceil returns NaN. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/ceil/#example' + pipeline: + - + $project: + value: 1 + ceilingValue: + $ceil: '$value' diff --git a/generator/config/expression/cmp.yaml b/generator/config/expression/cmp.yaml new file mode 100644 index 000000000..dd24f9839 --- /dev/null +++ b/generator/config/expression/cmp.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $cmp +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/cmp/' +type: + - resolvesToInt +encode: array +description: | + Returns 0 if the two values are equivalent, 1 if the first value is greater than the second, and -1 if the first value is less than the second. +arguments: + - + name: expression1 + type: + - expression + - + name: expression2 + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/cmp/#example' + pipeline: + - + $project: + item: 1 + qty: 1 + cmpTo250: + $cmp: + - '$qty' + - 250 + _id: 0 diff --git a/generator/config/expression/concat.yaml b/generator/config/expression/concat.yaml new file mode 100644 index 000000000..e8b218d82 --- /dev/null +++ b/generator/config/expression/concat.yaml @@ -0,0 +1,26 @@ +# $schema: ../schema.json +name: $concat +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/concat/' +type: + - resolvesToString +encode: single +description: | + Concatenates any number of strings. +arguments: + - + name: expression + type: + - resolvesToString + variadic: array +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/concat/#examples' + pipeline: + - + $project: + itemDescription: + $concat: + - '$item' + - ' - ' + - '$description' diff --git a/generator/config/expression/concatArrays.yaml b/generator/config/expression/concatArrays.yaml new file mode 100644 index 000000000..026541092 --- /dev/null +++ b/generator/config/expression/concatArrays.yaml @@ -0,0 +1,25 @@ +# $schema: ../schema.json +name: $concatArrays +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/concatArrays/' +type: + - resolvesToArray +encode: single +description: | + Concatenates arrays to return the concatenated array. +arguments: + - + name: array + type: + - resolvesToArray + variadic: array +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/concatArrays/#example' + pipeline: + - + $project: + items: + $concatArrays: + - '$instock' + - '$ordered' diff --git a/generator/config/expression/cond.yaml b/generator/config/expression/cond.yaml new file mode 100644 index 000000000..e2fd66ad7 --- /dev/null +++ b/generator/config/expression/cond.yaml @@ -0,0 +1,37 @@ +# $schema: ../schema.json +name: $cond +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/' +type: + - resolvesToAny +encode: object +description: | + A ternary operator that evaluates one expression, and depending on the result, returns the value of one of the other two expressions. Accepts either three expressions in an ordered list or three named parameters. +arguments: + - + name: if + type: + - resolvesToBool + - + name: then + type: + - expression + - + name: else + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/cond/#example' + pipeline: + - + $project: + item: 1 + discount: + $cond: + if: + $gte: + - '$qty' + - 250 + then: 30 + else: 20 diff --git a/generator/config/expression/convert.yaml b/generator/config/expression/convert.yaml new file mode 100644 index 000000000..a76311ed5 --- /dev/null +++ b/generator/config/expression/convert.yaml @@ -0,0 +1,82 @@ +# $schema: ../schema.json +name: $convert +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/convert/' +type: + - resolvesToAny +encode: object +description: | + Converts a value to a specified type. + New in MongoDB 4.0. +arguments: + - + name: input + type: + - expression + - + name: to + type: + - resolvesToString + - resolvesToInt + - + name: onError + type: + - expression + optional: true + description: | + The value to return on encountering an error during conversion, including unsupported type conversions. The arguments can be any valid expression. + If unspecified, the operation throws an error upon encountering an error and stops. + - + name: onNull + type: + - expression + optional: true + description: | + The value to return if the input is null or missing. The arguments can be any valid expression. + If unspecified, $convert returns null if the input is null or missing. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/convert/#example' + pipeline: + - + $addFields: + convertedPrice: + $convert: + input: '$price' + to: 'decimal' + onError: 'Error' + onNull: !bson_decimal128 '0' + convertedQty: + $convert: + input: '$qty' + to: 'int' + onError: + $concat: + - 'Could not convert ' + - + $toString: '$qty' + - ' to type integer.' + onNull: 0 + - + $project: + totalPrice: + $switch: + branches: + - + case: + $eq: + - + $type: '$convertedPrice' + - 'string' + then: 'NaN' + - + case: + $eq: + - + $type: '$convertedQty' + - 'string' + then: 'NaN' + default: + $multiply: + - '$convertedPrice' + - '$convertedQty' diff --git a/generator/config/expression/cos.yaml b/generator/config/expression/cos.yaml new file mode 100644 index 000000000..0b47670cf --- /dev/null +++ b/generator/config/expression/cos.yaml @@ -0,0 +1,30 @@ +# $schema: ../schema.json +name: $cos +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/cos/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the cosine of a value that is measured in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $cos takes any valid expression that resolves to a number. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the result to radians. + By default $cos returns values as a double. $cos can also return values as a 128-bit decimal as long as the resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/cos/#example' + pipeline: + - + $addFields: + side_a: + $multiply: + - + $cos: + $degreesToRadians: '$angle_a' + - '$hypotenuse' diff --git a/generator/config/expression/cosh.yaml b/generator/config/expression/cosh.yaml new file mode 100644 index 000000000..419fa8caa --- /dev/null +++ b/generator/config/expression/cosh.yaml @@ -0,0 +1,27 @@ +# $schema: ../schema.json +name: $cosh +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/cosh/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the hyperbolic cosine of a value that is measured in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $cosh takes any valid expression that resolves to a number, measured in radians. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the value to radians. + By default $cosh returns values as a double. $cosh can also return values as a 128-bit decimal if the resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/cosh/#example' + pipeline: + - + $addFields: + cosh_output: + $cosh: + $degreesToRadians: '$angle' diff --git a/generator/config/expression/dateAdd.yaml b/generator/config/expression/dateAdd.yaml new file mode 100644 index 000000000..c7d85d571 --- /dev/null +++ b/generator/config/expression/dateAdd.yaml @@ -0,0 +1,133 @@ +# $schema: ../schema.json +name: $dateAdd +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateAdd/' +type: + - resolvesToDate +encode: object +description: | + Adds a number of time units to a date object. +arguments: + - + name: startDate + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The beginning date, in UTC, for the addition operation. The startDate can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: unit + type: + - timeUnit + description: | + The unit used to measure the amount of time added to the startDate. + - + name: amount + type: + - resolvesToInt + - resolvesToLong + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Add a Future Date' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateAdd/#add-a-future-date' + pipeline: + - + $project: + expectedDeliveryDate: + $dateAdd: + startDate: '$purchaseDate' + unit: 'day' + amount: 3 + - + # Example uses the short form, the builder always generates the verbose form + # $merge: 'shipping' + $merge: + into: 'shipping' + - + name: 'Filter on a Date Range' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateAdd/#filter-on-a-date-range' + pipeline: + - + $match: + $expr: + $gt: + - '$deliveryDate' + - + $dateAdd: + startDate: '$purchaseDate' + unit: 'day' + amount: 5 + - + $project: + _id: 0 + custId: 1 + purchased: + $dateToString: + format: '%Y-%m-%d' + date: '$purchaseDate' + delivery: + $dateToString: + format: '%Y-%m-%d' + date: '$deliveryDate' + - + name: 'Adjust for Daylight Savings Time' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateAdd/#adjust-for-daylight-savings-time' + pipeline: + - + $project: + _id: 0 + location: 1 + start: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: '$login' + days: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: + $dateAdd: + startDate: '$login' + unit: 'day' + amount: 1 + timezone: '$location' + hours: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: + $dateAdd: + startDate: '$login' + unit: 'hour' + amount: 24 + timezone: '$location' + startTZInfo: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: '$login' + timezone: '$location' + daysTZInfo: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: + $dateAdd: + startDate: '$login' + unit: 'day' + amount: 1 + timezone: '$location' + timezone: '$location' + hoursTZInfo: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: + $dateAdd: + startDate: '$login' + unit: 'hour' + amount: 24 + timezone: '$location' + timezone: '$location' diff --git a/generator/config/expression/dateDiff.yaml b/generator/config/expression/dateDiff.yaml new file mode 100644 index 000000000..42cb55d15 --- /dev/null +++ b/generator/config/expression/dateDiff.yaml @@ -0,0 +1,114 @@ +# $schema: ../schema.json +name: $dateDiff +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateDiff/' +type: + - resolvesToInt +encode: object +description: | + Returns the difference between two dates. +arguments: + - + name: startDate + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The start of the time period. The startDate can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: endDate + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The end of the time period. The endDate can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: unit + type: + - timeUnit + description: | + The time measurement unit between the startDate and endDate + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + - + name: startOfWeek + type: + - resolvesToString + optional: true + description: | + Used when the unit is equal to week. Defaults to Sunday. The startOfWeek parameter is an expression that resolves to a case insensitive string +tests: + - + name: 'Elapsed Time' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateDiff/#elapsed-time' + pipeline: + - + $group: + _id: ~ + averageTime: + $avg: + $dateDiff: + startDate: '$purchased' + endDate: '$delivered' + unit: 'day' + - + $project: + _id: 0 + numDays: + $trunc: + - '$averageTime' + - 1 + - + name: 'Result Precision' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateDiff/#result-precision' + pipeline: + - + $project: + Start: '$start' + End: '$end' + years: + $dateDiff: + startDate: '$start' + endDate: '$end' + unit: 'year' + months: + $dateDiff: + startDate: '$start' + endDate: '$end' + unit: 'month' + days: + $dateDiff: + startDate: '$start' + endDate: '$end' + unit: 'day' + _id: 0 + - + name: 'Weeks Per Month' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateDiff/#weeks-per-month' + pipeline: + - + $project: + wks_default: + $dateDiff: + startDate: '$start' + endDate: '$end' + unit: 'week' + wks_monday: + $dateDiff: + startDate: '$start' + endDate: '$end' + unit: 'week' + startOfWeek: 'Monday' + wks_friday: + $dateDiff: + startDate: '$start' + endDate: '$end' + unit: 'week' + startOfWeek: 'fri' + _id: 0 diff --git a/generator/config/expression/dateFromParts.yaml b/generator/config/expression/dateFromParts.yaml new file mode 100644 index 000000000..3ed35004e --- /dev/null +++ b/generator/config/expression/dateFromParts.yaml @@ -0,0 +1,114 @@ +# $schema: ../schema.json +name: $dateFromParts +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateFromParts/' +type: + - resolvesToDate +encode: object +description: | + Constructs a BSON Date object given the date's constituent parts. +arguments: + - + name: year + type: + - resolvesToNumber + optional: true + description: | + Calendar year. Can be any expression that evaluates to a number. + - + name: isoWeekYear + type: + - resolvesToNumber + optional: true + description: | + ISO Week Date Year. Can be any expression that evaluates to a number. + - + name: month + type: + - resolvesToNumber + optional: true + description: | + Month. Defaults to 1. + - + name: isoWeek + type: + - resolvesToNumber + optional: true + description: | + Week of year. Defaults to 1. + - + name: day + type: + - resolvesToNumber + optional: true + description: | + Day of month. Defaults to 1. + - + name: isoDayOfWeek + type: + - resolvesToNumber + optional: true + description: | + Day of week (Monday 1 - Sunday 7). Defaults to 1. + - + name: hour + type: + - resolvesToNumber + optional: true + description: | + Hour. Defaults to 0. + - + name: minute + type: + - resolvesToNumber + optional: true + description: | + Minute. Defaults to 0. + - + name: second + type: + - resolvesToNumber + optional: true + description: | + Second. Defaults to 0. + - + name: millisecond + type: + - resolvesToNumber + optional: true + description: | + Millisecond. Defaults to 0. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateFromParts/#example' + pipeline: + - + $project: + date: + $dateFromParts: + year: 2017 + month: 2 + day: 8 + hour: 12 + date_iso: + $dateFromParts: + isoWeekYear: 2017 + isoWeek: 6 + isoDayOfWeek: 3 + hour: 12 + date_timezone: + $dateFromParts: + year: 2016 + month: 12 + day: 31 + hour: 23 + minute: 46 + second: 12 + timezone: 'America/New_York' diff --git a/generator/config/expression/dateFromString.yaml b/generator/config/expression/dateFromString.yaml new file mode 100644 index 000000000..713200f5d --- /dev/null +++ b/generator/config/expression/dateFromString.yaml @@ -0,0 +1,80 @@ +# $schema: ../schema.json +name: $dateFromString +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateFromString/' +type: + - resolvesToDate +encode: object +description: | + Converts a date/time string to a date object. +arguments: + - + name: dateString + type: + - resolvesToString + description: | + The date/time string to convert to a date object. + - + name: format + type: + - resolvesToString + optional: true + description: | + The date format specification of the dateString. The format can be any expression that evaluates to a string literal, containing 0 or more format specifiers. + If unspecified, $dateFromString uses "%Y-%m-%dT%H:%M:%S.%LZ" as the default format but accepts a variety of formats and attempts to parse the dateString if possible. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The time zone to use to format the date. + - + name: onError + type: + - expression + optional: true + description: | + If $dateFromString encounters an error while parsing the given dateString, it outputs the result value of the provided onError expression. This result value can be of any type. + If you do not specify onError, $dateFromString throws an error if it cannot parse dateString. + - + name: onNull + type: + - expression + optional: true + description: | + If the dateString provided to $dateFromString is null or missing, it outputs the result value of the provided onNull expression. This result value can be of any type. + If you do not specify onNull and dateString is null or missing, then $dateFromString outputs null. +tests: + - + name: 'Converting Dates' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateFromString/#converting-dates' + pipeline: + - + $project: + date: + $dateFromString: + dateString: '$date' + timezone: 'America/New_York' + - + name: 'onError' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateFromString/#onerror' + pipeline: + - + $project: + date: + $dateFromString: + dateString: '$date' + timezone: '$timezone' + onError: '$date' + - + name: 'onNull' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateFromString/#onnull' + pipeline: + - + $project: + date: + $dateFromString: + dateString: '$date' + timezone: '$timezone' + onNull: !bson_utcdatetime 0 + diff --git a/generator/config/expression/dateSubtract.yaml b/generator/config/expression/dateSubtract.yaml new file mode 100644 index 000000000..e463fe8f8 --- /dev/null +++ b/generator/config/expression/dateSubtract.yaml @@ -0,0 +1,139 @@ +# $schema: ../schema.json +name: $dateSubtract +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateSubtract/' +type: + - resolvesToDate +encode: object +description: | + Subtracts a number of time units from a date object. +arguments: + - + name: startDate + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The beginning date, in UTC, for the addition operation. The startDate can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: unit + type: + - timeUnit + description: | + The unit used to measure the amount of time added to the startDate. + - + name: amount + type: + - resolvesToInt + - resolvesToLong + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Subtract A Fixed Amount' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateSubtract/#subtract-a-fixed-amount' + pipeline: + - + $match: + $expr: + $eq: + - + # Example uses the short form, the builder always generates the verbose form + # $month: '$logout' + $month: + date: '$logout' + - 1 + - + $project: + logoutTime: + $dateSubtract: + startDate: '$logout' + unit: 'hour' + amount: 3 + - + # Example uses the short form, the builder always generates the verbose form + # $merge: 'connectionTime' + $merge: + into: 'connectionTime' + - + name: 'Filter by Relative Dates' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateSubtract/#filter-by-relative-dates' + pipeline: + - + $match: + $expr: + $gt: + - '$logoutTime' + - + $dateSubtract: + startDate: '$$NOW' + unit: 'week' + amount: 1 + - + $project: + _id: 0 + custId: 1 + loggedOut: + $dateToString: + format: '%Y-%m-%d' + date: '$logoutTime' + - + name: 'Adjust for Daylight Savings Time' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateSubtract/#adjust-for-daylight-savings-time' + pipeline: + - + $project: + _id: 0 + location: 1 + start: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: '$login' + days: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: + $dateSubtract: + startDate: '$login' + unit: 'day' + amount: 1 + timezone: '$location' + hours: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: + $dateSubtract: + startDate: '$login' + unit: 'hour' + amount: 24 + timezone: '$location' + startTZInfo: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: '$login' + timezone: '$location' + daysTZInfo: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: + $dateSubtract: + startDate: '$login' + unit: 'day' + amount: 1 + timezone: '$location' + timezone: '$location' + hoursTZInfo: + $dateToString: + format: '%Y-%m-%d %H:%M' + date: + $dateSubtract: + startDate: '$login' + unit: 'hour' + amount: 24 + timezone: '$location' + timezone: '$location' diff --git a/generator/config/expression/dateToParts.yaml b/generator/config/expression/dateToParts.yaml new file mode 100644 index 000000000..d250e052f --- /dev/null +++ b/generator/config/expression/dateToParts.yaml @@ -0,0 +1,49 @@ +# $schema: ../schema.json +name: $dateToParts +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToParts/' +type: + - resolvesToObject +encode: object +description: | + Returns a document containing the constituent parts of a date. +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The input date for which to return parts. date can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + - + name: iso8601 + type: + - bool + optional: true + description: | + If set to true, modifies the output document to use ISO week date fields. Defaults to false. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToParts/#example' + pipeline: + - + $project: + date: + $dateToParts: + date: '$date' + date_iso: + $dateToParts: + date: '$date' + iso8601: true + date_timezone: + $dateToParts: + date: '$date' + timezone: 'America/New_York' diff --git a/generator/config/expression/dateToString.yaml b/generator/config/expression/dateToString.yaml new file mode 100644 index 000000000..29e0ea8c8 --- /dev/null +++ b/generator/config/expression/dateToString.yaml @@ -0,0 +1,81 @@ +# $schema: ../schema.json +name: $dateToString +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToString/' +type: + - resolvesToString +encode: object +description: | + Returns the date as a formatted string. +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to convert to string. Must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: format + type: + - resolvesToString + optional: true + description: | + The date format specification of the dateString. The format can be any expression that evaluates to a string literal, containing 0 or more format specifiers. + If unspecified, $dateFromString uses "%Y-%m-%dT%H:%M:%S.%LZ" as the default format but accepts a variety of formats and attempts to parse the dateString if possible. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The time zone to use to format the date. + - + name: onNull + type: + - expression + optional: true + description: | + The value to return if the date is null or missing. + If unspecified, $dateToString returns null if the date is null or missing. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToString/#example' + pipeline: + - + $project: + yearMonthDayUTC: + $dateToString: + format: '%Y-%m-%d' + date: '$date' + timewithOffsetNY: + $dateToString: + format: '%H:%M:%S:%L%z' + date: '$date' + timezone: 'America/New_York' + timewithOffset430: + $dateToString: + format: '%H:%M:%S:%L%z' + date: '$date' + timezone: '+04:30' + minutesOffsetNY: + $dateToString: + format: '%Z' + date: '$date' + timezone: 'America/New_York' + minutesOffset430: + $dateToString: + format: '%Z' + date: '$date' + timezone: '+04:30' + abbreviated_month: + $dateToString: + format: '%b' + date: '$date' + timezone: '+04:30' + full_month: + $dateToString: + format: '%B' + date: '$date' + timezone: '+04:30' diff --git a/generator/config/expression/dateTrunc.yaml b/generator/config/expression/dateTrunc.yaml new file mode 100644 index 000000000..aa3dcd6ca --- /dev/null +++ b/generator/config/expression/dateTrunc.yaml @@ -0,0 +1,77 @@ +# $schema: ../schema.json +name: $dateTrunc +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateTrunc/' +type: + - resolvesToDate +encode: object +description: | + Truncates a date. +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to truncate, specified in UTC. The date can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: unit + type: + - timeUnit + description: | + The unit of time, specified as an expression that must resolve to one of these strings: year, quarter, week, month, day, hour, minute, second. + Together, binSize and unit specify the time period used in the $dateTrunc calculation. + - + name: binSize + type: + - resolvesToNumber + optional: true + description: | + The numeric time value, specified as an expression that must resolve to a positive non-zero number. Defaults to 1. + Together, binSize and unit specify the time period used in the $dateTrunc calculation. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + - + name: startOfWeek + type: + - string + optional: true + description: | + The start of the week. Used when + unit is week. Defaults to Sunday. +tests: + - + name: 'Truncate Order Dates in a $project Pipeline Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateTrunc/#truncate-order-dates-in-a--project-pipeline-stage' + pipeline: + - + $project: + _id: 1 + orderDate: 1 + truncatedOrderDate: + $dateTrunc: + date: '$orderDate' + unit: 'week' + binSize: 2 + timezone: 'America/Los_Angeles' + startOfWeek: 'Monday' + - + name: 'Truncate Order Dates and Obtain Quantity Sum in a $group Pipeline Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateTrunc/#truncate-order-dates-and-obtain-quantity-sum-in-a--group-pipeline-stage' + pipeline: + - + $group: + _id: + truncatedOrderDate: + $dateTrunc: + date: '$orderDate' + unit: 'month' + binSize: 6 + sumQuantity: + $sum: '$quantity' diff --git a/generator/config/expression/dayOfMonth.yaml b/generator/config/expression/dayOfMonth.yaml new file mode 100644 index 000000000..46a4de0b7 --- /dev/null +++ b/generator/config/expression/dayOfMonth.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $dayOfMonth +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfMonth/' +type: + - resolvesToInt +encode: object +description: | + Returns the day of the month for a date as a number between 1 and 31. +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfMonth/#example' + pipeline: + - + $project: + day: + # Example uses the short form, the builder always generates the verbose form + # $dayOfMonth: '$date' + $dayOfMonth: + date: '$date' diff --git a/generator/config/expression/dayOfWeek.yaml b/generator/config/expression/dayOfWeek.yaml new file mode 100644 index 000000000..27a6a809d --- /dev/null +++ b/generator/config/expression/dayOfWeek.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $dayOfWeek +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfWeek/' +type: + - resolvesToInt +encode: object +description: | + Returns the day of the week for a date as a number between 1 (Sunday) and 7 (Saturday). +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfWeek/#example' + pipeline: + - + $project: + dayOfWeek: + # Example uses the short form, the builder always generates the verbose form + # $dayOfWeek: '$date' + $dayOfWeek: + date: '$date' diff --git a/generator/config/expression/dayOfYear.yaml b/generator/config/expression/dayOfYear.yaml new file mode 100644 index 000000000..8caa0374d --- /dev/null +++ b/generator/config/expression/dayOfYear.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $dayOfYear +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfYear/' +type: + - resolvesToInt +encode: object +description: | + Returns the day of the year for a date as a number between 1 and 366 (leap year). +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfYear/#example' + pipeline: + - + $project: + dayOfYear: + # Example uses the short form, the builder always generates the verbose form + # $dayOfYear: '$date' + $dayOfYear: + date: '$date' diff --git a/generator/config/expression/degreesToRadians.yaml b/generator/config/expression/degreesToRadians.yaml new file mode 100644 index 000000000..59c18d2e3 --- /dev/null +++ b/generator/config/expression/degreesToRadians.yaml @@ -0,0 +1,30 @@ +# $schema: ../schema.json +name: $degreesToRadians +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/degreesToRadians/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Converts a value from degrees to radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $degreesToRadians takes any valid expression that resolves to a number. + By default $degreesToRadians returns values as a double. $degreesToRadians can also return values as a 128-bit decimal as long as the resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/degreesToRadians/#example' + pipeline: + - + $addFields: + angle_a_rad: + $degreesToRadians: '$angle_a' + angle_b_rad: + $degreesToRadians: '$angle_b' + angle_c_rad: + $degreesToRadians: '$angle_c' diff --git a/generator/config/expression/divide.yaml b/generator/config/expression/divide.yaml new file mode 100644 index 000000000..3b69389d8 --- /dev/null +++ b/generator/config/expression/divide.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $divide +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/divide/' +type: + - resolvesToDouble +encode: array +description: | + Returns the result of dividing the first number by the second. Accepts two argument expressions. +arguments: + - + name: dividend + type: + - resolvesToNumber + description: | + The first argument is the dividend, and the second argument is the divisor; i.e. the first argument is divided by the second argument. + - + name: divisor + type: + - resolvesToNumber +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/divide/#example' + pipeline: + - + $project: + city: 1 + workdays: + $divide: + - '$hours' + - 8 diff --git a/generator/config/expression/eq.yaml b/generator/config/expression/eq.yaml new file mode 100644 index 000000000..009280ba1 --- /dev/null +++ b/generator/config/expression/eq.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $eq +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/eq/' +type: + - resolvesToBool +encode: array +description: | + Returns true if the values are equivalent. +arguments: + - + name: expression1 + type: + - expression + - + name: expression2 + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/eq/#example' + pipeline: + - + $project: + item: 1 + qty: 1 + qtyEq250: + $eq: + - '$qty' + - 250 + _id: 0 diff --git a/generator/config/expression/exp.yaml b/generator/config/expression/exp.yaml new file mode 100644 index 000000000..d1f6982a1 --- /dev/null +++ b/generator/config/expression/exp.yaml @@ -0,0 +1,25 @@ +# $schema: ../schema.json +name: $exp +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/exp/' +type: + - resolvesToDouble +encode: single +description: | + Raises e to the specified exponent. +arguments: + - + name: exponent + type: + - resolvesToNumber +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/exp/#example' + pipeline: + - + $project: + effectiveRate: + $subtract: + - + $exp: '$interestRate' + - 1 diff --git a/generator/config/expression/filter.yaml b/generator/config/expression/filter.yaml new file mode 100644 index 000000000..0b72f6d75 --- /dev/null +++ b/generator/config/expression/filter.yaml @@ -0,0 +1,94 @@ +# $schema: ../schema.json +name: $filter +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/' +type: + - resolvesToArray +encode: object +description: | + Selects a subset of the array to return an array with only the elements that match the filter condition. +arguments: + - + name: input + type: + - resolvesToArray + - + name: cond + type: + - resolvesToBool + description: | + An expression that resolves to a boolean value used to determine if an element should be included in the output array. The expression references each element of the input array individually with the variable name specified in as. + - + name: as + type: + - string + optional: true + description: | + A name for the variable that represents each individual element of the input array. If no name is specified, the variable name defaults to this. + - + name: limit + type: + - resolvesToInt + optional: true + description: | + A number expression that restricts the number of matching array elements that $filter returns. You cannot specify a limit less than 1. The matching array elements are returned in the order they appear in the input array. + If the specified limit is greater than the number of matching array elements, $filter returns all matching array elements. If the limit is null, $filter returns all matching array elements. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/#examples' + pipeline: + - + $project: + items: + $filter: + input: '$items' + as: 'item' + cond: + $gte: + - '$$item.price' + - 100 + - + name: 'Using the limit field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/#using-the-limit-field' + pipeline: + - + $project: + items: + $filter: + input: '$items' + cond: + $gte: + - '$$item.price' + - 100 + as: 'item' + limit: 1 + - + name: 'limit as a Numeric Expression' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/#limit-as-a-numeric-expression' + pipeline: + - + $project: + items: + $filter: + input: '$items' + cond: + $lte: + - '$$item.price' + - 150 + as: 'item' + limit: 2 + - + name: 'limit Greater than Possible Matches' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/#limit-greater-than-possible-matches' + pipeline: + - + $project: + items: + $filter: + input: '$items' + cond: + $gte: + - '$$item.price' + - 100 + as: 'item' + limit: 5 diff --git a/generator/config/expression/first.yaml b/generator/config/expression/first.yaml new file mode 100644 index 000000000..262d340c3 --- /dev/null +++ b/generator/config/expression/first.yaml @@ -0,0 +1,21 @@ +# $schema: ../schema.json +name: $first +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/first/' +type: + - resolvesToAny +encode: single +description: | + Returns the result of an expression for the first document in an array. +arguments: + - + name: expression + type: + - resolvesToArray +tests: + - + name: 'Use in $addFields Stage' + pipeline: + - + $addFields: + firstItem: + $first: '$items' diff --git a/generator/config/expression/firstN.yaml b/generator/config/expression/firstN.yaml new file mode 100644 index 000000000..e914ff88c --- /dev/null +++ b/generator/config/expression/firstN.yaml @@ -0,0 +1,47 @@ +# $schema: ../schema.json +name: $firstN +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN-array-element/' +type: + - resolvesToArray +encode: object +description: | + Returns a specified number of elements from the beginning of an array. +arguments: + - + name: 'n' + type: + - resolvesToInt + description: | + An expression that resolves to a positive integer. The integer specifies the number of array elements that $firstN returns. + - + name: input + type: + - resolvesToArray + description: | + An expression that resolves to the array from which to return n elements. + +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN-array-element/#example' + pipeline: + - + $addFields: + firstScores: + $firstN: + n: 3 + input: '$score' + - + name: 'Using $firstN as an Aggregation Expression' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN/#using--firstn-as-an-aggregation-expression' + pipeline: + - + $documents: + - + array: [10, 20, 30, 40] + - + $project: + firstThreeElements: + $firstN: + input: '$array' + n: 3 diff --git a/generator/config/expression/floor.yaml b/generator/config/expression/floor.yaml new file mode 100644 index 000000000..4c5856264 --- /dev/null +++ b/generator/config/expression/floor.yaml @@ -0,0 +1,23 @@ +# $schema: ../schema.json +name: $floor +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/floor/' +type: + - resolvesToInt +encode: single +description: | + Returns the largest integer less than or equal to the specified number. +arguments: + - + name: expression + type: + - resolvesToNumber +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/floor/#example' + pipeline: + - + $project: + value: 1 + floorValue: + $floor: '$value' diff --git a/generator/config/expression/function.yaml b/generator/config/expression/function.yaml new file mode 100644 index 000000000..fa4dba8b5 --- /dev/null +++ b/generator/config/expression/function.yaml @@ -0,0 +1,74 @@ +# $schema: ../schema.json +name: $function +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/function/' +type: + - resolvesToAny +encode: object +description: | + Defines a custom function. + New in MongoDB 4.4. +arguments: + - + name: body + type: + - javascript + description: | + The function definition. You can specify the function definition as either BSON\JavaScript or string. + function(arg1, arg2, ...) { ... } + - + name: args + type: + - array + description: | + Arguments passed to the function body. If the body function does not take an argument, you can specify an empty array [ ]. + default: [] + - + name: lang + type: + - string + default: js +tests: + - + name: 'Usage Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/function/#example-1--usage-example' + pipeline: + - + $addFields: + isFound: + $function: + body: + $code: |- + function(name) { + return hex_md5(name) == "15b0a220baa16331e8d80e15367677ad" + } + args: + - '$name' + lang: 'js' + message: + $function: + body: + $code: |- + function(name, scores) { + let total = Array.sum(scores); + return `Hello ${name}. Your total score is ${total}.` + } + args: + - '$name' + - '$scores' + lang: 'js' + - + name: 'Alternative to $where' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/function/#example-2--alternative-to--where' + pipeline: + - + $match: + $expr: + $function: + body: + $code: |- + function(name) { + return hex_md5(name) == "15b0a220baa16331e8d80e15367677ad"; + } + args: + - '$name' + lang: 'js' diff --git a/generator/config/expression/getField.yaml b/generator/config/expression/getField.yaml new file mode 100644 index 000000000..04b5d4ace --- /dev/null +++ b/generator/config/expression/getField.yaml @@ -0,0 +1,69 @@ +# $schema: ../schema.json +name: $getField +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/getField/' +type: + - resolvesToAny +encode: object +description: | + Returns the value of a specified field from a document. You can use $getField to retrieve the value of fields with names that contain periods (.) or start with dollar signs ($). + New in MongoDB 5.0. +arguments: + - + name: field + type: + - resolvesToString + description: | + Field in the input object for which you want to return a value. field can be any valid expression that resolves to a string constant. + If field begins with a dollar sign ($), place the field name inside of a $literal expression to return its value. + - + name: input + type: + - expression + optional: true + description: | + Default: $$CURRENT + A valid expression that contains the field for which you want to return a value. input must resolve to an object, missing, null, or undefined. If omitted, defaults to the document currently being processed in the pipeline ($$CURRENT). +tests: + - + name: 'Query Fields that Contain Periods' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/getField/#query-fields-that-contain-periods--.-' + pipeline: + - + $match: + $expr: + $gt: + - + # Example uses the short form, the builder always generates the verbose form + # $getField: 'price.usd' + $getField: + field: 'price.usd' + - 200 + - + name: 'Query Fields that Start with a Dollar Sign' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/getField/#query-fields-that-start-with-a-dollar-sign----' + pipeline: + - + $match: + $expr: + $gt: + - + $getField: + # Example uses the short form, the builder always generates the verbose form + # $literal: '$price' + field: + $literal: '$price' + - 200 + - + name: 'Query a Field in a Sub-document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/getField/#query-a-field-in-a-sub-document' + pipeline: + - + $match: + $expr: + $lte: + - + $getField: + field: + $literal: '$small' + input: '$quantity' + - 20 diff --git a/generator/config/expression/gt.yaml b/generator/config/expression/gt.yaml new file mode 100644 index 000000000..ddd4dcdd0 --- /dev/null +++ b/generator/config/expression/gt.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $gt +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/gt/' +type: + - resolvesToBool +encode: array +description: | + Returns true if the first value is greater than the second. +arguments: + - + name: expression1 + type: + - expression + - + name: expression2 + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/gt/#example' + pipeline: + - + $project: + item: 1 + qty: 1 + qtyGt250: + $gt: + - '$qty' + - 250 + _id: 0 diff --git a/generator/config/expression/gte.yaml b/generator/config/expression/gte.yaml new file mode 100644 index 000000000..6b456daf6 --- /dev/null +++ b/generator/config/expression/gte.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $gte +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/gte/' +type: + - resolvesToBool +encode: array +description: | + Returns true if the first value is greater than or equal to the second. +arguments: + - + name: expression1 + type: + - expression + - + name: expression2 + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/gte/#example' + pipeline: + - + $project: + item: 1 + qty: 1 + qtyGte250: + $gte: + - '$qty' + - 250 + _id: 0 diff --git a/generator/config/expression/hour.yaml b/generator/config/expression/hour.yaml new file mode 100644 index 000000000..ebd62c3a0 --- /dev/null +++ b/generator/config/expression/hour.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $hour +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/hour/' +type: + - resolvesToInt +encode: object +description: | + Returns the hour for a date as a number between 0 and 23. +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/hour/#example' + pipeline: + - + $project: + hour: + # Example uses the short form, the builder always generates the verbose form + # $hour: '$date' + $hour: + date: '$date' diff --git a/generator/config/expression/ifNull.yaml b/generator/config/expression/ifNull.yaml new file mode 100644 index 000000000..9be8e9044 --- /dev/null +++ b/generator/config/expression/ifNull.yaml @@ -0,0 +1,38 @@ +# $schema: ../schema.json +name: $ifNull +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/ifNull/' +type: + - resolvesToAny +encode: single +description: | + Returns either the non-null result of the first expression or the result of the second expression if the first expression results in a null result. Null result encompasses instances of undefined values or missing fields. Accepts two expressions as arguments. The result of the second expression can be null. +arguments: + - + name: expression + type: + - expression + variadic: array +tests: + - + name: 'Single Input Expression' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/ifNull/#single-input-expression' + pipeline: + - + $project: + item: 1 + description: + $ifNull: + - '$description' + - 'Unspecified' + - + name: 'Multiple Input Expressions' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/ifNull/#multiple-input-expressions' + pipeline: + - + $project: + item: 1 + value: + $ifNull: + - '$description' + - '$quantity' + - 'Unspecified' diff --git a/generator/config/expression/in.yaml b/generator/config/expression/in.yaml new file mode 100644 index 000000000..7cef8c149 --- /dev/null +++ b/generator/config/expression/in.yaml @@ -0,0 +1,33 @@ +# $schema: ../schema.json +name: $in +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/in/' +type: + - resolvesToBool +encode: array +description: | + Returns a boolean indicating whether a specified value is in an array. +arguments: + - + name: expression + type: + - expression + description: | + Any valid expression expression. + - + name: array + type: + - resolvesToArray + description: | + Any valid expression that resolves to an array. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/in/#example' + pipeline: + - + $project: + store location: '$location' + has bananas: + $in: + - 'bananas' + - '$in_stock' diff --git a/generator/config/expression/indexOfArray.yaml b/generator/config/expression/indexOfArray.yaml new file mode 100644 index 000000000..5840ee0fa --- /dev/null +++ b/generator/config/expression/indexOfArray.yaml @@ -0,0 +1,48 @@ +# $schema: ../schema.json +name: $indexOfArray +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexOfArray/' +type: + - resolvesToInt +encode: array +description: | + Searches an array for an occurrence of a specified value and returns the array index of the first occurrence. Array indexes start at zero. +arguments: + - + name: array + type: + - resolvesToArray + description: | + Can be any valid expression as long as it resolves to an array. + If the array expression resolves to a value of null or refers to a field that is missing, $indexOfArray returns null. + If the array expression does not resolve to an array or null nor refers to a missing field, $indexOfArray returns an error. + - + name: search + type: + - expression + - + name: start + type: + - resolvesToInt + optional: true + description: | + An integer, or a number that can be represented as integers (such as 2.0), that specifies the starting index position for the search. Can be any valid expression that resolves to a non-negative integral number. + If unspecified, the starting index position for the search is the beginning of the string. + - + name: end + type: + - resolvesToInt + optional: true + description: | + An integer, or a number that can be represented as integers (such as 2.0), that specifies the ending index position for the search. Can be any valid expression that resolves to a non-negative integral number. If you specify a index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + If unspecified, the ending index position for the search is the end of the string. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexOfArray/#example' + pipeline: + - + $project: + index: + $indexOfArray: + - '$items' + - 2 diff --git a/generator/config/expression/indexOfBytes.yaml b/generator/config/expression/indexOfBytes.yaml new file mode 100644 index 000000000..7fe890891 --- /dev/null +++ b/generator/config/expression/indexOfBytes.yaml @@ -0,0 +1,50 @@ +# $schema: ../schema.json +name: $indexOfBytes +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexOfBytes/' +type: + - resolvesToInt +encode: array +description: | + Searches a string for an occurrence of a substring and returns the UTF-8 byte index of the first occurrence. If the substring is not found, returns -1. +arguments: + - + name: string + type: + - resolvesToString + description: | + Can be any valid expression as long as it resolves to a string. + If the string expression resolves to a value of null or refers to a field that is missing, $indexOfBytes returns null. + If the string expression does not resolve to a string or null nor refers to a missing field, $indexOfBytes returns an error. + - + name: substring + type: + - resolvesToString + description: | + Can be any valid expression as long as it resolves to a string. + - + name: start + type: + - resolvesToInt + optional: true + description: | + An integer, or a number that can be represented as integers (such as 2.0), that specifies the starting index position for the search. Can be any valid expression that resolves to a non-negative integral number. + If unspecified, the starting index position for the search is the beginning of the string. + - + name: end + type: + - resolvesToInt + optional: true + description: | + An integer, or a number that can be represented as integers (such as 2.0), that specifies the ending index position for the search. Can be any valid expression that resolves to a non-negative integral number. If you specify a index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + If unspecified, the ending index position for the search is the end of the string. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexOfBytes/#examples' + pipeline: + - + $project: + byteLocation: + $indexOfBytes: + - '$item' + - 'foo' diff --git a/generator/config/expression/indexOfCP.yaml b/generator/config/expression/indexOfCP.yaml new file mode 100644 index 000000000..88ed55ec6 --- /dev/null +++ b/generator/config/expression/indexOfCP.yaml @@ -0,0 +1,50 @@ +# $schema: ../schema.json +name: $indexOfCP +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexOfCP/' +type: + - resolvesToInt +encode: array +description: | + Searches a string for an occurrence of a substring and returns the UTF-8 code point index of the first occurrence. If the substring is not found, returns -1 +arguments: + - + name: string + type: + - resolvesToString + description: | + Can be any valid expression as long as it resolves to a string. + If the string expression resolves to a value of null or refers to a field that is missing, $indexOfCP returns null. + If the string expression does not resolve to a string or null nor refers to a missing field, $indexOfCP returns an error. + - + name: substring + type: + - resolvesToString + description: | + Can be any valid expression as long as it resolves to a string. + - + name: start + type: + - resolvesToInt + optional: true + description: | + An integer, or a number that can be represented as integers (such as 2.0), that specifies the starting index position for the search. Can be any valid expression that resolves to a non-negative integral number. + If unspecified, the starting index position for the search is the beginning of the string. + - + name: end + type: + - resolvesToInt + optional: true + description: | + An integer, or a number that can be represented as integers (such as 2.0), that specifies the ending index position for the search. Can be any valid expression that resolves to a non-negative integral number. If you specify a index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + If unspecified, the ending index position for the search is the end of the string. +tests: + - + name: 'Examples' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexOfCP/#examples' + pipeline: + - + $project: + cpLocation: + $indexOfCP: + - '$item' + - 'foo' diff --git a/generator/config/expression/isArray.yaml b/generator/config/expression/isArray.yaml new file mode 100644 index 000000000..b9a5d5cb5 --- /dev/null +++ b/generator/config/expression/isArray.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $isArray +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isArray/' +type: + - resolvesToBool +encode: array +description: | + Determines if the operand is an array. Returns a boolean. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isArray/#example' + pipeline: + - + $project: + items: + $cond: + if: + $and: + # Example uses the short form, the builder always generates the verbose form + - + $isArray: + - '$instock' + - + $isArray: + - '$ordered' + then: + $concatArrays: + - '$instock' + - '$ordered' + else: 'One or more fields is not an array.' diff --git a/generator/config/expression/isNumber.yaml b/generator/config/expression/isNumber.yaml new file mode 100644 index 000000000..3bce99e99 --- /dev/null +++ b/generator/config/expression/isNumber.yaml @@ -0,0 +1,75 @@ +# $schema: ../schema.json +name: $isNumber +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/' +type: + - resolvesToBool +encode: single +description: | + Returns boolean true if the specified expression resolves to an integer, decimal, double, or long. + Returns boolean false if the expression resolves to any other BSON type, null, or a missing field. + New in MongoDB 4.4. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Use $isNumber to Check if a Field is Numeric' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/#use--isnumber-to-check-if-a-field-is-numeric' + pipeline: + - + $addFields: + isNumber: + $isNumber: '$reading' + hasType: + $type: '$reading' + - + name: 'Conditionally Modify Fields using $isNumber' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/#conditionally-modify-fields-using--isnumber' + pipeline: + - + $addFields: + points: + $cond: + if: + $isNumber: '$grade' + then: '$grade' + else: + $switch: + branches: + - + case: + $eq: + - '$grade' + - 'A' + then: 4 + - + case: + $eq: + - '$grade' + - 'B' + then: 3 + - + case: + $eq: + - '$grade' + - 'C' + then: 2 + - + case: + $eq: + - '$grade' + - 'D' + then: 1 + - + case: + $eq: + - '$grade' + - 'F' + then: 0 + - + $group: + _id: '$student_id' + GPA: + $avg: '$points' diff --git a/generator/config/expression/isoDayOfWeek.yaml b/generator/config/expression/isoDayOfWeek.yaml new file mode 100644 index 000000000..1956ff5c4 --- /dev/null +++ b/generator/config/expression/isoDayOfWeek.yaml @@ -0,0 +1,38 @@ +# $schema: ../schema.json +name: $isoDayOfWeek +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isoDayOfWeek/' +type: + - resolvesToInt +encode: object +description: | + Returns the weekday number in ISO 8601 format, ranging from 1 (for Monday) to 7 (for Sunday). +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isoDayOfWeek/#example' + pipeline: + - + $project: + _id: 0 + name: '$name' + dayOfWeek: + # Example uses the short form, the builder always generates the verbose form + # $isoDayOfWeek: '$birthday' + $isoDayOfWeek: + date: '$birthday' diff --git a/generator/config/expression/isoWeek.yaml b/generator/config/expression/isoWeek.yaml new file mode 100644 index 000000000..2958a20c3 --- /dev/null +++ b/generator/config/expression/isoWeek.yaml @@ -0,0 +1,38 @@ +# $schema: ../schema.json +name: $isoWeek +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isoWeek/' +type: + - resolvesToInt +encode: object +description: | + Returns the week number in ISO 8601 format, ranging from 1 to 53. Week numbers start at 1 with the week (Monday through Sunday) that contains the year's first Thursday. +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isoWeek/#example' + pipeline: + - + $project: + _id: 0 + city: '$city' + weekNumber: + # Example uses the short form, the builder always generates the verbose form + # $isoWeek: '$date' + $isoWeek: + date: '$date' diff --git a/generator/config/expression/isoWeekYear.yaml b/generator/config/expression/isoWeekYear.yaml new file mode 100644 index 000000000..5dcefa7dd --- /dev/null +++ b/generator/config/expression/isoWeekYear.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $isoWeekYear +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isoWeekYear/' +type: + - resolvesToInt +encode: object +description: | + Returns the year number in ISO 8601 format. The year starts with the Monday of week 1 (ISO 8601) and ends with the Sunday of the last week (ISO 8601). +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/isoWeekYear/#example' + pipeline: + - + $project: + yearNumber: + # Example uses the short form, the builder always generates the verbose form + # $isoWeekYear: '$date' + $isoWeekYear: + date: '$date' diff --git a/generator/config/expression/last.yaml b/generator/config/expression/last.yaml new file mode 100644 index 000000000..2bf18dc7b --- /dev/null +++ b/generator/config/expression/last.yaml @@ -0,0 +1,21 @@ +# $schema: ../schema.json +name: $last +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/last/' +type: + - resolvesToAny +encode: single +description: | + Returns the result of an expression for the last document in an array. +arguments: + - + name: expression + type: + - resolvesToArray +tests: + - + name: 'Use in $addFields Stage' + pipeline: + - + $addFields: + lastItem: + $last: '$items' diff --git a/generator/config/expression/lastN.yaml b/generator/config/expression/lastN.yaml new file mode 100644 index 000000000..0c29ad76e --- /dev/null +++ b/generator/config/expression/lastN.yaml @@ -0,0 +1,51 @@ +# $schema: ../schema.json +name: $lastN +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN-array-element/' +type: + - resolvesToArray +encode: object +description: | + Returns a specified number of elements from the end of an array. +arguments: + - + name: 'n' + type: + - resolvesToInt + description: | + An expression that resolves to a positive integer. The integer specifies the number of array elements that $firstN returns. + - + name: input + type: + - resolvesToArray + description: | + An expression that resolves to the array from which to return n elements. +tests: + - + name: Example + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN-array-element/#example' + pipeline: + - + $addFields: + lastScores: + $lastN: + n: 3 + input: '$score' + + - + name: 'Using $lastN as an Aggregation Expression' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN/#using--lastn-as-an-aggregation-expression' + pipeline: + - + $documents: + - + array: + - 10 + - 20 + - 30 + - 40 + - + $project: + lastThreeElements: + $lastN: + input: '$array' + n: 3 diff --git a/generator/config/expression/let.yaml b/generator/config/expression/let.yaml new file mode 100644 index 000000000..7d3017282 --- /dev/null +++ b/generator/config/expression/let.yaml @@ -0,0 +1,46 @@ +# $schema: ../schema.json +name: $let +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/let/' +type: + - resolvesToAny +encode: object +description: | + Defines variables for use within the scope of a subexpression and returns the result of the subexpression. Accepts named parameters. + Accepts any number of argument expressions. +arguments: + - + name: vars + type: + - object # of expression + description: | + Assignment block for the variables accessible in the in expression. To assign a variable, specify a string for the variable name and assign a valid expression for the value. + The variable assignments have no meaning outside the in expression, not even within the vars block itself. + - + name: in + type: + - expression + description: | + The expression to evaluate. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/let/#example' + pipeline: + - + $project: + finalTotal: + $let: + vars: + total: + $add: + - '$price' + - '$tax' + discounted: + $cond: + if: '$applyDiscount' + then: 0.9 + else: 1 + in: + $multiply: + - '$$total' + - '$$discounted' diff --git a/generator/config/expression/literal.yaml b/generator/config/expression/literal.yaml new file mode 100644 index 000000000..dfae7bbb3 --- /dev/null +++ b/generator/config/expression/literal.yaml @@ -0,0 +1,15 @@ +# $schema: ../schema.json +name: $literal +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/literal/' +type: + - resolvesToAny +encode: single +description: | + Return a value without parsing. Use for values that the aggregation pipeline may interpret as an expression. For example, use a $literal expression to a string that starts with a dollar sign ($) to avoid parsing as a field path. +arguments: + - + name: value + type: + - any + description: | + If the value is an expression, $literal does not evaluate the expression but instead returns the unparsed expression. diff --git a/generator/config/expression/ln.yaml b/generator/config/expression/ln.yaml new file mode 100644 index 000000000..f7412aeb9 --- /dev/null +++ b/generator/config/expression/ln.yaml @@ -0,0 +1,26 @@ +# $schema: ../schema.json +name: $ln +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/ln/' +type: + - resolvesToDouble +encode: single +description: | + Calculates the natural log of a number. + $ln is equivalent to $log: [ , Math.E ] expression, where Math.E is a JavaScript representation for Euler's number e. +arguments: + - + name: number + type: + - resolvesToNumber + description: | + Any valid expression as long as it resolves to a non-negative number. For more information on expressions, see Expressions. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/ln/#example' + pipeline: + - + $project: + x: '$year' + y: + $ln: '$sales' diff --git a/generator/config/expression/log.yaml b/generator/config/expression/log.yaml new file mode 100644 index 000000000..462eb9492 --- /dev/null +++ b/generator/config/expression/log.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $log +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/log/' +type: + - resolvesToDouble +encode: array +description: | + Calculates the log of a number in the specified base. +arguments: + - + name: number + type: + - resolvesToNumber + description: | + Any valid expression as long as it resolves to a non-negative number. + - + name: base + type: + - resolvesToNumber + description: | + Any valid expression as long as it resolves to a positive number greater than 1. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/log/#example' + pipeline: + - + $project: + bitsNeeded: + $floor: + $add: + - 1 + - + $log: + - '$int' + - 2 diff --git a/generator/config/expression/log10.yaml b/generator/config/expression/log10.yaml new file mode 100644 index 000000000..77cab075a --- /dev/null +++ b/generator/config/expression/log10.yaml @@ -0,0 +1,27 @@ +# $schema: ../schema.json +name: $log10 +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/log10/' +type: + - resolvesToDouble +encode: single +description: | + Calculates the log base 10 of a number. +arguments: + - + name: number + type: + - resolvesToNumber + description: | + Any valid expression as long as it resolves to a non-negative number. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/log10/#example' + pipeline: + - + $project: + pH: + $multiply: + - -1 + - + $log10: '$H3O' diff --git a/generator/config/expression/lt.yaml b/generator/config/expression/lt.yaml new file mode 100644 index 000000000..4b7319b97 --- /dev/null +++ b/generator/config/expression/lt.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $lt +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lt/' +type: + - resolvesToBool +encode: array +description: | + Returns true if the first value is less than the second. +arguments: + - + name: expression1 + type: + - expression + - + name: expression2 + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lt/#example' + pipeline: + - + $project: + item: 1 + qty: 1 + qtyLt250: + $lt: + - '$qty' + - 250 + _id: 0 diff --git a/generator/config/expression/lte.yaml b/generator/config/expression/lte.yaml new file mode 100644 index 000000000..91c7b1d88 --- /dev/null +++ b/generator/config/expression/lte.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $lte +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lte/' +type: + - resolvesToBool +encode: array +description: | + Returns true if the first value is less than or equal to the second. +arguments: + - + name: expression1 + type: + - expression + - + name: expression2 + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lte/#example' + pipeline: + - + $project: + item: 1 + qty: 1 + qtyLte250: + $lte: + - '$qty' + - 250 + _id: 0 diff --git a/generator/config/expression/ltrim.yaml b/generator/config/expression/ltrim.yaml new file mode 100644 index 000000000..992b70616 --- /dev/null +++ b/generator/config/expression/ltrim.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $ltrim +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/ltrim/' +type: + - resolvesToString +encode: object +description: | + Removes whitespace or the specified characters from the beginning of a string. + New in MongoDB 4.0. +arguments: + - + name: input + type: + - resolvesToString + description: | + The string to trim. The argument can be any valid expression that resolves to a string. + - + name: chars + type: + - resolvesToString + optional: true + description: | + The character(s) to trim from the beginning of the input. + The argument can be any valid expression that resolves to a string. The $ltrim operator breaks down the string into individual UTF code point to trim from input. + If unspecified, $ltrim removes whitespace characters, including the null character. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/ltrim/#example' + pipeline: + - + $project: + item: 1 + description: + $ltrim: + input: '$description' diff --git a/generator/config/expression/map.yaml b/generator/config/expression/map.yaml new file mode 100644 index 000000000..11db40c24 --- /dev/null +++ b/generator/config/expression/map.yaml @@ -0,0 +1,76 @@ +# $schema: ../schema.json +name: $map +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/map/' +type: + - resolvesToArray +encode: object +description: | + Applies a subexpression to each element of an array and returns the array of resulting values in order. Accepts named parameters. +arguments: + - + name: input + type: + - resolvesToArray + description: | + An expression that resolves to an array. + - + name: as + type: + - resolvesToString + optional: true + description: | + A name for the variable that represents each individual element of the input array. If no name is specified, the variable name defaults to this. + - + name: in + type: + - expression + description: | + An expression that is applied to each element of the input array. The expression references each element individually with the variable name specified in as. +tests: + - + name: 'Add to Each Element of an Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/map/#add-to-each-element-of-an-array' + pipeline: + - + $project: + adjustedGrades: + $map: + input: '$quizzes' + as: 'grade' + in: + $add: + - '$$grade' + - 2 + - + name: 'Truncate Each Array Element' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/map/#truncate-each-array-element' + pipeline: + - + $project: + city: '$city' + integerValues: + $map: + input: '$distances' + as: 'decimalValue' + in: + # Example uses the short form, the builder always generates the verbose form + # $trunc: '$$decimalValue' + $trunc: + - '$$decimalValue' + - + name: 'Convert Celsius Temperatures to Fahrenheit' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/map/#convert-celsius-temperatures-to-fahrenheit' + pipeline: + - + $addFields: + tempsF: + $map: + input: '$tempsC' + as: 'tempInCelsius' + in: + $add: + - + $multiply: + - '$$tempInCelsius' + - 1.8 + - 32 diff --git a/generator/config/expression/max.yaml b/generator/config/expression/max.yaml new file mode 100644 index 000000000..b413679c5 --- /dev/null +++ b/generator/config/expression/max.yaml @@ -0,0 +1,35 @@ +# $schema: ../schema.json +name: $max +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/max/' +type: + - resolvesToAny +encode: single +description: | + Returns the maximum value that results from applying an expression to each document. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - expression + variadic: array +tests: + - + name: 'Use in $project Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/max/#use-in--project-stage' + pipeline: + - + $project: + quizMax: + # Example uses the short form, the builder always generates the verbose form + # $max: '$quizzes' + $max: + - '$quizzes' + labMax: + # $max: '$labs' + $max: + - '$labs' + examMax: + $max: + - '$final' + - '$midterm' diff --git a/generator/config/expression/maxN.yaml b/generator/config/expression/maxN.yaml new file mode 100644 index 000000000..e0d9d243e --- /dev/null +++ b/generator/config/expression/maxN.yaml @@ -0,0 +1,32 @@ +# $schema: ../schema.json +name: $maxN +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/maxN-array-element/' +type: + - resolvesToArray +encode: object +description: | + Returns the n largest values in an array. Distinct from the $maxN accumulator. +arguments: + - + name: input + type: + - resolvesToArray + description: | + An expression that resolves to the array from which to return the maximal n elements. + - + name: 'n' + type: + - resolvesToInt + description: | + An expression that resolves to a positive integer. The integer specifies the number of array elements that $maxN returns. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/maxN-array-element/#example' + pipeline: + - + $addFields: + maxScores: + $maxN: + n: 2 + input: '$score' diff --git a/generator/config/expression/median.yaml b/generator/config/expression/median.yaml new file mode 100644 index 000000000..2708f7f16 --- /dev/null +++ b/generator/config/expression/median.yaml @@ -0,0 +1,43 @@ +# $schema: ../schema.json +name: $median +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/median/' +type: + - resolvesToDouble +encode: object +description: | + Returns an approximation of the median, the 50th percentile, as a scalar value. + New in MongoDB 7.0. + This operator is available as an accumulator in these stages: + $group + $setWindowFields + It is also available as an aggregation expression. +arguments: + - + name: input + type: + - resolvesToNumber + - array # of number + description: | + $median calculates the 50th percentile value of this data. input must be a field name or an expression that evaluates to a numeric type. If the expression cannot be converted to a numeric type, the $median calculation ignores it. + - + name: method + type: + - string # AccumulatorPercentile + description: | + The method that mongod uses to calculate the 50th percentile value. The method must be 'approximate'. +tests: + - + name: 'Use $median in a $project Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/median/#use-operatorname-in-a--project-stage' + pipeline: + - + $project: + _id: 0 + studentId: 1 + testMedians: + $median: + input: + - '$test01' + - '$test02' + - '$test03' + method: 'approximate' diff --git a/generator/config/expression/mergeObjects.yaml b/generator/config/expression/mergeObjects.yaml new file mode 100644 index 000000000..928166852 --- /dev/null +++ b/generator/config/expression/mergeObjects.yaml @@ -0,0 +1,39 @@ +# $schema: ../schema.json +name: $mergeObjects +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/mergeObjects/' +type: + - resolvesToObject +encode: single +description: | + Combines multiple documents into a single document. +arguments: + - + name: document + type: + - resolvesToObject + variadic: array + description: | + Any valid expression that resolves to a document. +tests: + - + name: '$mergeObjects' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/mergeObjects/#-mergeobjects' + pipeline: + - + $lookup: + from: 'items' + localField: 'item' + foreignField: 'item' + as: 'fromItems' + - + $replaceRoot: + newRoot: + $mergeObjects: + - + $arrayElemAt: + - '$fromItems' + - 0 + - '$$ROOT' + - + $project: + fromItems: 0 diff --git a/generator/config/expression/meta.yaml b/generator/config/expression/meta.yaml new file mode 100644 index 000000000..9a96f7aaf --- /dev/null +++ b/generator/config/expression/meta.yaml @@ -0,0 +1,39 @@ +# $schema: ../schema.json +name: $meta +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/meta/' +type: + - resolvesToAny +encode: single +description: | + Access available per-document metadata related to the aggregation operation. +arguments: + - + name: keyword + type: + - string +tests: + - + name: 'textScore' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/meta/#-meta---textscore-' + pipeline: + - + $match: + $text: + $search: 'cake' + - + $group: + _id: + $meta: 'textScore' + count: + $sum: 1 + - + name: 'indexKey' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/meta/#-meta---indexkey-' + pipeline: + - + $match: + type: 'apparel' + - + $addFields: + idxKey: + $meta: 'indexKey' diff --git a/generator/config/expression/millisecond.yaml b/generator/config/expression/millisecond.yaml new file mode 100644 index 000000000..af1d26e75 --- /dev/null +++ b/generator/config/expression/millisecond.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $millisecond +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/millisecond/' +type: + - resolvesToInt +encode: object +description: | + Returns the milliseconds of a date as a number between 0 and 999. +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/millisecond/#example' + pipeline: + - + $project: + milliseconds: + # Example uses the short form, the builder always generates the verbose form + # $millisecond: '$date' + $millisecond: + date: '$date' diff --git a/generator/config/expression/min.yaml b/generator/config/expression/min.yaml new file mode 100644 index 000000000..1212fd4f7 --- /dev/null +++ b/generator/config/expression/min.yaml @@ -0,0 +1,35 @@ +# $schema: ../schema.json +name: $min +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/min/' +type: + - resolvesToAny +encode: single +description: | + Returns the minimum value that results from applying an expression to each document. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - expression + variadic: array +tests: + - + name: 'Use in $project Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/min/#use-in--project-stage' + pipeline: + - + $project: + quizMin: + # Example uses the short form, the builder always generates the verbose form + # $min: '$quizzes' + $min: + - '$quizzes' + labMin: + # $min: '$labs' + $min: + - '$labs' + examMin: + $min: + - '$final' + - '$midterm' diff --git a/generator/config/expression/minN.yaml b/generator/config/expression/minN.yaml new file mode 100644 index 000000000..e7fa8cac1 --- /dev/null +++ b/generator/config/expression/minN.yaml @@ -0,0 +1,32 @@ +# $schema: ../schema.json +name: $minN +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/minN-array-element/' +type: + - resolvesToArray +encode: object +description: | + Returns the n smallest values in an array. Distinct from the $minN accumulator. +arguments: + - + name: input + type: + - resolvesToArray + description: | + An expression that resolves to the array from which to return the maximal n elements. + - + name: 'n' + type: + - resolvesToInt + description: | + An expression that resolves to a positive integer. The integer specifies the number of array elements that $maxN returns. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/minN-array-element/#example' + pipeline: + - + $addFields: + minScores: + $minN: + n: 2 + input: '$score' diff --git a/generator/config/expression/minute.yaml b/generator/config/expression/minute.yaml new file mode 100644 index 000000000..109e87f6b --- /dev/null +++ b/generator/config/expression/minute.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $minute +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/minute/' +type: + - resolvesToInt +encode: object +description: | + Returns the minute for a date as a number between 0 and 59. +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/minute/#example' + pipeline: + - + $project: + minutes: + # Example uses the short form, the builder always generates the verbose form + # $minute: '$date' + $minute: + date: '$date' diff --git a/generator/config/expression/mod.yaml b/generator/config/expression/mod.yaml new file mode 100644 index 000000000..0ac48bd54 --- /dev/null +++ b/generator/config/expression/mod.yaml @@ -0,0 +1,30 @@ +# $schema: ../schema.json +name: $mod +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/mod/' +type: + - resolvesToInt +encode: array +description: | + Returns the remainder of the first number divided by the second. Accepts two argument expressions. +arguments: + - + name: dividend + type: + - resolvesToNumber + description: | + The first argument is the dividend, and the second argument is the divisor; i.e. first argument is divided by the second argument. + - + name: divisor + type: + - resolvesToNumber +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/mod/#example' + pipeline: + - + $project: + remainder: + $mod: + - '$hours' + - '$tasks' diff --git a/generator/config/expression/month.yaml b/generator/config/expression/month.yaml new file mode 100644 index 000000000..7bd383be9 --- /dev/null +++ b/generator/config/expression/month.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $month +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/month/' +type: + - resolvesToInt +encode: object +description: | + Returns the month for a date as a number between 1 (January) and 12 (December). +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/month/#example' + pipeline: + - + $project: + month: + # Example uses the short form, the builder always generates the verbose form + # $month: '$date' + $month: + date: '$date' diff --git a/generator/config/expression/multiply.yaml b/generator/config/expression/multiply.yaml new file mode 100644 index 000000000..5a069cc8c --- /dev/null +++ b/generator/config/expression/multiply.yaml @@ -0,0 +1,30 @@ +# $schema: ../schema.json +name: $multiply +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/multiply/' +type: + - resolvesToDecimal +encode: single +description: | + Multiplies numbers to return the product. Accepts any number of argument expressions. +arguments: + - + name: expression + type: + - resolvesToNumber + variadic: array + description: | + The arguments can be any valid expression as long as they resolve to numbers. + Starting in MongoDB 6.1 you can optimize the $multiply operation. To improve performance, group references at the end of the argument list. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/multiply/#example' + pipeline: + - + $project: + date: 1 + item: 1 + total: + $multiply: + - '$price' + - '$quantity' diff --git a/generator/config/expression/ne.yaml b/generator/config/expression/ne.yaml new file mode 100644 index 000000000..da92f1014 --- /dev/null +++ b/generator/config/expression/ne.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $ne +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/ne/' +type: + - resolvesToBool +encode: array +description: | + Returns true if the values are not equivalent. +arguments: + - + name: expression1 + type: + - expression + - + name: expression2 + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/ne/#example' + pipeline: + - + $project: + item: 1 + qty: 1 + qtyNe250: + $ne: + - '$qty' + - 250 + _id: 0 diff --git a/generator/config/expression/not.yaml b/generator/config/expression/not.yaml new file mode 100644 index 000000000..d4e4c90ea --- /dev/null +++ b/generator/config/expression/not.yaml @@ -0,0 +1,28 @@ +# $schema: ../schema.json +name: $not +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/not/' +type: + - resolvesToBool +encode: array +description: | + Returns the boolean value that is the opposite of its argument expression. Accepts a single argument expression. +arguments: + - + name: expression + type: + - expression + - resolvesToBool +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/not/#example' + pipeline: + - + $project: + item: 1 + result: + $not: + - + $gt: + - '$qty' + - 250 diff --git a/generator/config/expression/objectToArray.yaml b/generator/config/expression/objectToArray.yaml new file mode 100644 index 000000000..460977f33 --- /dev/null +++ b/generator/config/expression/objectToArray.yaml @@ -0,0 +1,44 @@ +# $schema: ../schema.json +name: $objectToArray +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/objectToArray/' +type: + - resolvesToArray +encode: single +description: | + Converts a document to an array of documents representing key-value pairs. +arguments: + - + name: object + type: + - resolvesToObject + description: | + Any valid expression as long as it resolves to a document object. $objectToArray applies to the top-level fields of its argument. If the argument is a document that itself contains embedded document fields, the $objectToArray does not recursively apply to the embedded document fields. +tests: + # "$objectToArray and $arrayToObject Example" omitted as it's already in arrayToObject.yaml + - + name: '$objectToArray Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/objectToArray/#-objecttoarray-example' + pipeline: + - + $project: + item: 1 + dimensions: + $objectToArray: '$dimensions' + - + name: '$objectToArray to Sum Nested Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/objectToArray/#-objecttoarray-to-sum-nested-fields' + pipeline: + - + $project: + warehouses: + $objectToArray: '$instock' + - + # Example uses the short form, the builder always generates the verbose form + # $unwind: '$warehouses' + $unwind: + path: '$warehouses' + - + $group: + _id: '$warehouses.k' + total: + $sum: '$warehouses.v' diff --git a/generator/config/expression/or.yaml b/generator/config/expression/or.yaml new file mode 100644 index 000000000..2bbce1910 --- /dev/null +++ b/generator/config/expression/or.yaml @@ -0,0 +1,33 @@ +# $schema: ../schema.json +name: $or +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/or/' +type: + - resolvesToBool +encode: single +description: | + Returns true when any of its expressions evaluates to true. Accepts any number of argument expressions. +arguments: + - + name: expression + type: + - expression + - resolvesToBool + variadic: array +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/or/#example' + pipeline: + - + $project: + item: 1 + result: + $or: + - + $gt: + - '$qty' + - 250 + - + $lt: + - '$qty' + - 200 diff --git a/generator/config/expression/percentile.yaml b/generator/config/expression/percentile.yaml new file mode 100644 index 000000000..8c601cc83 --- /dev/null +++ b/generator/config/expression/percentile.yaml @@ -0,0 +1,51 @@ +# $schema: ../schema.json +name: $percentile +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/percentile/' +type: + - resolvesToArray # of scalar +encode: object +description: | + Returns an array of scalar values that correspond to specified percentile values. + New in MongoDB 7.0. + + This operator is available as an accumulator in these stages: + $group + + $setWindowFields + + It is also available as an aggregation expression. +arguments: + - + name: input + type: + - resolvesToNumber + - array # of number + description: | + $percentile calculates the percentile values of this data. input must be a field name or an expression that evaluates to a numeric type. If the expression cannot be converted to a numeric type, the $percentile calculation ignores it. + - + name: p + type: + - resolvesToArray # of resolvesToNumber + description: | + $percentile calculates a percentile value for each element in p. The elements represent percentages and must evaluate to numeric values in the range 0.0 to 1.0, inclusive. + $percentile returns results in the same order as the elements in p. + - + name: method + type: + - string # AccumulatorPercentile + description: | + The method that mongod uses to calculate the percentile value. The method must be 'approximate'. +tests: + - + name: 'Use $percentile in a $project Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/percentile/#use-operatorname-in-a--project-stage' + pipeline: + - + $project: + _id: 0 + studentId: 1 + testPercentiles: + $percentile: + input: ['$test01', '$test02', '$test03'] + p: [0.5, 0.95] + method: 'approximate' diff --git a/generator/config/expression/pow.yaml b/generator/config/expression/pow.yaml new file mode 100644 index 000000000..afab7f875 --- /dev/null +++ b/generator/config/expression/pow.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $pow +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/pow/' +type: + - resolvesToNumber +encode: array +description: | + Raises a number to the specified exponent. +arguments: + - + name: number + type: + - resolvesToNumber + - + name: exponent + type: + - resolvesToNumber +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/pow/#example' + pipeline: + - + $project: + variance: + $pow: + - + # Example uses the short form, the builder always generates the verbose form + # $stdDevPop: '$scores.score' + $stdDevPop: ['$scores.score'] + - 2 diff --git a/generator/config/expression/radiansToDegrees.yaml b/generator/config/expression/radiansToDegrees.yaml new file mode 100644 index 000000000..be4dd1e1d --- /dev/null +++ b/generator/config/expression/radiansToDegrees.yaml @@ -0,0 +1,27 @@ +# $schema: ../schema.json +name: $radiansToDegrees +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/radiansToDegrees/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Converts a value from radians to degrees. +arguments: + - + name: expression + type: + - resolvesToNumber +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/radiansToDegrees/#example' + pipeline: + - + $addFields: + angle_a_deg: + $radiansToDegrees: '$angle_a' + angle_b_deg: + $radiansToDegrees: '$angle_b' + angle_c_deg: + $radiansToDegrees: '$angle_c' diff --git a/generator/config/expression/rand.yaml b/generator/config/expression/rand.yaml new file mode 100644 index 000000000..a19f3163e --- /dev/null +++ b/generator/config/expression/rand.yaml @@ -0,0 +1,48 @@ +# $schema: ../schema.json +name: $rand +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/rand/' +type: + - resolvesToDouble +encode: object +description: | + Returns a random float between 0 and 1 +tests: + - + name: 'Generate Random Data Points' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/rand/#generate-random-data-points' + pipeline: + - + $set: + amount: + $multiply: + - + $rand: {} + - 100 + - + $set: + amount: + $floor: '$amount' + - + # Example uses the short form, the builder always generates the verbose form + # $merge: 'donors' + $merge: + into: 'donors' + - + name: 'Select Random Items From a Collection' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/rand/#select-random-items-from-a-collection' + pipeline: + - + $match: + district: 3 + - + $match: + $expr: + $lt: + - 0.5 + - + $rand: {} + - + $project: + _id: 0 + name: 1 + registered: 1 diff --git a/generator/config/expression/range.yaml b/generator/config/expression/range.yaml new file mode 100644 index 000000000..5c78d8898 --- /dev/null +++ b/generator/config/expression/range.yaml @@ -0,0 +1,42 @@ +# $schema: ../schema.json +name: $range +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/range/' +type: + - resolvesToArray # of int +encode: array +description: | + Outputs an array containing a sequence of integers according to user-defined inputs. +arguments: + - + name: start + type: + - resolvesToInt + description: | + An integer that specifies the start of the sequence. Can be any valid expression that resolves to an integer. + - + name: end + type: + - resolvesToInt + description: | + An integer that specifies the exclusive upper limit of the sequence. Can be any valid expression that resolves to an integer. + - + name: step + type: + - resolvesToInt + optional: true + description: | + An integer that specifies the increment value. Can be any valid expression that resolves to a non-zero integer. Defaults to 1. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/range/#example' + pipeline: + - + $project: + _id: 0 + city: 1 + Rest stops: + $range: + - 0 + - '$distance' + - 25 diff --git a/generator/config/expression/reduce.yaml b/generator/config/expression/reduce.yaml new file mode 100644 index 000000000..bf51eabe3 --- /dev/null +++ b/generator/config/expression/reduce.yaml @@ -0,0 +1,134 @@ +# $schema: ../schema.json +name: $reduce +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/reduce/' +type: + - resolvesToAny +encode: object +description: | + Applies an expression to each element in an array and combines them into a single value. +arguments: + - + name: input + type: + - resolvesToArray + description: | + Can be any valid expression that resolves to an array. + If the argument resolves to a value of null or refers to a missing field, $reduce returns null. + If the argument does not resolve to an array or null nor refers to a missing field, $reduce returns an error. + - + name: initialValue + type: + - expression + description: | + The initial cumulative value set before in is applied to the first element of the input array. + - + name: in + type: + - expression + description: | + A valid expression that $reduce applies to each element in the input array in left-to-right order. Wrap the input value with $reverseArray to yield the equivalent of applying the combining expression from right-to-left. + During evaluation of the in expression, two variables will be available: + - value is the variable that represents the cumulative value of the expression. + - this is the variable that refers to the element being processed. +tests: + - + name: 'Multiplication' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/reduce/#multiplication' + pipeline: + - + $group: + _id: '$experimentId' + probabilityArr: + $push: '$probability' + - + $project: + description: 1 + results: + $reduce: + input: '$probabilityArr' + initialValue: 1 + in: + $multiply: + - '$$value' + - '$$this' + - + name: 'Discounted Merchandise' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/reduce/#discounted-merchandise' + pipeline: + - + $project: + discountedPrice: + $reduce: + input: '$discounts' + initialValue: '$price' + in: + $multiply: + - '$$value' + - + $subtract: + - 1 + - '$$this' + - + name: 'String Concatenation' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/reduce/#string-concatenation' + pipeline: + # Filter to return only non-empty arrays + - + $match: + hobbies: + $gt: [] + - + $project: + name: 1 + bio: + $reduce: + input: '$hobbies' + initialValue: 'My hobbies include:' + in: + $concat: + - '$$value' + - + $cond: + if: + $eq: + - '$$value' + - 'My hobbies include:' + then: ' ' + else: ', ' + - '$$this' + - + name: 'Array Concatenation' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/reduce/#array-concatenation' + pipeline: + - + $project: + collapsed: + $reduce: + input: '$arr' + initialValue: [] + in: + $concatArrays: + - '$$value' + - '$$this' + - + name: 'Computing a Multiple Reductions' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/reduce/#computing-a-multiple-reductions' + pipeline: + - + $project: + results: + $reduce: + input: '$arr' + initialValue: [] + in: + collapsed: + $concatArrays: + - '$$value.collapsed' + - '$$this' + firstValues: + $concatArrays: + - '$$value.firstValues' + - + $slice: + - '$$this' + - 1 diff --git a/generator/config/expression/regexFind.yaml b/generator/config/expression/regexFind.yaml new file mode 100644 index 000000000..d953a4ae6 --- /dev/null +++ b/generator/config/expression/regexFind.yaml @@ -0,0 +1,66 @@ +# $schema: ../schema.json +name: $regexFind +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexFind/' +type: + - resolvesToObject +encode: object +description: | + Applies a regular expression (regex) to a string and returns information on the first matched substring. + New in MongoDB 4.2. +arguments: + - + name: input + type: + - resolvesToString + description: | + The string on which you wish to apply the regex pattern. Can be a string or any valid expression that resolves to a string. + - + name: regex + type: + - resolvesToString + - regex + description: | + The regex pattern to apply. Can be any valid expression that resolves to either a string or regex pattern //. When using the regex //, you can also specify the regex options i and m (but not the s or x options) + - + name: options + type: + - string + optional: true +tests: + - + name: '$regexFind and Its Options' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexFind/#-regexfind-and-its-options' + pipeline: + - + $addFields: + returnObject: + $regexFind: + input: '$description' + regex: !bson_regex 'line' + - + name: 'i Option' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexFind/#i-option' + pipeline: + - + $addFields: + returnObject: + # Specify i as part of the Regex type + $regexFind: + input: '$description' + regex: !bson_regex ['line', 'i'] + - + $addFields: + returnObject: + # Specify i in the options field + $regexFind: + input: '$description' + regex: 'line' + options: 'i' + - + $addFields: + returnObject: + # Mix Regex type with options field + $regexFind: + input: '$description' + regex: !bson_regex 'line' + options: 'i' diff --git a/generator/config/expression/regexFindAll.yaml b/generator/config/expression/regexFindAll.yaml new file mode 100644 index 000000000..6aea184d7 --- /dev/null +++ b/generator/config/expression/regexFindAll.yaml @@ -0,0 +1,99 @@ +# $schema: ../schema.json +name: $regexFindAll +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexFindAll/' +type: + - resolvesToArray # of object +encode: object +description: | + Applies a regular expression (regex) to a string and returns information on the all matched substrings. + New in MongoDB 4.2. +arguments: + - + name: input + type: + - resolvesToString + description: | + The string on which you wish to apply the regex pattern. Can be a string or any valid expression that resolves to a string. + - + name: regex + type: + - resolvesToString + - regex + description: | + The regex pattern to apply. Can be any valid expression that resolves to either a string or regex pattern //. When using the regex //, you can also specify the regex options i and m (but not the s or x options) + - + name: options + type: + - string + optional: true +tests: + - + name: '$regexFindAll and Its Options' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexFindAll/#-regexfindall-and-its-options' + pipeline: + - + $addFields: + returnObject: + $regexFindAll: + input: '$description' + regex: !bson_regex 'line' + - + name: 'i Option' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexFindAll/#i-option' + pipeline: + - + $addFields: + returnObject: + # Specify i as part of the regex type + $regexFindAll: + input: '$description' + regex: !bson_regex ['line', 'i'] + - + $addFields: + returnObject: + # Specify i in the options field + $regexFindAll: + input: '$description' + regex: 'line' + options: 'i' + - + $addFields: + returnObject: + # Mix Regex type with options field + $regexFindAll: + input: '$description' + regex: !bson_regex 'line' + options: 'i' + - + name: 'Use $regexFindAll to Parse Email from String' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexFindAll/#use--regexfindall-to-parse-email-from-string' + pipeline: + - + $addFields: + email: + $regexFindAll: + input: '$comment' + regex: !bson_regex ['[a-z0-9_.+-]+@[a-z0-9_.+-]+\.[a-z0-9_.+-]+', 'i'] + - + $set: + email: '$email.match' + - + name: 'Use Captured Groupings to Parse User Name' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexFindAll/#use-captured-groupings-to-parse-user-name' + pipeline: + - + $addFields: + names: + $regexFindAll: + input: '$comment' + regex: !bson_regex ['([a-z0-9_.+-]+)@[a-z0-9_.+-]+\.[a-z0-9_.+-]+', 'i'] + - + $set: + names: + $reduce: + input: '$names.captures' + initialValue: [] + in: + $concatArrays: + - '$$value' + - '$$this' diff --git a/generator/config/expression/regexMatch.yaml b/generator/config/expression/regexMatch.yaml new file mode 100644 index 000000000..4ea9f0aab --- /dev/null +++ b/generator/config/expression/regexMatch.yaml @@ -0,0 +1,80 @@ +# $schema: ../schema.json +name: $regexMatch +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexMatch/' +type: + - resolvesToBool +encode: object +description: | + Applies a regular expression (regex) to a string and returns a boolean that indicates if a match is found or not. + New in MongoDB 4.2. +arguments: + - + name: input + type: + - resolvesToString + description: | + The string on which you wish to apply the regex pattern. Can be a string or any valid expression that resolves to a string. + - + name: regex + type: + - resolvesToString + - regex + description: | + The regex pattern to apply. Can be any valid expression that resolves to either a string or regex pattern //. When using the regex //, you can also specify the regex options i and m (but not the s or x options) + - + name: options + type: + - string + optional: true +tests: + - + name: '$regexMatch and Its Options' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexMatch/#-regexmatch-and-its-options' + pipeline: + - + $addFields: + result: + $regexMatch: + input: '$description' + regex: !bson_regex 'line' + - + name: 'i Option' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexMatch/#i-option' + pipeline: + - + $addFields: + result: + # Specify i as part of the Regex type + $regexMatch: + input: '$description' + regex: !bson_regex ['line', 'i'] + - + $addFields: + result: + # Specify i in the options field + $regexMatch: + input: '$description' + regex: 'line' + options: 'i' + - + $addFields: + result: + # Mix Regex type with options field + $regexMatch: + input: '$description' + regex: !bson_regex 'line' + options: 'i' + - + name: 'Use $regexMatch to Check Email Address' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexMatch/#use--regexmatch-to-check-email-address' + pipeline: + - + $addFields: + category: + $cond: + if: + $regexMatch: + input: '$comment' + regex: !bson_regex ['[a-z0-9_.+-]+@mongodb.com', 'i'] + then: 'Employee' + else: 'External' diff --git a/generator/config/expression/replaceAll.yaml b/generator/config/expression/replaceAll.yaml new file mode 100644 index 000000000..74d479cb7 --- /dev/null +++ b/generator/config/expression/replaceAll.yaml @@ -0,0 +1,44 @@ +# $schema: ../schema.json +name: $replaceAll +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceAll/' +type: + - resolvesToString +encode: object +description: | + Replaces all instances of a search string in an input string with a replacement string. + $replaceAll is both case-sensitive and diacritic-sensitive, and ignores any collation present on a collection. + New in MongoDB 4.4. +arguments: + - + name: input + type: + - resolvesToString + - resolvesToNull + description: | + The string on which you wish to apply the find. Can be any valid expression that resolves to a string or a null. If input refers to a field that is missing, $replaceAll returns null. + - + name: find + type: + - resolvesToString + - resolvesToNull + description: | + The string to search for within the given input. Can be any valid expression that resolves to a string or a null. If find refers to a field that is missing, $replaceAll returns null. + - + name: replacement + type: + - resolvesToString + - resolvesToNull + description: | + The string to use to replace all matched instances of find in input. Can be any valid expression that resolves to a string or a null. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceAll/#example' + pipeline: + - + $project: + item: + $replaceAll: + input: '$item' + find: 'blue paint' + replacement: 'red paint' diff --git a/generator/config/expression/replaceOne.yaml b/generator/config/expression/replaceOne.yaml new file mode 100644 index 000000000..0962cc6c5 --- /dev/null +++ b/generator/config/expression/replaceOne.yaml @@ -0,0 +1,43 @@ +# $schema: ../schema.json +name: $replaceOne +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceOne/' +type: + - resolvesToString +encode: object +description: | + Replaces the first instance of a matched string in a given input. + New in MongoDB 4.4. +arguments: + - + name: input + type: + - resolvesToString + - resolvesToNull + description: | + The string on which you wish to apply the find. Can be any valid expression that resolves to a string or a null. If input refers to a field that is missing, $replaceAll returns null. + - + name: find + type: + - resolvesToString + - resolvesToNull + description: | + The string to search for within the given input. Can be any valid expression that resolves to a string or a null. If find refers to a field that is missing, $replaceAll returns null. + - + name: replacement + type: + - resolvesToString + - resolvesToNull + description: | + The string to use to replace all matched instances of find in input. Can be any valid expression that resolves to a string or a null. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceOne/#example' + pipeline: + - + $project: + item: + $replaceOne: + input: '$item' + find: 'blue paint' + replacement: 'red paint' diff --git a/generator/config/expression/reverseArray.yaml b/generator/config/expression/reverseArray.yaml new file mode 100644 index 000000000..2cbe3f3cd --- /dev/null +++ b/generator/config/expression/reverseArray.yaml @@ -0,0 +1,25 @@ +# $schema: ../schema.json +name: $reverseArray +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/reverseArray/' +type: + - resolvesToArray +encode: single +description: | + Returns an array with the elements in reverse order. +arguments: + - + name: expression + type: + - resolvesToArray + description: | + The argument can be any valid expression as long as it resolves to an array. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/reverseArray/#example' + pipeline: + - + $project: + name: 1 + reverseFavorites: + $reverseArray: '$favorites' diff --git a/generator/config/expression/round.yaml b/generator/config/expression/round.yaml new file mode 100644 index 000000000..52e6004a0 --- /dev/null +++ b/generator/config/expression/round.yaml @@ -0,0 +1,40 @@ +# $schema: ../schema.json +name: $round +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/round/' +type: + - resolvesToInt + - resolvesToDouble + - resolvesToDecimal + - resolvesToLong +encode: array +description: | + Rounds a number to to a whole integer or to a specified decimal place. +arguments: + - + name: number + type: + - resolvesToInt + - resolvesToDouble + - resolvesToDecimal + - resolvesToLong + description: | + Can be any valid expression that resolves to a number. Specifically, the expression must resolve to an integer, double, decimal, or long. + $round returns an error if the expression resolves to a non-numeric data type. + - + name: place + type: + - resolvesToInt + optional: true + description: | + Can be any valid expression that resolves to an integer between -20 and 100, exclusive. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/round/#example' + pipeline: + - + $project: + roundedValue: + $round: + - '$value' + - 1 diff --git a/generator/config/expression/rtrim.yaml b/generator/config/expression/rtrim.yaml new file mode 100644 index 000000000..a9d1974db --- /dev/null +++ b/generator/config/expression/rtrim.yaml @@ -0,0 +1,35 @@ +# $schema: ../schema.json +name: $rtrim +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/rtrim/' +type: + - resolvesToString +encode: object +description: | + Removes whitespace characters, including null, or the specified characters from the end of a string. +arguments: + - + name: input + type: + - resolvesToString + description: | + The string to trim. The argument can be any valid expression that resolves to a string. + - + name: chars + type: + - resolvesToString + optional: true + description: | + The character(s) to trim from the beginning of the input. + The argument can be any valid expression that resolves to a string. The $ltrim operator breaks down the string into individual UTF code point to trim from input. + If unspecified, $ltrim removes whitespace characters, including the null character. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/rtrim/#example' + pipeline: + - + $project: + item: 1 + description: + $rtrim: + input: '$description' diff --git a/generator/config/expression/second.yaml b/generator/config/expression/second.yaml new file mode 100644 index 000000000..83e7fd370 --- /dev/null +++ b/generator/config/expression/second.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $second +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/second/' +type: + - resolvesToInt +encode: object +description: | + Returns the seconds for a date as a number between 0 and 60 (leap seconds). +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/second/#example' + pipeline: + - + $project: + seconds: + # Example uses the short form, the builder always generates the verbose form + # $second: '$date' + $second: + date: '$date' diff --git a/generator/config/expression/setDifference.yaml b/generator/config/expression/setDifference.yaml new file mode 100644 index 000000000..69f448cb7 --- /dev/null +++ b/generator/config/expression/setDifference.yaml @@ -0,0 +1,35 @@ +# $schema: ../schema.json +name: $setDifference +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setDifference/' +type: + - resolvesToArray +encode: array +description: | + Returns a set with elements that appear in the first set but not in the second set; i.e. performs a relative complement of the second set relative to the first. Accepts exactly two argument expressions. +arguments: + - + name: expression1 + type: + - resolvesToArray + description: | + The arguments can be any valid expression as long as they each resolve to an array. + - + name: expression2 + type: + - resolvesToArray + description: | + The arguments can be any valid expression as long as they each resolve to an array. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setDifference/#example' + pipeline: + - + $project: + flowerFieldA: 1 + flowerFieldB: 1 + inBOnly: + $setDifference: + - '$flowerFieldB' + - '$flowerFieldA' + _id: 0 diff --git a/generator/config/expression/setEquals.yaml b/generator/config/expression/setEquals.yaml new file mode 100644 index 000000000..5194c9ae9 --- /dev/null +++ b/generator/config/expression/setEquals.yaml @@ -0,0 +1,28 @@ +# $schema: ../schema.json +name: $setEquals +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setEquals/' +type: + - resolvesToBool +encode: single +description: | + Returns true if the input sets have the same distinct elements. Accepts two or more argument expressions. +arguments: + - + name: expression + type: + - resolvesToArray + variadic: array +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setEquals/#example' + pipeline: + - + $project: + _id: 0 + cakes: 1 + cupcakes: 1 + sameFlavors: + $setEquals: + - '$cakes' + - '$cupcakes' diff --git a/generator/config/expression/setField.yaml b/generator/config/expression/setField.yaml new file mode 100644 index 000000000..b53cf6584 --- /dev/null +++ b/generator/config/expression/setField.yaml @@ -0,0 +1,109 @@ +# $schema: ../schema.json +name: $setField +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setField/' +type: + - resolvesToObject +encode: object +description: | + Adds, updates, or removes a specified field in a document. You can use $setField to add, update, or remove fields with names that contain periods (.) or start with dollar signs ($). + New in MongoDB 5.0. +arguments: + - + name: field + type: + - resolvesToString + description: | + Field in the input object that you want to add, update, or remove. field can be any valid expression that resolves to a string constant. + - + name: input + type: + - resolvesToObject + description: | + Document that contains the field that you want to add or update. input must resolve to an object, missing, null, or undefined. + - + name: value + type: + - expression + description: | + The value that you want to assign to field. value can be any valid expression. + Set to $$REMOVE to remove field from the input document. +tests: + - + name: 'Add Fields that Contain Periods' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setField/#add-fields-that-contain-periods--.-' + pipeline: + - + $replaceWith: + $setField: + field: 'price.usd' + input: '$$ROOT' + value: '$price' + - + # Example uses the short form, the builder always generates the verbose form + # $unset: 'price' + $unset: + - 'price' + - + name: 'Add Fields that Start with a Dollar Sign' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setField/#add-fields-that-start-with-a-dollar-sign----' + pipeline: + - + $replaceWith: + $setField: + field: + $literal: '$price' + input: '$$ROOT' + value: '$price' + - + # Example uses the short form, the builder always generates the verbose form + # $unset: 'price' + $unset: + - 'price' + - + name: 'Update Fields that Contain Periods' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setField/#update-fields-that-contain-periods--.-' + pipeline: + - + $match: + _id: 1 + - + $replaceWith: + $setField: + field: 'price.usd' + input: '$$ROOT' + value: 49.99 + - + name: 'Update Fields that Start with a Dollar Sign' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setField/#update-fields-that-start-with-a-dollar-sign----' + pipeline: + - + $match: + _id: 1 + - + $replaceWith: + $setField: + field: + $literal: '$price' + input: '$$ROOT' + value: 49.99 + - + name: 'Remove Fields that Contain Periods' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setField/#remove-fields-that-contain-periods--.-' + pipeline: + - + $replaceWith: + $setField: + field: 'price.usd' + input: '$$ROOT' + value: '$$REMOVE' + - + name: 'Remove Fields that Start with a Dollar Sign' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setField/#remove-fields-that-start-with-a-dollar-sign----' + pipeline: + - + $replaceWith: + $setField: + field: + $literal: '$price' + input: '$$ROOT' + value: '$$REMOVE' diff --git a/generator/config/expression/setIntersection.yaml b/generator/config/expression/setIntersection.yaml new file mode 100644 index 000000000..8f03651d5 --- /dev/null +++ b/generator/config/expression/setIntersection.yaml @@ -0,0 +1,44 @@ +# $schema: ../schema.json +name: $setIntersection +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setIntersection/' +type: + - resolvesToArray +encode: single +description: | + Returns a set with elements that appear in all of the input sets. Accepts any number of argument expressions. +arguments: + - + name: expression + type: + - resolvesToArray + variadic: array +tests: + - + name: 'Elements Array Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setIntersection/#elements-array-example' + pipeline: + - + $project: + flowerFieldA: 1 + flowerFieldB: 1 + commonToBoth: + $setIntersection: + - '$flowerFieldA' + - '$flowerFieldB' + _id: 0 + - + name: 'Retrieve Documents for Roles Granted to the Current User' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setIntersection/#retrieve-documents-for-roles-granted-to-the-current-user' + pipeline: + - + $match: + $expr: + $not: + # the example doesn't use an array inside $not, but the documentation say it is necessary + - + $eq: + - + $setIntersection: + - '$allowedRoles' + - '$$USER_ROLES.role' + - [] diff --git a/generator/config/expression/setIsSubset.yaml b/generator/config/expression/setIsSubset.yaml new file mode 100644 index 000000000..fe7c9ed02 --- /dev/null +++ b/generator/config/expression/setIsSubset.yaml @@ -0,0 +1,32 @@ +# $schema: ../schema.json +name: $setIsSubset +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setIsSubset/' +type: + - resolvesToBool +encode: array +description: | + Returns true if all elements of the first set appear in the second set, including when the first set equals the second set; i.e. not a strict subset. Accepts exactly two argument expressions. +arguments: + - + name: expression1 + type: + - resolvesToArray + - + name: expression2 + type: + - resolvesToArray +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setIsSubset/#example' + pipeline: + - + $project: + flowerFieldA: 1 + flowerFieldB: 1 + AisSubset: + $setIsSubset: + - '$flowerFieldA' + - '$flowerFieldB' + _id: 0 + diff --git a/generator/config/expression/setUnion.yaml b/generator/config/expression/setUnion.yaml new file mode 100644 index 000000000..2cfca3e16 --- /dev/null +++ b/generator/config/expression/setUnion.yaml @@ -0,0 +1,28 @@ +# $schema: ../schema.json +name: $setUnion +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setUnion/' +type: + - resolvesToArray +encode: single +description: | + Returns a set with elements that appear in any of the input sets. +arguments: + - + name: expression + type: + - resolvesToArray + variadic: array +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setUnion/#example' + pipeline: + - + $project: + flowerFieldA: 1 + flowerFieldB: 1 + allValues: + $setUnion: + - '$flowerFieldA' + - '$flowerFieldB' + _id: 0 diff --git a/generator/config/expression/sin.yaml b/generator/config/expression/sin.yaml new file mode 100644 index 000000000..fe02b4f28 --- /dev/null +++ b/generator/config/expression/sin.yaml @@ -0,0 +1,30 @@ +# $schema: ../schema.json +name: $sin +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sin/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the sine of a value that is measured in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $sin takes any valid expression that resolves to a number. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the result to radians. + By default $sin returns values as a double. $sin can also return values as a 128-bit decimal as long as the expression resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sin/#example' + pipeline: + - + $addFields: + side_b: + $multiply: + - + $sin: + $degreesToRadians: '$angle_a' + - '$hypotenuse' diff --git a/generator/config/expression/sinh.yaml b/generator/config/expression/sinh.yaml new file mode 100644 index 000000000..a5b446add --- /dev/null +++ b/generator/config/expression/sinh.yaml @@ -0,0 +1,27 @@ +# $schema: ../schema.json +name: $sinh +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sinh/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the hyperbolic sine of a value that is measured in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $sinh takes any valid expression that resolves to a number, measured in radians. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the value to radians. + By default $sinh returns values as a double. $sinh can also return values as a 128-bit decimal if the expression resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sinh/#example' + pipeline: + - + $addFields: + sinh_output: + $sinh: + $degreesToRadians: '$angle' diff --git a/generator/config/expression/size.yaml b/generator/config/expression/size.yaml new file mode 100644 index 000000000..ce4fe775e --- /dev/null +++ b/generator/config/expression/size.yaml @@ -0,0 +1,33 @@ +# $schema: ../schema.json +name: $size +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/size/' +type: + - resolvesToInt +encode: single +description: | + Returns the number of elements in the array. Accepts a single expression as argument. +arguments: + - + name: expression + type: + - resolvesToArray + description: | + The argument for $size can be any expression as long as it resolves to an array. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/size/#example' + pipeline: + - + $project: + item: 1 + numberOfColors: + $cond: + if: + # Example uses the short form, the builder always generates the verbose form + # $isArray: '$colors' + $isArray: + - '$colors' + then: + $size: '$colors' + else: 'NA' diff --git a/generator/config/expression/slice.yaml b/generator/config/expression/slice.yaml new file mode 100644 index 000000000..22cc287e1 --- /dev/null +++ b/generator/config/expression/slice.yaml @@ -0,0 +1,44 @@ +# $schema: ../schema.json +name: $slice +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/slice/' +type: + - resolvesToArray +encode: array +description: | + Returns a subset of an array. +arguments: + - + name: expression + type: + - resolvesToArray + description: | + Any valid expression as long as it resolves to an array. + - + name: position + type: + - resolvesToInt + optional: true + description: | + Any valid expression as long as it resolves to an integer. + If positive, $slice determines the starting position from the start of the array. If position is greater than the number of elements, the $slice returns an empty array. + If negative, $slice determines the starting position from the end of the array. If the absolute value of the is greater than the number of elements, the starting position is the start of the array. + - + name: "n" + type: + - resolvesToInt + description: | + Any valid expression as long as it resolves to an integer. If position is specified, n must resolve to a positive integer. + If positive, $slice returns up to the first n elements in the array. If the position is specified, $slice returns the first n elements starting from the position. + If negative, $slice returns up to the last n elements in the array. n cannot resolve to a negative number if is specified. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/slice/#example' + pipeline: + - + $project: + name: 1 + threeFavorites: + $slice: + - '$favorites' + - 3 diff --git a/generator/config/expression/sortArray.yaml b/generator/config/expression/sortArray.yaml new file mode 100644 index 000000000..962ebf1ce --- /dev/null +++ b/generator/config/expression/sortArray.yaml @@ -0,0 +1,108 @@ +# $schema: ../schema.json +name: $sortArray +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/' +type: + - resolvesToArray +encode: object +description: | + Sorts the elements of an array. +arguments: + - + name: input + type: + - resolvesToArray + description: | + The array to be sorted. + The result is null if the expression: is missing, evaluates to null, or evaluates to undefined + If the expression evaluates to any other non-array value, the document returns an error. + - + name: sortBy + type: + - object # SortSpec + - int + - sortSpec + description: | + The document specifies a sort ordering. +tests: + - + name: 'Sort on a Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/#sort-on-a-field' + pipeline: + - + $project: + _id: 0 + result: + $sortArray: + input: '$team' + sortBy: + name: 1 + - + name: 'Sort on a Subfield' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/#sort-on-a-subfield' + pipeline: + - + $project: + _id: 0 + result: + $sortArray: + input: '$team' + sortBy: + address.city: -1 + - + name: 'Sort on Multiple Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/#sort-on-multiple-fields' + pipeline: + - + $project: + _id: 0 + result: + $sortArray: + input: '$team' + sortBy: + age: -1 + name: 1 + - + name: 'Sort an Array of Integers' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/#sort-an-array-of-integers' + pipeline: + - + $project: + _id: 0 + result: + $sortArray: + input: + - 1 + - 4 + - 1 + - 6 + - 12 + - 5 + sortBy: 1 + - + name: 'Sort on Mixed Type Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/#sort-on-mixed-type-fields' + pipeline: + - + $project: + _id: 0 + result: + $sortArray: + input: + - 20 + - 4 + - + a: 'Free' + - 6 + - 21 + - 5 + - 'Gratis' + - + a: ~ + - + a: + sale: true + price: 19 + - !bson_decimal128 '10.23' + - + a: 'On sale' + sortBy: 1 diff --git a/generator/config/expression/split.yaml b/generator/config/expression/split.yaml new file mode 100644 index 000000000..1c6169910 --- /dev/null +++ b/generator/config/expression/split.yaml @@ -0,0 +1,48 @@ +# $schema: ../schema.json +name: $split +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/split/' +type: + - resolvesToArray # of string +encode: array +description: | + Splits a string into substrings based on a delimiter. Returns an array of substrings. If the delimiter is not found within the string, returns an array containing the original string. +arguments: + - + name: string + type: + - resolvesToString + description: | + The string to be split. string expression can be any valid expression as long as it resolves to a string. + - + name: delimiter + type: + - resolvesToString + description: | + The delimiter to use when splitting the string expression. delimiter can be any valid expression as long as it resolves to a string. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/split/#example' + pipeline: + - + $project: + city_state: + $split: + - '$city' + - ', ' + qty: 1 + - + $unwind: + path: '$city_state' + - + $match: + city_state: !bson_regex '[A-Z]{2}' + - + $group: + _id: + state: '$city_state' + total_qty: + $sum: '$qty' + - + $sort: + total_qty: -1 diff --git a/generator/config/expression/sqrt.yaml b/generator/config/expression/sqrt.yaml new file mode 100644 index 000000000..52f5bb7c2 --- /dev/null +++ b/generator/config/expression/sqrt.yaml @@ -0,0 +1,39 @@ +# $schema: ../schema.json +name: $sqrt +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sqrt/' +type: + - resolvesToDouble +encode: single +description: | + Calculates the square root. +arguments: + - + name: number + type: + - resolvesToNumber + description: | + The argument can be any valid expression as long as it resolves to a non-negative number. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sqrt/#example' + pipeline: + - + $project: + distance: + $sqrt: + $add: + - + $pow: + - + $subtract: + - '$p2.y' + - '$p1.y' + - 2 + - + $pow: + - + $subtract: + - '$p2.x' + - '$p1.x' + - 2 diff --git a/generator/config/expression/stdDevPop.yaml b/generator/config/expression/stdDevPop.yaml new file mode 100644 index 000000000..46641ebe8 --- /dev/null +++ b/generator/config/expression/stdDevPop.yaml @@ -0,0 +1,27 @@ +# $schema: ../schema.json +name: $stdDevPop +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/stdDevPop/' +type: + - resolvesToDouble +encode: single +description: | + Calculates the population standard deviation of the input values. Use if the values encompass the entire population of data you want to represent and do not wish to generalize about a larger population. $stdDevPop ignores non-numeric values. + If the values represent only a sample of a population of data from which to generalize about the population, use $stdDevSamp instead. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - resolvesToNumber + variadic: array +tests: + - + name: 'Use in $project Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/stdDevPop/#use-in--project-stage' + pipeline: + - + $project: + stdDev: + # Example uses the short form, the builder always generates the verbose form + # $stdDevPop: '$scores.score' + $stdDevPop: ['$scores.score'] diff --git a/generator/config/expression/stdDevSamp.yaml b/generator/config/expression/stdDevSamp.yaml new file mode 100644 index 000000000..84b35f52a --- /dev/null +++ b/generator/config/expression/stdDevSamp.yaml @@ -0,0 +1,15 @@ +# $schema: ../schema.json +name: $stdDevSamp +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/stdDevSamp/' +type: + - resolvesToDouble +encode: single +description: | + Calculates the sample standard deviation of the input values. Use if the values encompass a sample of a population of data from which to generalize about the population. $stdDevSamp ignores non-numeric values. + If the values represent the entire population of data or you do not wish to generalize about a larger population, use $stdDevPop instead. +arguments: + - + name: expression + type: + - resolvesToNumber + variadic: array diff --git a/generator/config/expression/strLenBytes.yaml b/generator/config/expression/strLenBytes.yaml new file mode 100644 index 000000000..301150d19 --- /dev/null +++ b/generator/config/expression/strLenBytes.yaml @@ -0,0 +1,23 @@ +# $schema: ../schema.json +name: $strLenBytes +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenBytes/' +type: + - resolvesToInt +encode: single +description: | + Returns the number of UTF-8 encoded bytes in a string. +arguments: + - + name: expression + type: + - resolvesToString +tests: + - + name: 'Single-Byte and Multibyte Character Set' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenBytes/#single-byte-and-multibyte-character-set' + pipeline: + - + $project: + name: 1 + length: + $strLenBytes: '$name' diff --git a/generator/config/expression/strLenCP.yaml b/generator/config/expression/strLenCP.yaml new file mode 100644 index 000000000..b852c80ec --- /dev/null +++ b/generator/config/expression/strLenCP.yaml @@ -0,0 +1,23 @@ +# $schema: ../schema.json +name: $strLenCP +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenCP/' +type: + - resolvesToInt +encode: single +description: | + Returns the number of UTF-8 code points in a string. +arguments: + - + name: expression + type: + - resolvesToString +tests: + - + name: 'Single-Byte and Multibyte Character Set' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenBytes/#single-byte-and-multibyte-character-set' + pipeline: + - + $project: + name: 1 + length: + $strLenCP: '$name' diff --git a/generator/config/expression/strcasecmp.yaml b/generator/config/expression/strcasecmp.yaml new file mode 100644 index 000000000..6775c44b1 --- /dev/null +++ b/generator/config/expression/strcasecmp.yaml @@ -0,0 +1,29 @@ +# $schema: ../schema.json +name: $strcasecmp +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/strcasecmp/' +type: + - resolvesToInt +encode: array +description: | + Performs case-insensitive string comparison and returns: 0 if two strings are equivalent, 1 if the first string is greater than the second, and -1 if the first string is less than the second. +arguments: + - + name: expression1 + type: + - resolvesToString + - + name: expression2 + type: + - resolvesToString +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/strcasecmp/#example' + pipeline: + - + $project: + item: 1 + comparisonResult: + $strcasecmp: + - '$quarter' + - '13q4' diff --git a/generator/config/expression/substr.yaml b/generator/config/expression/substr.yaml new file mode 100644 index 000000000..6bcf6143f --- /dev/null +++ b/generator/config/expression/substr.yaml @@ -0,0 +1,43 @@ +# $schema: ../schema.json +name: $substr +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/substr/' +type: + - resolvesToString +encode: array +description: | + Deprecated. Use $substrBytes or $substrCP. +arguments: + - + name: string + type: + - resolvesToString + - + name: start + type: + - resolvesToInt + description: | + If start is a negative number, $substr returns an empty string "". + - + name: length + type: + - resolvesToInt + description: | + If length is a negative number, $substr returns a substring that starts at the specified index and includes the rest of the string. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/substr/#example' + pipeline: + - + $project: + item: 1 + yearSubstring: + $substr: + - '$quarter' + - 0 + - 2 + quarterSubtring: + $substr: + - '$quarter' + - 2 + - -1 diff --git a/generator/config/expression/substrBytes.yaml b/generator/config/expression/substrBytes.yaml new file mode 100644 index 000000000..fed7677c8 --- /dev/null +++ b/generator/config/expression/substrBytes.yaml @@ -0,0 +1,59 @@ +# $schema: ../schema.json +name: $substrBytes +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrBytes/' +type: + - resolvesToString +encode: array +description: | + Returns the substring of a string. Starts with the character at the specified UTF-8 byte index (zero-based) in the string and continues for the specified number of bytes. +arguments: + - + name: string + type: + - resolvesToString + - + name: start + type: + - resolvesToInt + description: | + If start is a negative number, $substr returns an empty string "". + - + name: length + type: + - resolvesToInt + description: | + If length is a negative number, $substr returns a substring that starts at the specified index and includes the rest of the string. +tests: + - + name: 'Single-Byte Character Set' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrBytes/#single-byte-character-set' + pipeline: + - + $project: + item: 1 + yearSubstring: + $substrBytes: + - '$quarter' + - 0 + - 2 + quarterSubtring: + $substrBytes: + - '$quarter' + - 2 + - + $subtract: + - + $strLenBytes: '$quarter' + - 2 + - + name: 'Single-Byte and Multibyte Character Set' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrBytes/#single-byte-and-multibyte-character-set' + pipeline: + - + $project: + name: 1 + menuCode: + $substrBytes: + - '$name' + - 0 + - 3 diff --git a/generator/config/expression/substrCP.yaml b/generator/config/expression/substrCP.yaml new file mode 100644 index 000000000..843b2ca1a --- /dev/null +++ b/generator/config/expression/substrCP.yaml @@ -0,0 +1,59 @@ +# $schema: ../schema.json +name: $substrCP +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrCP/' +type: + - resolvesToString +encode: array +description: | + Returns the substring of a string. Starts with the character at the specified UTF-8 code point (CP) index (zero-based) in the string and continues for the number of code points specified. +arguments: + - + name: string + type: + - resolvesToString + - + name: start + type: + - resolvesToInt + description: | + If start is a negative number, $substr returns an empty string "". + - + name: length + type: + - resolvesToInt + description: | + If length is a negative number, $substr returns a substring that starts at the specified index and includes the rest of the string. +tests: + - + name: 'Single-Byte Character Set' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrCP/#single-byte-character-set' + pipeline: + - + $project: + item: 1 + yearSubstring: + $substrCP: + - '$quarter' + - 0 + - 2 + quarterSubtring: + $substrCP: + - '$quarter' + - 2 + - + $subtract: + - + $strLenCP: '$quarter' + - 2 + - + name: 'Single-Byte and Multibyte Character Set' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrCP/#single-byte-and-multibyte-character-set' + pipeline: + - + $project: + name: 1 + menuCode: + $substrCP: + - '$name' + - 0 + - 3 diff --git a/generator/config/expression/subtract.yaml b/generator/config/expression/subtract.yaml new file mode 100644 index 000000000..b6db65ac9 --- /dev/null +++ b/generator/config/expression/subtract.yaml @@ -0,0 +1,60 @@ +# $schema: ../schema.json +name: $subtract +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/subtract/' +type: + - resolvesToInt + - resolvesToLong + - resolvesToDouble + - resolvesToDecimal + - resolvesToDate +encode: array +description: | + Returns the result of subtracting the second value from the first. If the two values are numbers, return the difference. If the two values are dates, return the difference in milliseconds. If the two values are a date and a number in milliseconds, return the resulting date. Accepts two argument expressions. If the two values are a date and a number, specify the date argument first as it is not meaningful to subtract a date from a number. +arguments: + - + name: expression1 + type: + - resolvesToNumber + - resolvesToDate + - + name: expression2 + type: + - resolvesToNumber + - resolvesToDate +tests: + - + name: 'Subtract Numbers' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/subtract/#subtract-numbers' + pipeline: + - + $project: + item: 1 + total: + $subtract: + - + $add: + - '$price' + - '$fee' + - '$discount' + - + name: 'Subtract Two Dates' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/subtract/#subtract-two-dates' + pipeline: + - + $project: + item: 1 + dateDifference: + $subtract: + - '$$NOW' + - '$date' + - + name: 'Subtract Milliseconds from a Date' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/subtract/#subtract-milliseconds-from-a-date' + pipeline: + - + $project: + item: 1 + dateDifference: + $subtract: + - '$date' + - 300000 diff --git a/generator/config/expression/sum.yaml b/generator/config/expression/sum.yaml new file mode 100644 index 000000000..25b323ab1 --- /dev/null +++ b/generator/config/expression/sum.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $sum +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sum/' +type: + - resolvesToNumber +encode: single +description: | + Returns a sum of numerical values. Ignores non-numeric values. + Changed in MongoDB 5.0: Available in the $setWindowFields stage. +arguments: + - + name: expression + type: + - resolvesToNumber + - resolvesToArray + variadic: array +tests: + - + name: 'Use in $project Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sum/#use-in--project-stage' + pipeline: + - + $project: + quizTotal: + # Example uses the short form, the builder always generates the verbose form + # $sum: '$quizzes' + $sum: + - '$quizzes' + labTotal: + # $sum: '$labs' + $sum: + - '$labs' + examTotal: + $sum: + - '$final' + - '$midterm' diff --git a/generator/config/expression/switch.yaml b/generator/config/expression/switch.yaml new file mode 100644 index 000000000..d668e3d94 --- /dev/null +++ b/generator/config/expression/switch.yaml @@ -0,0 +1,70 @@ +# $schema: ../schema.json +name: $switch +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/switch/' +type: + - resolvesToAny +encode: object +description: | + Evaluates a series of case expressions. When it finds an expression which evaluates to true, $switch executes a specified expression and breaks out of the control flow. +arguments: + - + name: branches + type: + - array # of CaseOperator + description: | + An array of control branch documents. Each branch is a document with the following fields: + - case Can be any valid expression that resolves to a boolean. If the result is not a boolean, it is coerced to a boolean value. More information about how MongoDB evaluates expressions as either true or false can be found here. + - then Can be any valid expression. + The branches array must contain at least one branch document. + - + name: default + type: + - expression + optional: true + description: | + The path to take if no branch case expression evaluates to true. + Although optional, if default is unspecified and no branch case evaluates to true, $switch returns an error. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/switch/#example' + pipeline: + - + $project: + name: 1 + summary: + $switch: + branches: + - + case: + $gte: + - + #$avg: '$scores' + $avg: [ '$scores' ] + - 90 + then: 'Doing great!' + - + case: + $and: + - + $gte: + - + #$avg: '$scores' + $avg: [ '$scores' ] + - 80 + - + $lt: + - + #$avg: '$scores' + $avg: [ '$scores' ] + - 90 + then: 'Doing pretty well.' + - + case: + $lt: + - + #$avg: '$scores' + $avg: [ '$scores' ] + - 80 + then: 'Needs improvement.' + default: 'No scores found.' diff --git a/generator/config/expression/tan.yaml b/generator/config/expression/tan.yaml new file mode 100644 index 000000000..17b11ee63 --- /dev/null +++ b/generator/config/expression/tan.yaml @@ -0,0 +1,30 @@ +# $schema: ../schema.json +name: $tan +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/tan/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the tangent of a value that is measured in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $tan takes any valid expression that resolves to a number. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the result to radians. + By default $tan returns values as a double. $tan can also return values as a 128-bit decimal as long as the expression resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/tan/#example' + pipeline: + - + $addFields: + side_b: + $multiply: + - + $tan: + $degreesToRadians: '$angle_a' + - '$side_a' diff --git a/generator/config/expression/tanh.yaml b/generator/config/expression/tanh.yaml new file mode 100644 index 000000000..364589452 --- /dev/null +++ b/generator/config/expression/tanh.yaml @@ -0,0 +1,27 @@ +# $schema: ../schema.json +name: $tanh +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/tanh/' +type: + - resolvesToDouble + - resolvesToDecimal +encode: single +description: | + Returns the hyperbolic tangent of a value that is measured in radians. +arguments: + - + name: expression + type: + - resolvesToNumber + description: | + $tanh takes any valid expression that resolves to a number, measured in radians. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the value to radians. + By default $tanh returns values as a double. $tanh can also return values as a 128-bit decimal if the expression resolves to a 128-bit decimal value. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/tanh/#example' + pipeline: + - + $addFields: + tanh_output: + $tanh: + $degreesToRadians: '$angle' diff --git a/generator/config/expression/toBool.yaml b/generator/config/expression/toBool.yaml new file mode 100644 index 000000000..7f771ec8d --- /dev/null +++ b/generator/config/expression/toBool.yaml @@ -0,0 +1,41 @@ +# $schema: ../schema.json +name: $toBool +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toBool/' +type: + - resolvesToBool +encode: single +description: | + Converts value to a boolean. + New in MongoDB 4.0. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toBool/#example' + pipeline: + - + $addFields: + convertedShippedFlag: + $switch: + branches: + - + case: + $eq: + - '$shipped' + - 'false' + then: false + - + case: + $eq: + - '$shipped' + - '' + then: false + default: + $toBool: '$shipped' + - + $match: + convertedShippedFlag: false diff --git a/generator/config/expression/toDate.yaml b/generator/config/expression/toDate.yaml new file mode 100644 index 000000000..d9434a6bd --- /dev/null +++ b/generator/config/expression/toDate.yaml @@ -0,0 +1,26 @@ +# $schema: ../schema.json +name: $toDate +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDate/' +type: + - resolvesToDate +encode: single +description: | + Converts value to a Date. + New in MongoDB 4.0. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDate/#example' + pipeline: + - + $addFields: + convertedDate: + $toDate: '$order_date' + - + $sort: + convertedDate: 1 diff --git a/generator/config/expression/toDecimal.yaml b/generator/config/expression/toDecimal.yaml new file mode 100644 index 000000000..2f3588323 --- /dev/null +++ b/generator/config/expression/toDecimal.yaml @@ -0,0 +1,23 @@ +# $schema: ../schema.json +name: $toDecimal +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDecimal/' +type: + - resolvesToDecimal +encode: single +description: | + Converts value to a Decimal128. + New in MongoDB 4.0. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDecimal/#example' + pipeline: + - + $addFields: + convertedPrice: + $toDecimal: '$price' diff --git a/generator/config/expression/toDouble.yaml b/generator/config/expression/toDouble.yaml new file mode 100644 index 000000000..f34c36e9a --- /dev/null +++ b/generator/config/expression/toDouble.yaml @@ -0,0 +1,27 @@ +# $schema: ../schema.json +name: $toDouble +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDouble/' +type: + - resolvesToDouble +encode: single +description: | + Converts value to a double. + New in MongoDB 4.0. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDouble/#example' + pipeline: + - + $addFields: + degrees: + $toDouble: + $substrBytes: + - '$temp' + - 0 + - 4 diff --git a/generator/config/expression/toHashedIndexKey.yaml b/generator/config/expression/toHashedIndexKey.yaml new file mode 100644 index 000000000..f5811f56d --- /dev/null +++ b/generator/config/expression/toHashedIndexKey.yaml @@ -0,0 +1,28 @@ +# $schema: ../schema.json +name: $toHashedIndexKey +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toHashedIndexKey/' +type: + - resolvesToLong +encode: single +description: | + Computes and returns the hash value of the input expression using the same hash function that MongoDB uses to create a hashed index. A hash function maps a key or string to a fixed-size numeric value. +arguments: + - + name: value + type: + - expression + description: | + key or string to hash +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toHashedIndexKey/#example' + pipeline: + - + $documents: + - + val: 'string to hash' + - + $addFields: + hashedVal: + $toHashedIndexKey: '$val' diff --git a/generator/config/expression/toInt.yaml b/generator/config/expression/toInt.yaml new file mode 100644 index 000000000..2b0239955 --- /dev/null +++ b/generator/config/expression/toInt.yaml @@ -0,0 +1,23 @@ +# $schema: ../schema.json +name: $toInt +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/' +type: + - resolvesToInt +encode: single +description: | + Converts value to an integer. + New in MongoDB 4.0. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/#example' + pipeline: + - + $addFields: + convertedQty: + $toInt: '$qty' diff --git a/generator/config/expression/toLong.yaml b/generator/config/expression/toLong.yaml new file mode 100644 index 000000000..3168ad9ff --- /dev/null +++ b/generator/config/expression/toLong.yaml @@ -0,0 +1,26 @@ +# $schema: ../schema.json +name: $toLong +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLong/' +type: + - resolvesToLong +encode: single +description: | + Converts value to a long. + New in MongoDB 4.0. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLong/#example' + pipeline: + - + $addFields: + convertedQty: + $toLong: '$qty' + - + $sort: + convertedQty: -1 diff --git a/generator/config/expression/toLower.yaml b/generator/config/expression/toLower.yaml new file mode 100644 index 000000000..0d6176672 --- /dev/null +++ b/generator/config/expression/toLower.yaml @@ -0,0 +1,24 @@ +# $schema: ../schema.json +name: $toLower +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLower/' +type: + - resolvesToString +encode: single +description: | + Converts a string to lowercase. Accepts a single argument expression. +arguments: + - + name: expression + type: + - resolvesToString +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLower/#example' + pipeline: + - + $project: + item: + $toLower: '$item' + description: + $toLower: '$description' diff --git a/generator/config/expression/toObjectId.yaml b/generator/config/expression/toObjectId.yaml new file mode 100644 index 000000000..803f7cafa --- /dev/null +++ b/generator/config/expression/toObjectId.yaml @@ -0,0 +1,26 @@ +# $schema: ../schema.json +name: $toObjectId +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toObjectId/' +type: + - resolvesToObjectId +encode: single +description: | + Converts value to an ObjectId. + New in MongoDB 4.0. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toObjectId/#example' + pipeline: + - + $addFields: + convertedId: + $toObjectId: '$_id' + - + $sort: + convertedId: -1 diff --git a/generator/config/expression/toString.yaml b/generator/config/expression/toString.yaml new file mode 100644 index 000000000..0fd068562 --- /dev/null +++ b/generator/config/expression/toString.yaml @@ -0,0 +1,26 @@ +# $schema: ../schema.json +name: $toString +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toString/' +type: + - resolvesToString +encode: single +description: | + Converts value to a string. + New in MongoDB 4.0. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toString/#example' + pipeline: + - + $addFields: + convertedZipCode: + $toString: '$zipcode' + - + $sort: + convertedZipCode: 1 diff --git a/generator/config/expression/toUpper.yaml b/generator/config/expression/toUpper.yaml new file mode 100644 index 000000000..c2c71e1bc --- /dev/null +++ b/generator/config/expression/toUpper.yaml @@ -0,0 +1,24 @@ +# $schema: ../schema.json +name: $toUpper +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toUpper/' +type: + - resolvesToString +encode: single +description: | + Converts a string to uppercase. Accepts a single argument expression. +arguments: + - + name: expression + type: + - resolvesToString +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/toUpper/#example' + pipeline: + - + $project: + item: + $toUpper: '$item' + description: + $toUpper: '$description' diff --git a/generator/config/expression/trim.yaml b/generator/config/expression/trim.yaml new file mode 100644 index 000000000..d63423910 --- /dev/null +++ b/generator/config/expression/trim.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $trim +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/trim/' +type: + - resolvesToString +encode: object +description: | + Removes whitespace or the specified characters from the beginning and end of a string. + New in MongoDB 4.0. +arguments: + - + name: input + type: + - resolvesToString + description: | + The string to trim. The argument can be any valid expression that resolves to a string. + - + name: chars + type: + - resolvesToString + optional: true + description: | + The character(s) to trim from the beginning of the input. + The argument can be any valid expression that resolves to a string. The $ltrim operator breaks down the string into individual UTF code point to trim from input. + If unspecified, $ltrim removes whitespace characters, including the null character. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/trim/#example' + pipeline: + - + $project: + item: 1 + description: + $trim: + input: '$description' diff --git a/generator/config/expression/trunc.yaml b/generator/config/expression/trunc.yaml new file mode 100644 index 000000000..f930cf027 --- /dev/null +++ b/generator/config/expression/trunc.yaml @@ -0,0 +1,34 @@ +# $schema: ../schema.json +name: $trunc +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/trunc/' +type: + - resolvesToString +encode: array +description: | + Truncates a number to a whole integer or to a specified decimal place. +arguments: + - + name: number + type: + - resolvesToNumber + description: | + Can be any valid expression that resolves to a number. Specifically, the expression must resolve to an integer, double, decimal, or long. + $trunc returns an error if the expression resolves to a non-numeric data type. + - + name: place + type: + - resolvesToInt + optional: true + description: | + Can be any valid expression that resolves to an integer between -20 and 100, exclusive. e.g. -20 < place < 100. Defaults to 0. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/trunc/#example' + pipeline: + - + $project: + truncatedValue: + $trunc: + - '$value' + - 1 diff --git a/generator/config/expression/tsIncrement.yaml b/generator/config/expression/tsIncrement.yaml new file mode 100644 index 000000000..9fded2143 --- /dev/null +++ b/generator/config/expression/tsIncrement.yaml @@ -0,0 +1,39 @@ +# $schema: ../schema.json +name: $tsIncrement +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/tsIncrement/' +type: + - resolvesToLong +encode: single +description: | + Returns the incrementing ordinal from a timestamp as a long. + New in MongoDB 5.1. +arguments: + - + name: expression + type: + - resolvesToTimestamp +tests: + - + name: 'Obtain the Incrementing Ordinal from a Timestamp Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/tsIncrement/#obtain-the-incrementing-ordinal-from-a-timestamp-field' + pipeline: + - + $project: + _id: 0 + saleTimestamp: 1 + saleIncrement: + $tsIncrement: '$saleTimestamp' + - + name: 'Use $tsSecond in a Change Stream Cursor to Monitor Collection Changes' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/tsSecond/#use--tssecond-in-a-change-stream-cursor-to-monitor-collection-changes' + pipeline: + - + $match: + $expr: + $eq: + - + $mod: + - + $tsIncrement: '$clusterTime' + - 2 + - 0 diff --git a/generator/config/expression/tsSecond.yaml b/generator/config/expression/tsSecond.yaml new file mode 100644 index 000000000..20a84904b --- /dev/null +++ b/generator/config/expression/tsSecond.yaml @@ -0,0 +1,33 @@ +# $schema: ../schema.json +name: $tsSecond +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/tsSecond/' +type: + - resolvesToLong +encode: single +description: | + Returns the seconds from a timestamp as a long. + New in MongoDB 5.1. +arguments: + - + name: expression + type: + - resolvesToTimestamp +tests: + - + name: 'Obtain the Number of Seconds from a Timestamp Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/tsSecond/#obtain-the-number-of-seconds-from-a-timestamp-field' + pipeline: + - + $project: + _id: 0 + saleTimestamp: 1 + saleSeconds: + $tsSecond: '$saleTimestamp' + - + name: 'Use $tsSecond in a Change Stream Cursor to Monitor Collection Changes' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/tsSecond/#use--tssecond-in-a-change-stream-cursor-to-monitor-collection-changes' + pipeline: + - + $addFields: + clusterTimeSeconds: + $tsSecond: '$clusterTime' diff --git a/generator/config/expression/type.yaml b/generator/config/expression/type.yaml new file mode 100644 index 000000000..c1f63db79 --- /dev/null +++ b/generator/config/expression/type.yaml @@ -0,0 +1,22 @@ +# $schema: ../schema.json +name: $type +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/type/' +type: + - resolvesToString +encode: single +description: | + Return the BSON data type of the field. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/type/#example' + pipeline: + - + $project: + a: + $type: '$a' diff --git a/generator/config/expression/unsetField.yaml b/generator/config/expression/unsetField.yaml new file mode 100644 index 000000000..a4365a646 --- /dev/null +++ b/generator/config/expression/unsetField.yaml @@ -0,0 +1,59 @@ +# $schema: ../schema.json +name: $unsetField +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unsetField/' +type: + - resolvesToObject +encode: object +description: | + You can use $unsetField to remove fields with names that contain periods (.) or that start with dollar signs ($). + $unsetField is an alias for $setField using $$REMOVE to remove fields. +arguments: + - + name: field + type: + - resolvesToString + description: | + Field in the input object that you want to add, update, or remove. field can be any valid expression that resolves to a string constant. + - + name: input + type: + - resolvesToObject + description: | + Document that contains the field that you want to add or update. input must resolve to an object, missing, null, or undefined. +tests: + - + name: 'Remove Fields that Contain Periods' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unsetField/#remove-fields-that-contain-periods--.-' + pipeline: + - + $replaceWith: + $unsetField: + field: 'price.usd' + input: '$$ROOT' + - + name: 'Remove Fields that Start with a Dollar Sign' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unsetField/#remove-fields-that-start-with-a-dollar-sign----' + pipeline: + - + $replaceWith: + $unsetField: + field: + $literal: '$price' + input: '$$ROOT' + - + name: 'Remove A Subfield' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unsetField/#remove-a-subfield' + pipeline: + - + $replaceWith: + $setField: + field: 'price' + input: '$$ROOT' + value: + $unsetField: + field: 'euro' + input: + # Example uses the short form, the builder always generates the verbose form + # $getField: 'price' + $getField: + field: 'price' diff --git a/generator/config/expression/week.yaml b/generator/config/expression/week.yaml new file mode 100644 index 000000000..6086f57ee --- /dev/null +++ b/generator/config/expression/week.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $week +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/week/' +type: + - resolvesToInt +encode: object +description: | + Returns the week number for a date as a number between 0 (the partial week that precedes the first Sunday of the year) and 53 (leap year). +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/week/#example' + pipeline: + - + $project: + week: + # Example uses the short form, the builder always generates the verbose form + # $week: '$date' + $week: + date: '$date' diff --git a/generator/config/expression/year.yaml b/generator/config/expression/year.yaml new file mode 100644 index 000000000..3326e3495 --- /dev/null +++ b/generator/config/expression/year.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $year +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/year/' +type: + - resolvesToInt +encode: object +description: | + Returns the year for a date as a number (e.g. 2014). +arguments: + - + name: date + type: + - resolvesToDate + - resolvesToTimestamp + - resolvesToObjectId + description: | + The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + - + name: timezone + type: + - resolvesToString + optional: true + description: | + The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/year/#example' + pipeline: + - + $project: + year: + # Example uses the short form, the builder always generates the verbose form + # $year: '$date' + $year: + date: '$date' diff --git a/generator/config/expression/zip.yaml b/generator/config/expression/zip.yaml new file mode 100644 index 000000000..76402dac5 --- /dev/null +++ b/generator/config/expression/zip.yaml @@ -0,0 +1,87 @@ +# $schema: ../schema.json +name: $zip +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/zip/' +type: + - resolvesToArray # of array +encode: object +description: | + Merge two arrays together. +arguments: + - + name: inputs + type: + - resolvesToArray # of array + description: | + An array of expressions that resolve to arrays. The elements of these input arrays combine to form the arrays of the output array. + If any of the inputs arrays resolves to a value of null or refers to a missing field, $zip returns null. + If any of the inputs arrays does not resolve to an array or null nor refers to a missing field, $zip returns an error. + - + name: useLongestLength + type: + - bool + optional: true + description: | + A boolean which specifies whether the length of the longest array determines the number of arrays in the output array. + The default value is false: the shortest array length determines the number of arrays in the output array. + - + name: defaults + type: + - array + optional: true + description: | + An array of default element values to use if the input arrays have different lengths. You must specify useLongestLength: true along with this field, or else $zip will return an error. + If useLongestLength: true but defaults is empty or not specified, $zip uses null as the default value. + If specifying a non-empty defaults, you must specify a default for each input array or else $zip will return an error. +tests: + - + name: 'Matrix Transposition' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/zip/#matrix-transposition' + pipeline: + - + $project: + _id: false + transposed: + $zip: + inputs: + - + $arrayElemAt: + - '$matrix' + - 0 + - + $arrayElemAt: + - '$matrix' + - 1 + - + $arrayElemAt: + - '$matrix' + - 2 + - + name: 'Filtering and Preserving Indexes' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/zip/#filtering-and-preserving-indexes' + pipeline: + - + $project: + _id: false + pages: + $filter: + input: + $zip: + inputs: + - '$pages' + - + $range: + - 0 + - + $size: '$pages' + as: 'pageWithIndex' + cond: + $let: + vars: + page: + $arrayElemAt: + - '$$pageWithIndex' + - 0 + in: + $gte: + - '$$page.reviews' + - 1 diff --git a/generator/config/expressions.php b/generator/config/expressions.php new file mode 100644 index 000000000..f731234b2 --- /dev/null +++ b/generator/config/expressions.php @@ -0,0 +1,171 @@ + ['int', BSON\Int64::class, 'float'], + 'string' => ['string'], + 'object' => ['array', stdClass::class, BSON\Document::class, BSON\Serializable::class], + 'array' => ['array', BSONArray::class, BSON\PackedArray::class], + 'binData' => ['string', BSON\Binary::class], + 'objectId' => [BSON\ObjectId::class], + 'bool' => ['bool'], + 'date' => [BSON\UTCDateTime::class], + 'null' => ['null'], + 'regex' => [BSON\Regex::class], + 'javascript' => ['string', BSON\Javascript::class], + 'int' => ['int'], + 'timestamp' => ['int', BSON\Timestamp::class], + 'long' => ['int', BSON\Int64::class], + 'decimal' => ['int', BSON\Int64::class, 'float', BSON\Decimal128::class], +]; + +// "any" accepts all the BSON types. No generic "object" or "mixed" +$bsonTypes['any'] = ['bool', 'int', 'float', 'string', 'array', 'null', stdClass::class, BSON\Type::class]; + +// "number" accepts all the numeric types +$bsonTypes['number'] = ['int', 'float', BSON\Int64::class, BSON\Decimal128::class]; + +$expressions = []; +$resolvesToInterfaces = []; +foreach ($bsonTypes as $name => $acceptedTypes) { + $expressions[$name] = ['acceptedTypes' => $acceptedTypes]; + + $resolvesTo = 'resolvesTo' . ucfirst($name); + $resolvesToInterface = __NAMESPACE__ . '\\' . ucfirst($resolvesTo); + $expressions[$resolvesTo] = [ + 'generate' => PhpObject::PhpInterface, + 'implements' => [Type\ExpressionInterface::class], + 'returnType' => $resolvesToInterface, + 'acceptedTypes' => $acceptedTypes, + ]; + + $fieldPathName = $name . 'FieldPath'; + if ($name === 'any') { + $fieldPathName = 'fieldPath'; + } else { + $resolvesToInterfaces[] = $resolvesToInterface; + } + + $expressions[$fieldPathName] = [ + 'generate' => PhpObject::PhpClass, + 'implements' => [Type\FieldPathInterface::class, $resolvesToInterface], + 'acceptedTypes' => ['string'], + ]; +} + +$expressions['resolvesToLong']['implements'] = [ResolvesToInt::class]; +$expressions['resolvesToInt']['implements'] = [ResolvesToNumber::class]; +$expressions['resolvesToDecimal']['implements'] = [ResolvesToDouble::class]; +$expressions['resolvesToDouble']['implements'] = [ResolvesToNumber::class]; +$expressions['resolvesToAny']['implements'] = $resolvesToInterfaces; + +return $expressions + [ + 'expression' => [ + 'returnType' => Type\ExpressionInterface::class, + 'acceptedTypes' => [Type\ExpressionInterface::class, ...$bsonTypes['any']], + ], + 'fieldQuery' => [ + 'returnType' => Type\FieldQueryInterface::class, + 'acceptedTypes' => [Type\FieldQueryInterface::class, ...$bsonTypes['any']], + ], + 'query' => [ + 'returnType' => Type\QueryInterface::class, + 'acceptedTypes' => [Type\QueryInterface::class, 'array'], + ], + 'accumulator' => [ + 'returnType' => Type\AccumulatorInterface::class, + 'acceptedTypes' => [Type\AccumulatorInterface::class, ...$bsonTypes['object']], + ], + 'window' => [ + 'returnType' => Type\WindowInterface::class, + 'acceptedTypes' => [Type\WindowInterface::class, ...$bsonTypes['object']], + ], + 'stage' => [ + 'returnType' => Type\StageInterface::class, + 'acceptedTypes' => [Type\StageInterface::class, ...$bsonTypes['object']], + ], + 'pipeline' => [ + 'acceptedTypes' => [Pipeline::class, ...$bsonTypes['array']], + ], + 'variable' => [ + 'generate' => PhpObject::PhpClass, + 'implements' => [ResolvesToAny::class], + 'acceptedTypes' => ['string'], + ], + 'geometry' => [ + 'returnType' => Type\GeometryInterface::class, + 'acceptedTypes' => [Type\GeometryInterface::class, ...$bsonTypes['object']], + ], + 'switchBranch' => [ + 'returnType' => Type\SwitchBranchInterface::class, + 'acceptedTypes' => [Type\SwitchBranchInterface::class, ...$bsonTypes['object']], + ], + 'timeUnit' => [ + 'returnType' => Type\TimeUnit::class, + 'acceptedTypes' => [Type\TimeUnit::class, ResolvesToString::class, ...$bsonTypes['string']], + ], + 'sortSpec' => [ + 'returnType' => Type\Sort::class, + 'acceptedTypes' => [Type\Sort::class], + ], + + // @todo add enum values + 'Granularity' => [ + 'acceptedTypes' => [...$bsonTypes['string']], + ], + 'FullDocument' => [ + 'acceptedTypes' => [...$bsonTypes['string']], + ], + 'FullDocumentBeforeChange' => [ + 'acceptedTypes' => [...$bsonTypes['string']], + ], + 'AccumulatorPercentile' => [ + 'acceptedTypes' => [...$bsonTypes['string']], + ], + 'WhenMatched' => [ + 'acceptedTypes' => [...$bsonTypes['string']], + ], + 'WhenNotMatched' => [ + 'acceptedTypes' => [...$bsonTypes['string']], + ], + + // @todo create specific model classes factories + 'OutCollection' => [ + 'acceptedTypes' => [...$bsonTypes['object']], + ], + 'CollStats' => [ + 'acceptedTypes' => [...$bsonTypes['object']], + ], + 'Range' => [ + 'acceptedTypes' => [...$bsonTypes['object']], + ], + 'FillOut' => [ + 'acceptedTypes' => [...$bsonTypes['object']], + ], + 'SortSpec' => [ + 'acceptedTypes' => [...$bsonTypes['object']], + ], + 'Window' => [ + 'acceptedTypes' => [...$bsonTypes['object']], + ], + 'GeoPoint' => [ + 'acceptedTypes' => [...$bsonTypes['object']], + ], +]; diff --git a/generator/config/query/all.yaml b/generator/config/query/all.yaml new file mode 100644 index 000000000..868e205e2 --- /dev/null +++ b/generator/config/query/all.yaml @@ -0,0 +1,43 @@ +# $schema: ../schema.json +name: $all +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/all/' +type: + - fieldQuery +encode: single +description: | + Matches arrays that contain all elements specified in the query. +arguments: + - + name: value + type: + - fieldQuery + variadic: array +tests: + - + name: 'Use $all to Match Values' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/all/#use--all-to-match-values' + pipeline: + - + $match: + tags: + $all: + - 'appliance' + - 'school' + - 'book' + - + name: 'Use $all with $elemMatch' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/all/#use--all-with--elemmatch' + pipeline: + - + $match: + qty: + $all: + - + $elemMatch: + size: 'M' + num: + $gt: 50 + - + $elemMatch: + num: 100 + color: 'green' diff --git a/generator/config/query/and.yaml b/generator/config/query/and.yaml new file mode 100644 index 000000000..74ebf506e --- /dev/null +++ b/generator/config/query/and.yaml @@ -0,0 +1,51 @@ +# $schema: ../schema.json +name: $and +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/and/' +type: + - query +encode: single +description: | + Joins query clauses with a logical AND returns all documents that match the conditions of both clauses. +arguments: + - + name: queries + type: + - query + variadic: array + variadicMin: 1 +tests: + - + name: 'AND Queries With Multiple Expressions Specifying the Same Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/and/#and-queries-with-multiple-expressions-specifying-the-same-field' + pipeline: + - + $match: + $and: + - + price: + $ne: 1.99 + - + price: + $exists: true + - + name: 'AND Queries With Multiple Expressions Specifying the Same Operator' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/and/#and-queries-with-multiple-expressions-specifying-the-same-operator' + pipeline: + - + $match: + $and: + - + $or: + - + qty: + $lt: 10 + - + qty: + $gt: 50 + - + $or: + - + sale: true + - + price: + $lt: 5 diff --git a/generator/config/query/bitsAllClear.yaml b/generator/config/query/bitsAllClear.yaml new file mode 100644 index 000000000..48e98037d --- /dev/null +++ b/generator/config/query/bitsAllClear.yaml @@ -0,0 +1,38 @@ +# $schema: ../schema.json +name: $bitsAllClear +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAllClear/' +type: + - fieldQuery +encode: single +description: | + Matches numeric or binary values in which a set of bit positions all have a value of 0. +arguments: + - + name: bitmask + type: + - int + - binData + - array # of int +tests: + - + name: 'Bit Position Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAllClear/#bit-position-array' + pipeline: + - + $match: + a: + $bitsAllClear: [1, 5] + - + name: 'Integer Bitmask' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAllClear/#integer-bitmask' + pipeline: + - $match: + a: + $bitsAllClear: 35 + - + name: 'BinData Bitmask' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAllClear/#bindata-bitmask' + pipeline: + - $match: + a: + $bitsAllClear: !bson_binary 'IA==' diff --git a/generator/config/query/bitsAllSet.yaml b/generator/config/query/bitsAllSet.yaml new file mode 100644 index 000000000..25e2c6eb8 --- /dev/null +++ b/generator/config/query/bitsAllSet.yaml @@ -0,0 +1,38 @@ +# $schema: ../schema.json +name: $bitsAllSet +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAllSet/' +type: + - fieldQuery +encode: single +description: | + Matches numeric or binary values in which a set of bit positions all have a value of 1. +arguments: + - + name: bitmask + type: + - int + - binData + - array # of int +tests: + - + name: 'Bit Position Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAllSet/#bit-position-array' + pipeline: + - + $match: + a: + $bitsAllSet: [1, 5] + - + name: 'Integer Bitmask' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAllSet/#integer-bitmask' + pipeline: + - $match: + a: + $bitsAllSet: 50 + - + name: 'BinData Bitmask' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAllSet/#bindata-bitmask' + pipeline: + - $match: + a: + $bitsAllSet: !bson_binary 'MA==' diff --git a/generator/config/query/bitsAnyClear.yaml b/generator/config/query/bitsAnyClear.yaml new file mode 100644 index 000000000..a41260998 --- /dev/null +++ b/generator/config/query/bitsAnyClear.yaml @@ -0,0 +1,38 @@ +# $schema: ../schema.json +name: $bitsAnyClear +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAnyClear/' +type: + - fieldQuery +encode: single +description: | + Matches numeric or binary values in which any bit from a set of bit positions has a value of 0. +arguments: + - + name: bitmask + type: + - int + - binData + - array # of int +tests: + - + name: 'Bit Position Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAnyClear/#bit-position-array' + pipeline: + - + $match: + a: + $bitsAnyClear: [1, 5] + - + name: 'Integer Bitmask' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAnyClear/#integer-bitmask' + pipeline: + - $match: + a: + $bitsAnyClear: 35 + - + name: 'BinData Bitmask' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAnyClear/#bindata-bitmask' + pipeline: + - $match: + a: + $bitsAnyClear: !bson_binary 'MA==' diff --git a/generator/config/query/bitsAnySet.yaml b/generator/config/query/bitsAnySet.yaml new file mode 100644 index 000000000..95aae908a --- /dev/null +++ b/generator/config/query/bitsAnySet.yaml @@ -0,0 +1,38 @@ +# $schema: ../schema.json +name: $bitsAnySet +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAnySet/' +type: + - fieldQuery +encode: single +description: | + Matches numeric or binary values in which any bit from a set of bit positions has a value of 1. +arguments: + - + name: bitmask + type: + - int + - binData + - array # of int +tests: + - + name: 'Bit Position Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAnySet/#bit-position-array' + pipeline: + - + $match: + a: + $bitsAnySet: [1, 5] + - + name: 'Integer Bitmask' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAnySet/#integer-bitmask' + pipeline: + - $match: + a: + $bitsAnySet: 35 + - + name: 'BinData Bitmask' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/bitsAnySet/#bindata-bitmask' + pipeline: + - $match: + a: + $bitsAnySet: !bson_binary 'MA==' diff --git a/generator/config/query/box.yaml b/generator/config/query/box.yaml new file mode 100644 index 000000000..14043c4ae --- /dev/null +++ b/generator/config/query/box.yaml @@ -0,0 +1,13 @@ +# $schema: ../schema.json +name: $box +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/box/' +type: + - geometry +encode: single +description: | + Specifies a rectangular box using legacy coordinate pairs for $geoWithin queries. The 2d index supports $box. +arguments: + - + name: value + type: + - array diff --git a/generator/config/query/center.yaml b/generator/config/query/center.yaml new file mode 100644 index 000000000..c86215a52 --- /dev/null +++ b/generator/config/query/center.yaml @@ -0,0 +1,13 @@ +# $schema: ../schema.json +name: $center +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/center/' +type: + - geometry +encode: single +description: | + Specifies a circle using legacy coordinate pairs to $geoWithin queries when using planar geometry. The 2d index supports $center. +arguments: + - + name: value + type: + - array diff --git a/generator/config/query/centerSphere.yaml b/generator/config/query/centerSphere.yaml new file mode 100644 index 000000000..e3677dade --- /dev/null +++ b/generator/config/query/centerSphere.yaml @@ -0,0 +1,13 @@ +# $schema: ../schema.json +name: $centerSphere +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/centerSphere/' +type: + - geometry +encode: single +description: | + Specifies a circle using either legacy coordinate pairs or GeoJSON format for $geoWithin queries when using spherical geometry. The 2dsphere and 2d indexes support $centerSphere. +arguments: + - + name: value + type: + - array diff --git a/generator/config/query/comment.yaml b/generator/config/query/comment.yaml new file mode 100644 index 000000000..13a344613 --- /dev/null +++ b/generator/config/query/comment.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $comment +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/comment/' +type: + - query +encode: single +description: | + Adds a comment to a query predicate. +arguments: + - + name: comment + type: + - string +tests: + - + name: 'Attach a Comment to an Aggregation Expression' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/comment/#attach-a-comment-to-an-aggregation-expression' + pipeline: + - + $match: + x: + $gt: 0 + $comment: 'Don''t allow negative inputs.' + - + $group: + _id: + $mod: + - '$x' + - 2 + total: + $sum: '$x' diff --git a/generator/config/query/elemMatch.yaml b/generator/config/query/elemMatch.yaml new file mode 100644 index 000000000..95db9572e --- /dev/null +++ b/generator/config/query/elemMatch.yaml @@ -0,0 +1,68 @@ +# $schema: ../schema.json +name: $elemMatch +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/elemMatch/' +type: + - fieldQuery +encode: single +description: | + The $elemMatch operator matches documents that contain an array field with at least one element that matches all the specified query criteria. +arguments: + - + name: query + type: + - query + - fieldQuery +tests: + - + name: 'Element Match' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/elemMatch/#element-match' + pipeline: + - + $match: + results: + $elemMatch: + $gte: 80 + $lt: 85 + - + name: 'Array of Embedded Documents' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/elemMatch/#array-of-embedded-documents' + pipeline: + - + $match: + results: + $elemMatch: + product: 'xyz' + score: + $gte: 8 + - + name: 'Single Query Condition' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/elemMatch/#single-query-condition' + pipeline: + - + $match: + results: + $elemMatch: + product: + $ne: 'xyz' + - + name: 'Using $or with $elemMatch' + pipeline: + - + $match: + game: + $elemMatch: + $or: + - + score: + $gt: 10 + - + score: + $lt: 5 + - + name: 'Single field operator' + pipeline: + - + $match: + results: + $elemMatch: + $gt: 10 diff --git a/generator/config/query/eq.yaml b/generator/config/query/eq.yaml new file mode 100644 index 000000000..5629114cc --- /dev/null +++ b/generator/config/query/eq.yaml @@ -0,0 +1,61 @@ +# $schema: ../schema.json +name: $eq +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/eq/' +type: + - fieldQuery +encode: single +description: | + Matches values that are equal to a specified value. +arguments: + - + name: value + type: + - any +tests: + - + name: 'Equals a Specified Value' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/eq/#equals-a-specified-value' + pipeline: + - + $match: + qty: + $eq: 20 + + - + name: 'Field in Embedded Document Equals a Value' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/eq/#field-in-embedded-document-equals-a-value' + pipeline: + - + $match: + 'item.name': + $eq: 'ab' + + - + name: 'Equals an Array Value' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/eq/#equals-an-array-value' + pipeline: + - + $match: + tags: + $eq: ['A', 'B'] + + - + name: 'Regex Match Behaviour' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/eq/#regex-match-behaviour' + pipeline: + - + $match: + company: 'MongoDB' + - + $match: + company: + $eq: 'MongoDB' + - + $match: + company: + !bson_regex '^MongoDB' + - + $match: + company: + $eq: + !bson_regex '^MongoDB' diff --git a/generator/config/query/exists.yaml b/generator/config/query/exists.yaml new file mode 100644 index 000000000..00d7ce2cb --- /dev/null +++ b/generator/config/query/exists.yaml @@ -0,0 +1,39 @@ +# $schema: ../schema.json +name: $exists +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/exists/' +type: + - fieldQuery +encode: single +description: | + Matches documents that have the specified field. +arguments: + - + name: exists + type: + - bool + default: true +tests: + - + name: 'Exists and Not Equal To' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/exists/#exists-and-not-equal-to' + pipeline: + - + $match: + qty: + $exists: true + $nin: [5, 15] + - + name: 'Null Values' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/exists/#null-values' + pipeline: + - + $match: + qty: + $exists: true + - + name: 'Missing Field' + pipeline: + - + $match: + qty: + $exists: false diff --git a/generator/config/query/expr.yaml b/generator/config/query/expr.yaml new file mode 100644 index 000000000..320c84507 --- /dev/null +++ b/generator/config/query/expr.yaml @@ -0,0 +1,47 @@ +# $schema: ../schema.json +name: $expr +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/expr/' +type: + - query +encode: single +description: | + Allows use of aggregation expressions within the query language. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Compare Two Fields from A Single Document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/expr/#compare-two-fields-from-a-single-document' + pipeline: + - + $match: + $expr: + $gt: + - '$spent' + - '$budget' + - + name: 'Using $expr With Conditional Statements' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/expr/#using--expr-with-conditional-statements' + pipeline: + - + $match: + $expr: + $lt: + - + $cond: + if: + $gte: + - '$qty' + - 100 + then: + $multiply: + - '$price' + - 0.5 + else: + $multiply: + - '$price' + - 0.75 + - 5 diff --git a/generator/config/query/geoIntersects.yaml b/generator/config/query/geoIntersects.yaml new file mode 100644 index 000000000..ca1eda02f --- /dev/null +++ b/generator/config/query/geoIntersects.yaml @@ -0,0 +1,52 @@ +# $schema: ../schema.json +name: $geoIntersects +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/geoIntersects/' +type: + - fieldQuery +encode: single +description: | + Selects geometries that intersect with a GeoJSON geometry. The 2dsphere index supports $geoIntersects. +arguments: + - + name: geometry + type: + - geometry +tests: + - + name: 'Intersects a Polygon' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/geoIntersects/#intersects-a-polygon' + pipeline: + - + $match: + loc: + $geoIntersects: + $geometry: + type: 'Polygon' + coordinates: + - + - [ 0, 0 ] + - [ 3, 6 ] + - [ 6, 1 ] + - [ 0, 0 ] + - + name: 'Intersects a Big Polygon' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/geoIntersects/#intersects-a--big--polygon' + pipeline: + - + $match: + loc: + $geoIntersects: + $geometry: + type: 'Polygon' + coordinates: + - + - [ -100, 60 ] + - [ -100, 0 ] + - [ -100, -60 ] + - [ 100, -60 ] + - [ 100, 60 ] + - [ -100, 60 ] + crs: + type: 'name' + properties: + name: 'urn:x-mongodb:crs:strictwinding:EPSG:4326' diff --git a/generator/config/query/geoWithin.yaml b/generator/config/query/geoWithin.yaml new file mode 100644 index 000000000..3a87e4b72 --- /dev/null +++ b/generator/config/query/geoWithin.yaml @@ -0,0 +1,52 @@ +# $schema: ../schema.json +name: $geoWithin +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/geoWithin/' +type: + - fieldQuery +encode: single +description: | + Selects geometries within a bounding GeoJSON geometry. The 2dsphere and 2d indexes support $geoWithin. +arguments: + - + name: geometry + type: + - geometry +tests: + - + name: 'Within a Polygon' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/geoWithin/#within-a-polygon' + pipeline: + - + $match: + loc: + $geoWithin: + $geometry: + type: 'Polygon' + coordinates: + - + - [ 0, 0 ] + - [ 3, 6 ] + - [ 6, 1 ] + - [ 0, 0 ] + - + name: 'Within a Big Polygon' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/geoWithin/#within-a--big--polygon' + pipeline: + - + $match: + loc: + $geoWithin: + $geometry: + type: 'Polygon' + coordinates: + - + - [ -100, 60 ] + - [ -100, 0 ] + - [ -100, -60 ] + - [ 100, -60 ] + - [ 100, 60 ] + - [ -100, 60 ] + crs: + type: 'name' + properties: + name: 'urn:x-mongodb:crs:strictwinding:EPSG:4326' diff --git a/generator/config/query/geometry.yaml b/generator/config/query/geometry.yaml new file mode 100644 index 000000000..40b18dc99 --- /dev/null +++ b/generator/config/query/geometry.yaml @@ -0,0 +1,22 @@ +# $schema: ../schema.json +name: $geometry +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/geometry/' +type: + - geometry +encode: object +description: | + Specifies a geometry in GeoJSON format to geospatial query operators. +arguments: + - + name: type + type: + - string + - + name: coordinates + type: + - array + - + name: crs + type: + - object + optional: true diff --git a/generator/config/query/gt.yaml b/generator/config/query/gt.yaml new file mode 100644 index 000000000..9914a5f34 --- /dev/null +++ b/generator/config/query/gt.yaml @@ -0,0 +1,22 @@ +# $schema: ../schema.json +name: $gt +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/gt/' +type: + - fieldQuery +encode: single +description: | + Matches values that are greater than a specified value. +arguments: + - + name: value + type: + - any +tests: + - + name: 'Match Document Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/gt/#match-document-fields' + pipeline: + - + $match: + qty: + $gt: 20 diff --git a/generator/config/query/gte.yaml b/generator/config/query/gte.yaml new file mode 100644 index 000000000..d8617a7c6 --- /dev/null +++ b/generator/config/query/gte.yaml @@ -0,0 +1,22 @@ +# $schema: ../schema.json +name: $gte +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/gte/' +type: + - fieldQuery +encode: single +description: | + Matches values that are greater than or equal to a specified value. +arguments: + - + name: value + type: + - any +tests: + - + name: 'Match Document Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/gte/#match-document-fields' + pipeline: + - + $match: + qty: + $gte: 20 diff --git a/generator/config/query/in.yaml b/generator/config/query/in.yaml new file mode 100644 index 000000000..67f069416 --- /dev/null +++ b/generator/config/query/in.yaml @@ -0,0 +1,32 @@ +# $schema: ../schema.json +name: $in +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/in/' +type: + - fieldQuery +encode: single +description: | + Matches any of the values specified in an array. +arguments: + - + name: value + type: + - array # of expression +tests: + - + name: 'Use the $in Operator to Match Values in an Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/in/#use-the--in-operator-to-match-values' + pipeline: + - + $match: + tags: + $in: ['home', 'school'] + - + name: 'Use the $in Operator with a Regular Expression' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/in/#use-the--in-operator-with-a-regular-expression' + pipeline: + - + $match: + tags: + $in: + - !bson_regex '^be' + - !bson_regex '^st' diff --git a/generator/config/query/jsonSchema.yaml b/generator/config/query/jsonSchema.yaml new file mode 100644 index 000000000..4c1dca6ad --- /dev/null +++ b/generator/config/query/jsonSchema.yaml @@ -0,0 +1,39 @@ +# $schema: ../schema.json +name: $jsonSchema +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/jsonSchema/' +type: + - query +encode: single +description: | + Validate documents against the given JSON Schema. +arguments: + - + name: schema + type: + - object +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/jsonSchema/#syntax' + pipeline: + - + $match: + $jsonSchema: + required: + - 'name' + - 'major' + - 'gpa' + - 'address' + properties: + name: + bsonType: 'string' + description: 'must be a string and is required' + address: + bsonType: 'object' + required: + - 'zipcode' + properties: + street: + bsonType: 'string' + zipcode: + bsonType: 'string' diff --git a/generator/config/query/lt.yaml b/generator/config/query/lt.yaml new file mode 100644 index 000000000..f1c996ded --- /dev/null +++ b/generator/config/query/lt.yaml @@ -0,0 +1,22 @@ +# $schema: ../schema.json +name: $lt +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/lt/' +type: + - fieldQuery +encode: single +description: | + Matches values that are less than a specified value. +arguments: + - + name: value + type: + - any +tests: + - + name: 'Match Document Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/lt/#match-document-fields' + pipeline: + - + $match: + qty: + $lt: 20 diff --git a/generator/config/query/lte.yaml b/generator/config/query/lte.yaml new file mode 100644 index 000000000..e61d01b03 --- /dev/null +++ b/generator/config/query/lte.yaml @@ -0,0 +1,22 @@ +# $schema: ../schema.json +name: $lte +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/lte/' +type: + - fieldQuery +encode: single +description: | + Matches values that are less than or equal to a specified value. +arguments: + - + name: value + type: + - any +tests: + - + name: 'Match Document Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/lte/#match-document-fields' + pipeline: + - + $match: + qty: + $lte: 20 diff --git a/generator/config/query/maxDistance.yaml b/generator/config/query/maxDistance.yaml new file mode 100644 index 000000000..e95212d23 --- /dev/null +++ b/generator/config/query/maxDistance.yaml @@ -0,0 +1,13 @@ +# $schema: ../schema.json +name: $maxDistance +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/maxDistance/' +type: + - fieldQuery +encode: single +description: | + Specifies a maximum distance to limit the results of $near and $nearSphere queries. The 2dsphere and 2d indexes support $maxDistance. +arguments: + - + name: value + type: + - number diff --git a/generator/config/query/minDistance.yaml b/generator/config/query/minDistance.yaml new file mode 100644 index 000000000..fd467a96f --- /dev/null +++ b/generator/config/query/minDistance.yaml @@ -0,0 +1,14 @@ +# $schema: ../schema.json +name: $minDistance +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/minDistance/' +type: + - fieldQuery +encode: single +description: | + Specifies a minimum distance to limit the results of $near and $nearSphere queries. For use with 2dsphere index only. +arguments: + - + name: value + type: + - int + - double diff --git a/generator/config/query/mod.yaml b/generator/config/query/mod.yaml new file mode 100644 index 000000000..04a187253 --- /dev/null +++ b/generator/config/query/mod.yaml @@ -0,0 +1,42 @@ +# $schema: ../schema.json +name: $mod +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/mod/' +type: + - fieldQuery +encode: array +description: | + Performs a modulo operation on the value of a field and selects documents with a specified result. +arguments: + - + name: divisor + type: + - number + - + name: remainder + type: + - number +tests: + - + name: 'Use $mod to Select Documents' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/mod/#use--mod-to-select-documents' + pipeline: + - + $match: + qty: + $mod: [4, 0] + - + name: 'Floating Point Arguments' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/mod/#floating-point-arguments' + pipeline: + - + $match: + qty: + $mod: [4.0, 0] + - + $match: + qty: + $mod: [4.5, 0] + - + $match: + qty: + $mod: [4.99, 0] diff --git a/generator/config/query/ne.yaml b/generator/config/query/ne.yaml new file mode 100644 index 000000000..a1f5a046b --- /dev/null +++ b/generator/config/query/ne.yaml @@ -0,0 +1,22 @@ +# $schema: ../schema.json +name: $ne +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/ne/' +type: + - fieldQuery +encode: single +description: | + Matches all values that are not equal to a specified value. +arguments: + - + name: value + type: + - any +tests: + - + name: 'Match Document Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/ne/#match-document-fields' + pipeline: + - + $match: + quantity: + $ne: 20 diff --git a/generator/config/query/near.yaml b/generator/config/query/near.yaml new file mode 100644 index 000000000..f2c791630 --- /dev/null +++ b/generator/config/query/near.yaml @@ -0,0 +1,43 @@ +# $schema: ../schema.json +name: $near +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/near/' +type: + - fieldQuery +encode: dollar_object +description: | + Returns geospatial objects in proximity to a point. Requires a geospatial index. The 2dsphere and 2d indexes support $near. +arguments: + - + name: geometry + type: + - geometry + - + name: maxDistance + type: + - number + optional: true + description: | + Distance in meters. Limits the results to those documents that are at most the specified distance from the center point. + - + name: minDistance + type: + - number + optional: true + description: | + Distance in meters. Limits the results to those documents that are at least the specified distance from the center point. +tests: + - + name: 'Query on GeoJSON Data' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/near/#query-on-geojson-data' + pipeline: + - + $match: + location: + $near: + $geometry: + type: 'Point' + coordinates: + - -73.9667 + - 40.78 + $minDistance: 1000 + $maxDistance: 5000 diff --git a/generator/config/query/nearSphere.yaml b/generator/config/query/nearSphere.yaml new file mode 100644 index 000000000..3411127ad --- /dev/null +++ b/generator/config/query/nearSphere.yaml @@ -0,0 +1,41 @@ +# $schema: ../schema.json +name: $nearSphere +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/nearSphere/' +type: + - fieldQuery +encode: dollar_object +description: | + Returns geospatial objects in proximity to a point on a sphere. Requires a geospatial index. The 2dsphere and 2d indexes support $nearSphere. +arguments: + - + name: geometry + type: + - geometry + - + name: maxDistance + type: + - number + optional: true + description: | + Distance in meters. + - + name: minDistance + type: + - number + optional: true + description: | + Distance in meters. Limits the results to those documents that are at least the specified distance from the center point. +tests: + - + name: 'Specify Center Point Using GeoJSON' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/nearSphere/#specify-center-point-using-geojson' + pipeline: + - + $match: + location: + $nearSphere: + $geometry: + type: 'Point' + coordinates: [-73.9667, 40.78] + $minDistance: 1000 + $maxDistance: 5000 diff --git a/generator/config/query/nin.yaml b/generator/config/query/nin.yaml new file mode 100644 index 000000000..4285e4fe7 --- /dev/null +++ b/generator/config/query/nin.yaml @@ -0,0 +1,30 @@ +# $schema: ../schema.json +name: $nin +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/nin/' +type: + - fieldQuery +encode: single +description: | + Matches none of the values specified in an array. +arguments: + - + name: value + type: + - array # of expression +tests: + - + name: 'Select on Unmatching Documents' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/nin/#select-on-unmatching-documents' + pipeline: + - + $match: + quantity: + $nin: [5, 15] + - + name: 'Select on Elements Not in an Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/nin/#select-on-elements-not-in-an-array' + pipeline: + - + $match: + tags: + $nin: ['school'] diff --git a/generator/config/query/nor.yaml b/generator/config/query/nor.yaml new file mode 100644 index 000000000..d1ad7159a --- /dev/null +++ b/generator/config/query/nor.yaml @@ -0,0 +1,58 @@ +# $schema: ../schema.json +name: $nor +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/nor/' +type: + - query +encode: single +description: | + Joins query clauses with a logical NOR returns all documents that fail to match both clauses. +arguments: + - + name: queries + type: + - query + variadic: array + variadicMin: 1 +tests: + - + name: 'Query with Two Expressions' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/nor/#-nor-query-with-two-expressions' + pipeline: + - + $match: + $nor: + - + price: 1.99 + - + sale: true + - + name: 'Additional Comparisons' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/nor/#-nor-and-additional-comparisons' + pipeline: + - + $match: + $nor: + - + price: 1.99 + - + qty: + $lt: 20 + - + sale: true + - + name: '$nor and $exists' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/nor/#-nor-and--exists' + pipeline: + - + $match: + $nor: + - + price: 1.99 + - + price: + $exists: false + - + sale: true + - + sale: + $exists: false diff --git a/generator/config/query/not.yaml b/generator/config/query/not.yaml new file mode 100644 index 000000000..eb2b43cdb --- /dev/null +++ b/generator/config/query/not.yaml @@ -0,0 +1,31 @@ +# $schema: ../schema.json +name: $not +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/not/' +type: + - fieldQuery +encode: single +description: | + Inverts the effect of a query expression and returns documents that do not match the query expression. +arguments: + - + name: expression + type: + - fieldQuery +tests: + - + name: 'Syntax' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/not/#syntax' + pipeline: + - + $match: + price: + $not: + $gt: 1.99 + - + name: 'Regular Expressions' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/not/#-not-and-regular-expressions' + pipeline: + - + $match: + price: + $not: !bson_regex '^p.*' diff --git a/generator/config/query/or.yaml b/generator/config/query/or.yaml new file mode 100644 index 000000000..ce2b7603c --- /dev/null +++ b/generator/config/query/or.yaml @@ -0,0 +1,47 @@ +# $schema: ../schema.json +name: $or +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/or/' +type: + - query +encode: single +description: | + Joins query clauses with a logical OR returns all documents that match the conditions of either clause. +arguments: + - + name: queries + type: + - query + variadic: array + variadicMin: 1 +tests: + - + name: '$or Clauses' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/or/#-or-clauses-and-indexes' + pipeline: + - + $match: + $or: + - + quantity: + $lt: 20 + - + price: 10 + + - + name: 'Error Handling' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/or/#error-handling' + pipeline: + - + $match: + $or: + - + x: + $eq: 0 + - + $expr: + $eq: + - + $divide: + - 1 + - '$x' + - 3 diff --git a/generator/config/query/polygon.yaml b/generator/config/query/polygon.yaml new file mode 100644 index 000000000..1a46f2bc4 --- /dev/null +++ b/generator/config/query/polygon.yaml @@ -0,0 +1,13 @@ +# $schema: ../schema.json +name: $polygon +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/polygon/' +type: + - geometry +encode: single +description: | + Specifies a polygon to using legacy coordinate pairs for $geoWithin queries. The 2d index supports $center. +arguments: + - + name: points + type: + - array diff --git a/generator/config/query/rand.yaml b/generator/config/query/rand.yaml new file mode 100644 index 000000000..6773ae0d5 --- /dev/null +++ b/generator/config/query/rand.yaml @@ -0,0 +1,26 @@ +# $schema: ../schema.json +name: $rand +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/rand/' +type: + - resolvesToDouble +encode: object +description: | + Generates a random float between 0 and 1. +tests: + - + name: 'Select Random Items From a Collection' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/rand/#select-random-items-from-a-collection' + pipeline: + - + $match: + district: 3 + $expr: + $lt: + - 0.5 + - + $rand: {} + - + $project: + _id: 0 + name: 1 + registered: 1 diff --git a/generator/config/query/regex.yaml b/generator/config/query/regex.yaml new file mode 100644 index 000000000..c7e378ddd --- /dev/null +++ b/generator/config/query/regex.yaml @@ -0,0 +1,33 @@ +# $schema: ../schema.json +name: $regex +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/regex/' +type: + - fieldQuery +encode: single +description: | + Selects documents where values match a specified regular expression. +arguments: + - + name: regex + type: + - regex + +tests: + - + name: 'Perform a LIKE Match' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/regex/#perform-a-like-match' + pipeline: + - + $match: + sku: + $regex: + !bson_regex '789$' + - + name: 'Perform Case-Insensitive Regular Expression Match' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/regex/#perform-case-insensitive-regular-expression-match' + pipeline: + - + $match: + sku: + $regex: + !bson_regex ['^ABC', 'i'] diff --git a/generator/config/query/sampleRate.yaml b/generator/config/query/sampleRate.yaml new file mode 100644 index 000000000..9995e2d8b --- /dev/null +++ b/generator/config/query/sampleRate.yaml @@ -0,0 +1,26 @@ +# $schema: ../schema.json +name: $sampleRate +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sampleRate/' +type: + - query +encode: single +description: | + Randomly select documents at a given rate. Although the exact number of documents selected varies on each run, the quantity chosen approximates the sample rate expressed as a percentage of the total number of documents. +arguments: + - + name: rate + type: + - resolvesToDouble + description: | + The selection process uses a uniform random distribution. The sample rate is a floating point number between 0 and 1, inclusive, which represents the probability that a given document will be selected as it passes through the pipeline. + For example, a sample rate of 0.33 selects roughly one document in three. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sampleRate/#examples' + pipeline: + - + $match: + $sampleRate: 0.33 + - + $count: 'numMatches' diff --git a/generator/config/query/size.yaml b/generator/config/query/size.yaml new file mode 100644 index 000000000..629de4035 --- /dev/null +++ b/generator/config/query/size.yaml @@ -0,0 +1,22 @@ +# $schema: ../schema.json +name: $size +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/size/' +type: + - fieldQuery +encode: single +description: | + Selects documents if the array field is a specified size. +arguments: + - + name: value + type: + - int +tests: + - + name: 'Query an Array by Array Length' + link: 'https://www.mongodb.com/docs/manual/tutorial/query-arrays/#query-an-array-by-array-length' + pipeline: + - + $match: + tags: + $size: 3 diff --git a/generator/config/query/text.yaml b/generator/config/query/text.yaml new file mode 100644 index 000000000..5ec26aaec --- /dev/null +++ b/generator/config/query/text.yaml @@ -0,0 +1,114 @@ +# $schema: ../schema.json +name: $text +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/text/' +type: + - query +encode: dollar_object +description: | + Performs text search. +arguments: + - + name: search + type: + - string + description: | + A string of terms that MongoDB parses and uses to query the text index. MongoDB performs a logical OR search of the terms unless specified as a phrase. + - + name: language + type: + - string + optional: true + description: | + The language that determines the list of stop words for the search and the rules for the stemmer and tokenizer. If not specified, the search uses the default language of the index. + If you specify a default_language value of none, then the text index parses through each word in the field, including stop words, and ignores suffix stemming. + - + name: caseSensitive + type: + - bool + optional: true + description: | + A boolean flag to enable or disable case sensitive search. Defaults to false; i.e. the search defers to the case insensitivity of the text index. + - + name: diacriticSensitive + type: + - bool + optional: true + description: | + A boolean flag to enable or disable diacritic sensitive search against version 3 text indexes. Defaults to false; i.e. the search defers to the diacritic insensitivity of the text index. + Text searches against earlier versions of the text index are inherently diacritic sensitive and cannot be diacritic insensitive. As such, the $diacriticSensitive option has no effect with earlier versions of the text index. +tests: + - + name: 'Search for a Single Word' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/text/#search-for-a-single-word' + pipeline: + - + $match: + $text: + $search: 'coffee' + - + name: 'Match Any of the Search Terms' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/text/#search-for-a-single-word' + pipeline: + - + $match: + $text: + $search: 'bake coffee cake' + - + name: 'Search a Different Language' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/text/#search-a-different-language' + pipeline: + - + $match: + $text: + $search: 'leche' + $language: 'es' + - + name: 'Case and Diacritic Insensitive Search' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/text/#case-and-diacritic-insensitive-search' + pipeline: + - + $match: + $text: + $search: 'сы́рники CAFÉS' + - + name: 'Perform Case Sensitive Search' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/text/#perform-case-sensitive-search' + pipeline: + - + $match: + $text: + $search: 'Coffee' + $caseSensitive: true + - + $match: + $text: + $search: '\"Café Con Leche\"' + $caseSensitive: true + - + name: 'Diacritic Sensitive Search' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/text/#perform-case-sensitive-search' + pipeline: + - + $match: + $text: + $search: 'CAFÉ' + $diacriticSensitive: true + - + name: 'Text Search Score Examples' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/text/#perform-case-sensitive-search' + pipeline: + - + $match: + $text: + $search: 'CAFÉ' + $diacriticSensitive: true + - + $project: + score: + $meta: 'textScore' + - + $sort: + score: + $meta: 'textScore' + - + $limit: 5 diff --git a/generator/config/query/type.yaml b/generator/config/query/type.yaml new file mode 100644 index 000000000..d8cd7bc86 --- /dev/null +++ b/generator/config/query/type.yaml @@ -0,0 +1,88 @@ +# $schema: ../schema.json +name: $type +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/type/' +type: + - fieldQuery +encode: single +description: | + Selects documents if a field is of the specified type. +arguments: + - + name: type + type: + - int + - string + variadic: array +tests: + - + name: 'Querying by Data Type' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/type/#querying-by-data-type' + pipeline: + - + $match: + zipCode: + # Example uses the short form, the builder always generates the verbose form + # $type: 2 + $type: [2] + - + $match: + zipCode: + # Example uses the short form, the builder always generates the verbose form + # $type: 'string' + $type: ['string'] + - + $match: + zipCode: + # Example uses the short form, the builder always generates the verbose form + # $type: 1 + $type: [1] + - + $match: + zipCode: + # Example uses the short form, the builder always generates the verbose form + # $type: 'double' + $type: ['double'] + - + $match: + zipCode: + # Example uses the short form, the builder always generates the verbose form + # $type: 'number' + $type: ['number'] + - + name: 'Querying by Multiple Data Type' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/type/#querying-by-multiple-data-type' + pipeline: + - + $match: + zipCode: + $type: [2, 1] + - + $match: + zipCode: + $type: ['string', 'double'] + - + name: 'Querying by MinKey and MaxKey' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/type/#querying-by-minkey-and-maxkey' + pipeline: + - + $match: + zipCode: + # Example uses the short form, the builder always generates the verbose form + # $type: 'minKey' + $type: ['minKey'] + - + $match: + zipCode: + # Example uses the short form, the builder always generates the verbose form + # $type: 'maxKey' + $type: ['maxKey'] + - + name: 'Querying by Array Type' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/type/#querying-by-array-type' + pipeline: + - + $match: + zipCode: + # Example uses the short form, the builder always generates the verbose form + # $type: 'array' + $type: ['array'] diff --git a/generator/config/query/where.yaml b/generator/config/query/where.yaml new file mode 100644 index 000000000..5f5c974ab --- /dev/null +++ b/generator/config/query/where.yaml @@ -0,0 +1,36 @@ +# $schema: ../schema.json +name: $where +link: 'https://www.mongodb.com/docs/manual/reference/operator/query/where/' +type: + - query +encode: single +description: | + Matches documents that satisfy a JavaScript expression. +arguments: + - + name: function + type: + - javascript +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/query/where/#example' + pipeline: + - + $match: + $where: + $code: |- + function() { + return hex_md5(this.name) == "9b53e667f30cd329dca1ec9e6a83e994" + } + - + $match: + $expr: + $function: + body: + $code: |- + function(name) { + return hex_md5(name) == "9b53e667f30cd329dca1ec9e6a83e994"; + } + args: ['$name'] + lang: 'js' diff --git a/generator/config/schema.json b/generator/config/schema.json new file mode 100644 index 000000000..a68564e8e --- /dev/null +++ b/generator/config/schema.json @@ -0,0 +1,201 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "$ref": "#/definitions/Operator", + "definitions": { + "Operator": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "$comment": "The name of the operator. Must start with a $", + "type": "string", + "pattern": "^\\$[a-z0-9][a-zA-Z0-9]*$" + }, + "link": { + "$comment": "The link to the operator's documentation on MongoDB's website.", + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "type": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "accumulator", + "stage", + "query", + "fieldQuery", + "filter", + "window", + "geometry", + "switchBranch", + "resolvesToAny", + "resolvesToNumber", + "resolvesToDouble", + "resolvesToString", + "resolvesToObject", + "resolvesToArray", + "resolvesToBinData", + "resolvesToObjectId", + "resolvesToBool", + "resolvesToDate", + "resolvesToNull", + "resolvesToRegex", + "resolvesToJavascript", + "resolvesToInt", + "resolvesToTimestamp", + "resolvesToLong", + "resolvesToDecimal" + ] + } + }, + "encode": { + "$comment": "Specifies how operator parameters are encoded.", + "$comment": "array: parameters are encoded as an array of values in the order they are defined by the spec", + "$comment": "object: parameters are encoded as an object with keys matching the parameter names", + "$comment": "single: get the single parameter value", + "$comment": "group: specific for $group stage", + "type": "string", + "enum": [ + "array", + "object", + "flat_object", + "dollar_object", + "single", + "group" + ] + }, + "description": { + "$comment": "The description of the argument from MongoDB's documentation.", + "type": "string" + }, + "arguments": { + "$comment": "An optional list of arguments for the operator.", + "type": "array", + "items": { + "$ref": "#/definitions/Argument" + } + }, + "tests": { + "$comment": "An optional list of examples for the operator.", + "type": "array", + "items": { + "$ref": "#/definitions/Test" + } + } + }, + "required": [ + "description", + "encode", + "link", + "name", + "type" + ], + "title": "Operator" + }, + "Argument": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "pattern": "^(_?[a-z][a-zA-Z0-9]*|N)$" + }, + "type": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "accumulator", + "query", + "fieldQuery", + "pipeline", + "window", + "expression", + "geometry", + "fieldPath", + "timeUnit", + "sortSpec", + "any", + "resolvesToNumber", "numberFieldPath", "number", + "resolvesToDouble", "doubleFieldPath", "double", + "resolvesToString", "stringFieldPath", "string", + "resolvesToObject", "objectFieldPath", "object", + "resolvesToArray", "arrayFieldPath", "array", + "resolvesToBinData", "binDataFieldPath", "binData", + "resolvesToObjectId", "objectIdFieldPath", "objectId", + "resolvesToBool", "boolFieldPath", "bool", + "resolvesToDate", "dateFieldPath", "date", + "resolvesToNull", "nullFieldPath", "null", + "resolvesToRegex", "regexFieldPath", "regex", + "resolvesToJavascript", "javascriptFieldPath", "javascript", + "resolvesToInt", "intFieldPath", "int", + "resolvesToTimestamp", "timestampFieldPath", "timestamp", + "resolvesToLong", "longFieldPath", "long", + "resolvesToDecimal", "decimalFieldPath", "decimal" + ] + } + }, + "description": { + "$comment": "The description of the argument from MongoDB's documentation.", + "type": "string" + }, + "optional": { + "$comment": "Whether the argument is optional or not.", + "type": "boolean" + }, + "valueMin": { + "$comment": "The minimum value for a numeric argument.", + "type": "number" + }, + "valueMax": { + "$comment": "The minimum value for a numeric argument.", + "type": "number" + }, + "variadic": { + "$comment": "Whether the argument is variadic or not.", + "type": "string", + "enum": [ + "array", + "object" + ] + }, + "variadicMin": { + "$comment": "The minimum number of arguments for a variadic parameter.", + "type": "integer", + "minimum": 0 + }, + "default": { + "$comment": "The default value for the argument.", + "type": ["array", "boolean", "number", "string"] + } + }, + "required": [ + "name", + "type" + ], + "title": "Argument" + }, + "Test": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "link": { + "type": "string", + "format": "uri", + "pattern": "^https://" + }, + "pipeline": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } +} diff --git a/generator/config/stage/addFields.yaml b/generator/config/stage/addFields.yaml new file mode 100644 index 000000000..e98f5de18 --- /dev/null +++ b/generator/config/stage/addFields.yaml @@ -0,0 +1,64 @@ +# $schema: ../schema.json +name: $addFields +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/' +type: + - stage +encode: single +description: | + Adds new fields to documents. Outputs documents that contain all existing fields from the input documents and newly added fields. +arguments: + - + name: expression + type: + - expression + variadic: object + description: | + Specify the name of each field to add and set its value to an aggregation expression or an empty object. +tests: + - + name: 'Using Two $addFields Stages' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#using-two--addfields-stages' + pipeline: + - + $addFields: + totalHomework: + # The example renders a single value, but the builder generates an array for consistency + # $sum: '$homework' + $sum: ['$homework'] + totalQuiz: + # $sum: '$quiz' + $sum: ['$quiz'] + - + $addFields: + totalScore: + $add: + - '$totalHomework' + - '$totalQuiz' + - '$extraCredit' + - + name: 'Adding Fields to an Embedded Document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#adding-fields-to-an-embedded-document' + pipeline: + - + $addFields: + specs.fuel_type: 'unleaded' + - + name: 'Overwriting an existing field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#overwriting-an-existing-field' + pipeline: + - + $addFields: + cats: 20 + - + name: 'Add Element to an Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/#add-element-to-an-array' + pipeline: + - + $match: + _id: 1 + - + $addFields: + homework: + $concatArrays: + - '$homework' + - [7] diff --git a/generator/config/stage/bucket.yaml b/generator/config/stage/bucket.yaml new file mode 100644 index 000000000..0cd65feac --- /dev/null +++ b/generator/config/stage/bucket.yaml @@ -0,0 +1,101 @@ +# $schema: ../schema.json +name: $bucket +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bucket/' +type: + - stage +encode: object +description: | + Categorizes incoming documents into groups, called buckets, based on a specified expression and bucket boundaries. +arguments: + - + name: groupBy + type: + - expression # mainly fieldPath + description: | + An expression to group documents by. To specify a field path, prefix the field name with a dollar sign $ and enclose it in quotes. + Unless $bucket includes a default specification, each input document must resolve the groupBy field path or expression to a value that falls within one of the ranges specified by the boundaries. + - + name: boundaries + type: + - array # of expression + description: | + An array of values based on the groupBy expression that specify the boundaries for each bucket. Each adjacent pair of values acts as the inclusive lower boundary and the exclusive upper boundary for the bucket. You must specify at least two boundaries. + The specified values must be in ascending order and all of the same type. The exception is if the values are of mixed numeric types, such as: + - + name: default + type: + - expression + optional: true + description: | + A literal that specifies the _id of an additional bucket that contains all documents whose groupBy expression result does not fall into a bucket specified by boundaries. + If unspecified, each input document must resolve the groupBy expression to a value within one of the bucket ranges specified by boundaries or the operation throws an error. + The default value must be less than the lowest boundaries value, or greater than or equal to the highest boundaries value. + The default value can be of a different type than the entries in boundaries. + - + name: output + type: + - object # of Accumulator + optional: true + description: | + A document that specifies the fields to include in the output documents in addition to the _id field. To specify the field to include, you must use accumulator expressions. + If you do not specify an output document, the operation returns a count field containing the number of documents in each bucket. + If you specify an output document, only the fields specified in the document are returned; i.e. the count field is not returned unless it is explicitly included in the output document. +tests: + - + name: 'Bucket by Year and Filter by Bucket Results' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bucket/#bucket-by-year-and-filter-by-bucket-results' + pipeline: + - + $bucket: + groupBy: '$year_born' + boundaries: [1840, 1850, 1860, 1870, 1880] + default: 'Other' + output: + count: + $sum: 1 + artists: + $push: + name: + $concat: + - '$first_name' + - ' ' + - '$last_name' + year_born: '$year_born' + - + $match: + count: + $gt: 3 + - + name: 'Use $bucket with $facet to Bucket by Multiple Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bucket/#use--bucket-with--facet-to-bucket-by-multiple-fields' + pipeline: + - + $facet: + price: + - + $bucket: + groupBy: '$price' + boundaries: [0, 200, 400] + default: 'Other' + output: + count: + $sum: 1 + artwork: + $push: + title: '$title' + price: '$price' + averagePrice: + $avg: '$price' + year: + - + $bucket: + groupBy: '$year' + boundaries: [1890, 1910, 1920, 1940] + default: 'Unknown' + output: + count: + $sum: 1 + artwork: + $push: + title: '$title' + year: '$year' diff --git a/generator/config/stage/bucketAuto.yaml b/generator/config/stage/bucketAuto.yaml new file mode 100644 index 000000000..48c102e11 --- /dev/null +++ b/generator/config/stage/bucketAuto.yaml @@ -0,0 +1,46 @@ +# $schema: ../schema.json +name: $bucketAuto +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bucketAuto/' +type: + - stage +encode: object +description: | + Categorizes incoming documents into a specific number of groups, called buckets, based on a specified expression. Bucket boundaries are automatically determined in an attempt to evenly distribute the documents into the specified number of buckets. +arguments: + - + name: groupBy + type: + - expression + description: | + An expression to group documents by. To specify a field path, prefix the field name with a dollar sign $ and enclose it in quotes. + - + name: buckets + type: + - int + description: | + A positive 32-bit integer that specifies the number of buckets into which input documents are grouped. + - + name: output + type: + - object # of Accumulator + optional: true + description: | + A document that specifies the fields to include in the output documents in addition to the _id field. To specify the field to include, you must use accumulator expressions. + The default count field is not included in the output document when output is specified. Explicitly specify the count expression as part of the output document to include it. + - + name: granularity + type: + - object # Granularity + optional: true + description: | + A string that specifies the preferred number series to use to ensure that the calculated boundary edges end on preferred round numbers or their powers of 10. + Available only if the all groupBy values are numeric and none of them are NaN. +tests: + - + name: 'Single Facet Aggregation' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/bucketAuto/#single-facet-aggregation' + pipeline: + - + $bucketAuto: + groupBy: '$price' + buckets: 4 diff --git a/generator/config/stage/changeStream.yaml b/generator/config/stage/changeStream.yaml new file mode 100644 index 000000000..44fdb3c5c --- /dev/null +++ b/generator/config/stage/changeStream.yaml @@ -0,0 +1,66 @@ +# $schema: ../schema.json +name: $changeStream +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/changeStream/' +type: + - stage +encode: object +description: | + Returns a Change Stream cursor for the collection or database. This stage can only occur once in an aggregation pipeline and it must occur as the first stage. +arguments: + - + name: allChangesForCluster + type: + - bool + optional: true + description: | + A flag indicating whether the stream should report all changes that occur on the deployment, aside from those on internal databases or collections. + - + name: fullDocument + type: + - string # FullDocument + optional: true + description: | + Specifies whether change notifications include a copy of the full document when modified by update operations. + - + name: fullDocumentBeforeChange + type: + - string # FullDocumentBeforeChange + optional: true + description: | + Valid values are "off", "whenAvailable", or "required". If set to "off", the "fullDocumentBeforeChange" field of the output document is always omitted. If set to "whenAvailable", the "fullDocumentBeforeChange" field will be populated with the pre-image of the document modified by the current change event if such a pre-image is available, and will be omitted otherwise. If set to "required", then the "fullDocumentBeforeChange" field is always populated and an exception is thrown if the pre-image is not available. + - + name: resumeAfter + type: + - int + optional: true + description: | + Specifies a resume token as the logical starting point for the change stream. Cannot be used with startAfter or startAtOperationTime fields. + - + name: showExpandedEvents + type: + - bool + optional: true + description: | + Specifies whether to include additional change events, such as such as DDL and index operations. + New in MongoDB 6.0. + - + name: startAfter + type: + - object + optional: true + description: | + Specifies a resume token as the logical starting point for the change stream. Cannot be used with resumeAfter or startAtOperationTime fields. + - + name: startAtOperationTime + type: + - timestamp + optional: true + description: | + Specifies a time as the logical starting point for the change stream. Cannot be used with resumeAfter or startAfter fields. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/changeStream/#examples' + pipeline: + - + $changeStream: {} diff --git a/generator/config/stage/changeStreamSplitLargeEvent.yaml b/generator/config/stage/changeStreamSplitLargeEvent.yaml new file mode 100644 index 000000000..208129346 --- /dev/null +++ b/generator/config/stage/changeStreamSplitLargeEvent.yaml @@ -0,0 +1,16 @@ +# $schema: ../schema.json +name: $changeStreamSplitLargeEvent +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/changeStreamSplitLargeEvent/' +type: + - stage +encode: object +description: | + Splits large change stream events that exceed 16 MB into smaller fragments returned in a change stream cursor. + You can only use $changeStreamSplitLargeEvent in a $changeStream pipeline and it must be the final stage in the pipeline. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/changeStreamSplitLargeEvent/#example' + pipeline: + - + $changeStreamSplitLargeEvent: {} diff --git a/generator/config/stage/collStats.yaml b/generator/config/stage/collStats.yaml new file mode 100644 index 000000000..26cbbc470 --- /dev/null +++ b/generator/config/stage/collStats.yaml @@ -0,0 +1,59 @@ +# $schema: ../schema.json +name: $collStats +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/collStats/' +type: + - stage +encode: object +description: | + Returns statistics regarding a collection or view. +arguments: + - + name: latencyStats + type: + - object + optional: true + - + name: storageStats + type: + - object + optional: true + - + name: count + type: + - object # empty object + optional: true + - + name: queryExecStats + type: + - object # empty object + optional: true +tests: + - + name: 'latencyStats Document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/collStats/#latencystats-document' + pipeline: + - + $collStats: + latencyStats: + histograms: true + - + name: 'storageStats Document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/collStats/#storagestats-document' + pipeline: + - + $collStats: + storageStats: {} + - + name: 'count Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/collStats/#count-field' + pipeline: + - + $collStats: + count: {} + - + name: 'queryExecStats Document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/collStats/#queryexecstats-document' + pipeline: + - + $collStats: + queryExecStats: {} diff --git a/generator/config/stage/count.yaml b/generator/config/stage/count.yaml new file mode 100644 index 000000000..a0fa3ba57 --- /dev/null +++ b/generator/config/stage/count.yaml @@ -0,0 +1,27 @@ +# $schema: ../schema.json +name: $count +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/count/' +type: + - stage +encode: single +description: | + Returns a count of the number of documents at this stage of the aggregation pipeline. + Distinct from the $count aggregation accumulator. +arguments: + - + name: field + type: + - string + description: | + Name of the output field which has the count as its value. It must be a non-empty string, must not start with $ and must not contain the . character. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/count/#example' + pipeline: + - + $match: + score: + $gt: 80 + - + $count: 'passing_scores' diff --git a/generator/config/stage/currentOp.yaml b/generator/config/stage/currentOp.yaml new file mode 100644 index 000000000..024c71318 --- /dev/null +++ b/generator/config/stage/currentOp.yaml @@ -0,0 +1,59 @@ +# $schema: ../schema.json +name: $currentOp +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/currentOp/' +type: + - stage +encode: object +description: | + Returns information on active and/or dormant operations for the MongoDB deployment. To run, use the db.aggregate() method. +arguments: + - + name: allUsers + type: + - bool + optional: true + - + name: idleConnections + type: + - bool + optional: true + - + name: idleCursors + type: + - bool + optional: true + - + name: idleSessions + type: + - bool + optional: true + - + name: localOps + type: + - bool + optional: true +tests: + - + name: 'Inactive Sessions' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/currentOp/#inactive-sessions' + pipeline: + - + $currentOp: + allUsers: true + idleSessions: true + - + $match: + active: false + transaction: + $exists: true + - + name: 'Sampled Queries' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/currentOp/#sampled-queries' + pipeline: + - + $currentOp: + allUsers: true + localOps: true + - + $match: + desc: 'query analyzer' diff --git a/generator/config/stage/densify.yaml b/generator/config/stage/densify.yaml new file mode 100644 index 000000000..a66cf18dd --- /dev/null +++ b/generator/config/stage/densify.yaml @@ -0,0 +1,56 @@ +# $schema: ../schema.json +name: $densify +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/densify/' +type: + - stage +encode: object +description: | + Creates new documents in a sequence of documents where certain values in a field are missing. +arguments: + - + name: field + type: + - string # field name + description: | + The field to densify. The values of the specified field must either be all numeric values or all dates. + Documents that do not contain the specified field continue through the pipeline unmodified. + To specify a in an embedded document or in an array, use dot notation. + - + name: partitionByFields + type: + - array # of string + optional: true + description: | + The field(s) that will be used as the partition keys. + - + name: range + type: + - object # Range + description: | + Specification for range based densification. +tests: + - + name: 'Densify Time Series Data' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/densify/#densify-time-series-data' + pipeline: + - + $densify: + field: 'timestamp' + range: + step: 1 + unit: 'hour' + bounds: + - !bson_utcdatetime '2021-05-18T00:00:00.000Z' + - !bson_utcdatetime '2021-05-18T08:00:00.000Z' + - + name: 'Densifiction with Partitions' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/densify/#densifiction-with-partitions' + pipeline: + - + $densify: + field: 'altitude' + partitionByFields: + - 'variety' + range: + bounds: 'full' + step: 200 diff --git a/generator/config/stage/documents.yaml b/generator/config/stage/documents.yaml new file mode 100644 index 000000000..666468da8 --- /dev/null +++ b/generator/config/stage/documents.yaml @@ -0,0 +1,53 @@ +# $schema: ../schema.json +name: $documents +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/documents/' +type: + - stage +encode: single +description: | + Returns literal documents from input values. +arguments: + - + name: documents + type: + - resolvesToArray # of object + description: | + $documents accepts any valid expression that resolves to an array of objects. This includes: + - system variables, such as $$NOW or $$SEARCH_META + - $let expressions + - variables in scope from $lookup expressions + Expressions that do not resolve to a current document, like $myField or $$ROOT, will result in an error. +tests: + - + name: 'Test a Pipeline Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/documents/#test-a-pipeline-stage' + pipeline: + - + $documents: + - { x: 10 } + - { x: 2 } + - { x: 5 } + - + $bucketAuto: + groupBy: '$x' + buckets: 4 + - + name: 'Use a $documents Stage in a $lookup Stage' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/documents/#use-a--documents-stage-in-a--lookup-stage' + pipeline: + - + $match: {} + - + $lookup: + localField: 'zip' + foreignField: 'zip_id' + as: 'city_state' + pipeline: + - + $documents: + - + zip_id: 94301 + name: 'Palo Alto, CA' + - + zip_id: 10019 + name: 'New York, NY' diff --git a/generator/config/stage/facet.yaml b/generator/config/stage/facet.yaml new file mode 100644 index 000000000..013163c2a --- /dev/null +++ b/generator/config/stage/facet.yaml @@ -0,0 +1,51 @@ +# $schema: ../schema.json +name: $facet +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/facet/' +type: + - stage +encode: single +description: | + Processes multiple aggregation pipelines within a single stage on the same set of input documents. Enables the creation of multi-faceted aggregations capable of characterizing data across multiple dimensions, or facets, in a single stage. +arguments: + - + name: facet + type: + - pipeline + variadic: object +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/facet/#example' + pipeline: + - + $facet: + categorizedByTags: + - + # The builder uses the verbose form of the $unwind operator + # $unwind: '$tags' + $unwind: + path: '$tags' + - + $sortByCount: '$tags' + categorizedByPrice: + - + $match: + price: + # The example uses an int, but the builder requires a bool + # $exists: 1 + $exists: true + - + $bucket: + groupBy: '$price' + boundaries: [0, 150, 200, 300, 400] + default: 'Other' + output: + count: + $sum: 1 + titles: + $push: '$title' + categorizedByYears(Auto): + - + $bucketAuto: + groupBy: '$year' + buckets: 4 diff --git a/generator/config/stage/fill.yaml b/generator/config/stage/fill.yaml new file mode 100644 index 000000000..d3a9ec390 --- /dev/null +++ b/generator/config/stage/fill.yaml @@ -0,0 +1,110 @@ +# $schema: ../schema.json +name: $fill +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/fill/' +type: + - stage +encode: object +description: | + Populates null and missing field values within documents. +arguments: + - + name: partitionBy + type: + - object # of expression + - string + optional: true + description: | + Specifies an expression to group the documents. In the $fill stage, a group of documents is known as a partition. + If you omit partitionBy and partitionByFields, $fill uses one partition for the entire collection. + partitionBy and partitionByFields are mutually exclusive. + - + name: partitionByFields + type: + - array # of string + optional: true + description: | + Specifies an array of fields as the compound key to group the documents. In the $fill stage, each group of documents is known as a partition. + If you omit partitionBy and partitionByFields, $fill uses one partition for the entire collection. + partitionBy and partitionByFields are mutually exclusive. + - + name: sortBy + type: + - object # SortSpec + optional: true + description: | + Specifies the field or fields to sort the documents within each partition. Uses the same syntax as the $sort stage. + - + name: output + type: + - object # of object{value:expression} or object{method:string}> + description: | + Specifies an object containing each field for which to fill missing values. You can specify multiple fields in the output object. + The object name is the name of the field to fill. The object value specifies how the field is filled. +tests: + - + name: 'Fill Missing Field Values with a Constant Value' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/fill/#fill-missing-field-values-with-a-constant-value' + pipeline: + - + $fill: + output: + bootsSold: + value: 0 + sandalsSold: + value: 0 + sneakersSold: + value: 0 + - + name: 'Fill Missing Field Values with Linear Interpolation' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/fill/#fill-missing-field-values-with-linear-interpolation' + pipeline: + - + $fill: + sortBy: + time: 1 + output: + price: + method: 'linear' + - + name: 'Fill Missing Field Values Based on the Last Observed Value' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/fill/#fill-missing-field-values-based-on-the-last-observed-value' + pipeline: + - + $fill: + sortBy: + date: 1 + output: + score: + method: 'locf' + - + name: 'Fill Data for Distinct Partitions' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/fill/#fill-data-for-distinct-partitions' + pipeline: + - + $fill: + sortBy: + date: 1 + partitionBy: + restaurant: '$restaurant' + output: + score: + method: 'locf' + - + name: 'Indicate if a Field was Populated Using $fill' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/fill/#indicate-if-a-field-was-populated-using--fill' + pipeline: + - + $set: + valueExisted: + $ifNull: + - + $toBool: + $toString: '$score' + - false + - + $fill: + sortBy: + date: 1 + output: + score: + method: 'locf' diff --git a/generator/config/stage/geoNear.yaml b/generator/config/stage/geoNear.yaml new file mode 100644 index 000000000..c9968509d --- /dev/null +++ b/generator/config/stage/geoNear.yaml @@ -0,0 +1,160 @@ +# $schema: ../schema.json +name: $geoNear +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/geoNear/' +type: + - stage +encode: object +description: | + Returns an ordered stream of documents based on the proximity to a geospatial point. Incorporates the functionality of $match, $sort, and $limit for geospatial data. The output documents include an additional distance field and can include a location identifier field. +arguments: + - + name: distanceField + type: + - string + description: | + The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. + - + name: distanceMultiplier + type: + - number + optional: true + description: | + The factor to multiply all distances returned by the query. For example, use the distanceMultiplier to convert radians, as returned by a spherical query, to kilometers by multiplying by the radius of the Earth. + - + name: includeLocs + type: + - string + optional: true + description: | + This specifies the output field that identifies the location used to calculate the distance. This option is useful when a location field contains multiple locations. To specify a field within an embedded document, use dot notation. + - + name: key + type: + - string + optional: true + description: | + Specify the geospatial indexed field to use when calculating the distance. + - + name: maxDistance + type: + - number + optional: true + description: | + The maximum distance from the center point that the documents can be. MongoDB limits the results to those documents that fall within the specified distance from the center point. + Specify the distance in meters if the specified point is GeoJSON and in radians if the specified point is legacy coordinate pairs. + - + name: minDistance + type: + - number + optional: true + description: | + The minimum distance from the center point that the documents can be. MongoDB limits the results to those documents that fall outside the specified distance from the center point. + Specify the distance in meters for GeoJSON data and in radians for legacy coordinate pairs. + - + name: near + type: + - object # GeoPoint + - resolvesToObject + description: | + The point for which to find the closest documents. + - + name: query + type: + - query + optional: true + description: | + Limits the results to the documents that match the query. The query syntax is the usual MongoDB read operation query syntax. + You cannot specify a $near predicate in the query field of the $geoNear stage. + - + name: spherical + type: + - bool + optional: true + description: | + Determines how MongoDB calculates the distance between two points: + - When true, MongoDB uses $nearSphere semantics and calculates distances using spherical geometry. + - When false, MongoDB uses $near semantics: spherical geometry for 2dsphere indexes and planar geometry for 2d indexes. + Default: false. +tests: + - + name: 'Maximum Distance' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/geoNear/#maximum-distance' + pipeline: + - + $geoNear: + near: + type: 'Point' + coordinates: + - -73.99279 + - 40.719296 + distanceField: 'dist.calculated' + maxDistance: 2 + query: + category: 'Parks' + includeLocs: 'dist.location' + spherical: true + - + name: 'Minimum Distance' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/geoNear/#minimum-distance' + pipeline: + - + $geoNear: + near: + type: 'Point' + coordinates: + - -73.99279 + - 40.719296 + distanceField: 'dist.calculated' + minDistance: 2 + query: + category: 'Parks' + includeLocs: 'dist.location' + spherical: true + - + name: 'with the let option' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/geoNear/#-geonear-with-the-let-option' + pipeline: + - + $geoNear: + near: '$$pt' + distanceField: 'distance' + maxDistance: 2 + query: + category: 'Parks' + includeLocs: 'dist.location' + spherical: true + - + name: 'with Bound let Option' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/geoNear/#-geonear-with-bound-let-option' + pipeline: + - + $lookup: + from: 'places' + let: + pt: '$location' + pipeline: + - + $geoNear: + near: '$$pt' + distanceField: 'distance' + as: 'joinedField' + - + $match: + name: 'Sara D. Roosevelt Park' + - + name: 'Specify Which Geospatial Index to Use' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/geoNear/#specify-which-geospatial-index-to-use' + pipeline: + - + $geoNear: + near: + type: 'Point' + coordinates: + - -73.98142 + - 40.71782 + key: 'location' + distanceField: 'dist.calculated' + query: + category: 'Parks' + - + $limit: 5 diff --git a/generator/config/stage/graphLookup.yaml b/generator/config/stage/graphLookup.yaml new file mode 100644 index 000000000..ae220620b --- /dev/null +++ b/generator/config/stage/graphLookup.yaml @@ -0,0 +1,108 @@ +# $schema: ../schema.json +name: $graphLookup +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/graphLookup/' +type: + - stage +encode: object +description: | + Performs a recursive search on a collection. To each output document, adds a new array field that contains the traversal results of the recursive search for that document. +arguments: + - + name: from + type: + - string + description: | + Target collection for the $graphLookup operation to search, recursively matching the connectFromField to the connectToField. The from collection must be in the same database as any other collections used in the operation. + Starting in MongoDB 5.1, the collection specified in the from parameter can be sharded. + - + name: startWith + type: + - expression + - array + description: | + Expression that specifies the value of the connectFromField with which to start the recursive search. Optionally, startWith may be array of values, each of which is individually followed through the traversal process. + - + name: connectFromField + type: + - string + description: | + Field name whose value $graphLookup uses to recursively match against the connectToField of other documents in the collection. If the value is an array, each element is individually followed through the traversal process. + - + name: connectToField + type: + - string + description: | + Field name in other documents against which to match the value of the field specified by the connectFromField parameter. + - + name: as + type: + - string + description: | + Name of the array field added to each output document. Contains the documents traversed in the $graphLookup stage to reach the document. + - + name: maxDepth + type: + - int + optional: true + description: | + Non-negative integral number specifying the maximum recursion depth. + - + name: depthField + type: + - string + optional: true + description: | + Name of the field to add to each traversed document in the search path. The value of this field is the recursion depth for the document, represented as a NumberLong. Recursion depth value starts at zero, so the first lookup corresponds to zero depth. + - + name: restrictSearchWithMatch + type: + - query + optional: true + description: | + A document specifying additional conditions for the recursive search. The syntax is identical to query filter syntax. +tests: + - + name: 'Within a Single Collection' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/graphLookup/#within-a-single-collection' + pipeline: + - + $graphLookup: + from: 'employees' + startWith: '$reportsTo' + connectFromField: 'reportsTo' + connectToField: 'name' + as: 'reportingHierarchy' + - + name: 'Across Multiple Collections' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/graphLookup/#across-multiple-collections' + pipeline: + - + $graphLookup: + from: 'airports' + startWith: '$nearestAirport' + connectFromField: 'connects' + connectToField: 'airport' + maxDepth: 2 + depthField: 'numConnections' + as: 'destinations' + - + name: 'With a Query Filter' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/graphLookup/#with-a-query-filter' + pipeline: + - + $match: + name: 'Tanya Jordan' + - + $graphLookup: + from: 'people' + startWith: '$friends' + connectFromField: 'friends' + connectToField: 'name' + as: 'golfers' + restrictSearchWithMatch: + hobbies: 'golf' + - + $project: + name: 1 + friends: 1 + connections who play golf: '$golfers.name' diff --git a/generator/config/stage/group.yaml b/generator/config/stage/group.yaml new file mode 100644 index 000000000..3e93588e9 --- /dev/null +++ b/generator/config/stage/group.yaml @@ -0,0 +1,122 @@ +# $schema: ../schema.json +name: $group +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/' +type: + - stage +encode: group +description: | + Groups input documents by a specified identifier expression and applies the accumulator expression(s), if specified, to each group. Consumes all input documents and outputs one document per each distinct group. The output documents only contain the identifier field and, if specified, accumulated fields. +arguments: + - + name: _id + type: + - expression + description: | + The _id expression specifies the group key. If you specify an _id value of null, or any other constant value, the $group stage returns a single document that aggregates values across all of the input documents. + - + name: field + type: + - accumulator + variadic: object + variadicMin: 0 + description: | + Computed using the accumulator operators. +tests: + - + name: 'Count the Number of Documents in a Collection' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/#count-the-number-of-documents-in-a-collection' + pipeline: + - + $group: + _id: ~ + count: + $count: {} + - + name: 'Retrieve Distinct Values' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/#retrieve-distinct-values' + pipeline: + - + $group: + _id: '$item' + - + name: 'Group by Item Having' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/#group-by-item-having' + pipeline: + - + $group: + _id: '$item' + totalSaleAmount: + $sum: + $multiply: + - '$price' + - '$quantity' + - + $match: + totalSaleAmount: + $gte: 100 + - + name: 'Calculate Count Sum and Average' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/#calculate-count--sum--and-average' + pipeline: + - + $match: + date: + $gte: !bson_utcdatetime '2014-01-01' + $lt: !bson_utcdatetime '2015-01-01' + - + $group: + _id: + $dateToString: + format: '%Y-%m-%d' + date: '$date' + totalSaleAmount: + $sum: + $multiply: + - '$price' + - '$quantity' + averageQuantity: + $avg: '$quantity' + count: + $sum: 1 + - + $sort: + totalSaleAmount: -1 + - + name: 'Group by null' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/#group-by-null' + pipeline: + - + $group: + _id: ~ + totalSaleAmount: + $sum: + $multiply: + - '$price' + - '$quantity' + averageQuantity: + $avg: '$quantity' + count: + $sum: 1 + - + name: 'Pivot Data' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/#pivot-data' + pipeline: + - + $group: + _id: '$author' + books: + $push: '$title' + - + name: 'Group Documents by author' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/#group-documents-by-author' + pipeline: + - + $group: + _id: '$author' + books: + $push: '$$ROOT' + - + $addFields: + totalCopies: + # $sum: '$books.copies' + $sum: ['$books.copies'] diff --git a/generator/config/stage/indexStats.yaml b/generator/config/stage/indexStats.yaml new file mode 100644 index 000000000..178b209d8 --- /dev/null +++ b/generator/config/stage/indexStats.yaml @@ -0,0 +1,15 @@ +# $schema: ../schema.json +name: $indexStats +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexStats/' +type: + - stage +encode: object +description: | + Returns statistics regarding the use of each index for the collection. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexStats/#example' + pipeline: + - + $indexStats: {} diff --git a/generator/config/stage/limit.yaml b/generator/config/stage/limit.yaml new file mode 100644 index 000000000..fff391a01 --- /dev/null +++ b/generator/config/stage/limit.yaml @@ -0,0 +1,20 @@ +# $schema: ../schema.json +name: $limit +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/limit/' +type: + - stage +encode: single +description: | + Passes the first n documents unmodified to the pipeline where n is the specified limit. For each input document, outputs either one document (for the first n documents) or zero documents (after the first n documents). +arguments: + - + name: limit + type: + - int +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/limit/#example' + pipeline: + - + $limit: 5 diff --git a/generator/config/stage/listLocalSessions.yaml b/generator/config/stage/listLocalSessions.yaml new file mode 100644 index 000000000..50dccc30e --- /dev/null +++ b/generator/config/stage/listLocalSessions.yaml @@ -0,0 +1,47 @@ +# $schema: ../schema.json +name: $listLocalSessions +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listLocalSessions/' +type: + - stage +encode: object +description: | + Lists all active sessions recently in use on the currently connected mongos or mongod instance. These sessions may have not yet propagated to the system.sessions collection. +arguments: + - + name: users + type: + - array + optional: true + description: | + Returns all sessions for the specified users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster to list sessions for other users. + - + name: allUsers + type: + - bool + optional: true + description: | + Returns all sessions for all users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster. +tests: + - + name: 'List All Local Sessions' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listLocalSessions/#list-all-local-sessions' + pipeline: + - + $listLocalSessions: + allUsers: true + - + name: 'List All Local Sessions for the Specified Users' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listLocalSessions/#list-all-local-sessions-for-the-specified-users' + pipeline: + - + $listLocalSessions: + users: + - + user: 'myAppReader' + db: 'test' + - + name: 'List All Local Sessions for the Current User' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listLocalSessions/#list-all-local-sessions-for-the-current-user' + pipeline: + - + $listLocalSessions: {} diff --git a/generator/config/stage/listSampledQueries.yaml b/generator/config/stage/listSampledQueries.yaml new file mode 100644 index 000000000..f767f0d04 --- /dev/null +++ b/generator/config/stage/listSampledQueries.yaml @@ -0,0 +1,29 @@ +# $schema: ../schema.json +name: $listSampledQueries +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSampledQueries/' +type: + - stage +encode: object +description: | + Lists sampled queries for all collections or a specific collection. +arguments: + - + name: namespace + type: + - string + optional: true +tests: + - + name: 'List Sampled Queries for All Collections' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSampledQueries/#list-sampled-queries-for-all-collections' + pipeline: + - + $listSampledQueries: {} + + - + name: 'List Sampled Queries for A Specific Collection' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSampledQueries/#list-sampled-queries-for-a-specific-collection' + pipeline: + - + $listSampledQueries: + namespace: 'social.post' diff --git a/generator/config/stage/listSearchIndexes.yaml b/generator/config/stage/listSearchIndexes.yaml new file mode 100644 index 000000000..afc4f6d05 --- /dev/null +++ b/generator/config/stage/listSearchIndexes.yaml @@ -0,0 +1,44 @@ +# $schema: ../schema.json +name: $listSearchIndexes +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSearchIndexes/' +type: + - stage +encode: object +description: | + Returns information about existing Atlas Search indexes on a specified collection. +arguments: + - + name: id + type: + - string + optional: true + description: | + The id of the index to return information about. + - + name: name + type: + - string + optional: true + description: | + The name of the index to return information about. +tests: + - + name: 'Return All Search Indexes' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSearchIndexes/#return-all-search-indexes' + pipeline: + - + $listSearchIndexes: {} + - + name: 'Return a Single Search Index by Name' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSearchIndexes/#return-a-single-search-index-by-name' + pipeline: + - + $listSearchIndexes: + name: 'synonym-mappings' + - + name: 'Return a Single Search Index by id' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSearchIndexes/#return-a-single-search-index-by-id' + pipeline: + - + $listSearchIndexes: + id: '6524096020da840844a4c4a7' diff --git a/generator/config/stage/listSessions.yaml b/generator/config/stage/listSessions.yaml new file mode 100644 index 000000000..efb56de05 --- /dev/null +++ b/generator/config/stage/listSessions.yaml @@ -0,0 +1,48 @@ +# $schema: ../schema.json +name: $listSessions +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSessions/' +type: + - stage +encode: object +description: | + Lists all sessions that have been active long enough to propagate to the system.sessions collection. +arguments: + - + name: users + type: + - array + optional: true + description: | + Returns all sessions for the specified users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster to list sessions for other users. + - + name: allUsers + type: + - bool + optional: true + description: | + Returns all sessions for all users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster. +tests: + - + name: 'List All Sessions' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSessions/#list-all-sessions' + pipeline: + - + $listSessions: + allUsers: true + - + name: 'List All Sessions for the Specified Users' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSessions/#list-all-sessions-for-the-specified-users' + pipeline: + - + $listSessions: + users: + - + user: 'myAppReader' + db: 'test' + - + name: 'List All Sessions for the Current User' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSessions/#list-all-sessions-for-the-current-user' + pipeline: + - + $listSessions: {} + diff --git a/generator/config/stage/lookup.yaml b/generator/config/stage/lookup.yaml new file mode 100644 index 000000000..b73770e47 --- /dev/null +++ b/generator/config/stage/lookup.yaml @@ -0,0 +1,165 @@ +# $schema: ../schema.json +name: $lookup +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/' +type: + - stage +encode: object +description: | + Performs a left outer join to another collection in the same database to filter in documents from the "joined" collection for processing. +arguments: + - + name: from + type: + - string + optional: true + description: | + Specifies the collection in the same database to perform the join with. + from is optional, you can use a $documents stage in a $lookup stage instead. For an example, see Use a $documents Stage in a $lookup Stage. + Starting in MongoDB 5.1, the collection specified in the from parameter can be sharded. + - + name: localField + type: + - string + optional: true + description: | + Specifies the field from the documents input to the $lookup stage. $lookup performs an equality match on the localField to the foreignField from the documents of the from collection. If an input document does not contain the localField, the $lookup treats the field as having a value of null for matching purposes. + - + name: foreignField + type: + - string + optional: true + description: | + Specifies the field from the documents in the from collection. $lookup performs an equality match on the foreignField to the localField from the input documents. If a document in the from collection does not contain the foreignField, the $lookup treats the value as null for matching purposes. + - + name: let + type: + - object # of expression + optional: true + description: | + Specifies variables to use in the pipeline stages. Use the variable expressions to access the fields from the joined collection's documents that are input to the pipeline. + - + name: pipeline + type: + - pipeline + optional: true + description: | + Specifies the pipeline to run on the joined collection. The pipeline determines the resulting documents from the joined collection. To return all documents, specify an empty pipeline []. + The pipeline cannot include the $out stage or the $merge stage. Starting in v6.0, the pipeline can contain the Atlas Search $search stage as the first stage inside the pipeline. + The pipeline cannot directly access the joined document fields. Instead, define variables for the joined document fields using the let option and then reference the variables in the pipeline stages. + - + name: as + type: + - string + description: | + Specifies the name of the new array field to add to the input documents. The new array field contains the matching documents from the from collection. If the specified name already exists in the input document, the existing field is overwritten. +tests: + - + name: 'Perform a Single Equality Join with $lookup' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#perform-a-single-equality-join-with--lookup' + pipeline: + - + $lookup: + from: 'inventory' + localField: 'item' + foreignField: 'sku' + as: 'inventory_docs' + - + name: 'Use $lookup with an Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#use--lookup-with-an-array' + pipeline: + - + $lookup: + from: 'members' + localField: 'enrollmentlist' + foreignField: 'name' + as: 'enrollee_info' + - + name: 'Use $lookup with $mergeObjects' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#use--lookup-with--mergeobjects' + pipeline: + - + $lookup: + from: 'items' + localField: 'item' + foreignField: 'item' + as: 'fromItems' + - + $replaceRoot: + newRoot: + $mergeObjects: + - + $arrayElemAt: + - '$fromItems' + - 0 + - '$$ROOT' + - + $project: + fromItems: 0 + - + name: 'Perform Multiple Joins and a Correlated Subquery with $lookup' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#perform-multiple-joins-and-a-correlated-subquery-with--lookup' + pipeline: + - + $lookup: + from: 'warehouses' + let: + order_item: '$item' + order_qty: '$ordered' + pipeline: + - + $match: + $expr: + $and: + - + $eq: + - '$stock_item' + - '$$order_item' + - + $gte: + - '$instock' + - '$$order_qty' + - + $project: + stock_item: 0 + _id: 0 + as: 'stockdata' + - + name: 'Perform an Uncorrelated Subquery with $lookup' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#perform-an-uncorrelated-subquery-with--lookup' + pipeline: + - + $lookup: + from: 'holidays' + pipeline: + - + $match: + year: 2018 + - + $project: + _id: 0 + date: + name: '$name' + date: '$date' + - + $replaceRoot: + newRoot: '$date' + as: 'holidays' + - + name: 'Perform a Concise Correlated Subquery with $lookup' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/#perform-a-concise-correlated-subquery-with--lookup' + pipeline: + - + $lookup: + from: 'restaurants' + localField: 'restaurant_name' + foreignField: 'name' + let: + orders_drink: '$drink' + pipeline: + - + $match: + $expr: + $in: + - '$$orders_drink' + - '$beverages' + as: 'matches' diff --git a/generator/config/stage/match.yaml b/generator/config/stage/match.yaml new file mode 100644 index 000000000..ab0081fd0 --- /dev/null +++ b/generator/config/stage/match.yaml @@ -0,0 +1,40 @@ +# $schema: ../schema.json +name: $match +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/' +type: + - stage +encode: single +description: | + Filters the document stream to allow only matching documents to pass unmodified into the next pipeline stage. $match uses standard MongoDB queries. For each input document, outputs either one document (a match) or zero documents (no match). +arguments: + - + name: query + type: + - query +tests: + - + name: 'Equality Match' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/#equality-match' + pipeline: + - + $match: + author: 'dave' + - + name: 'Perform a Count' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/#perform-a-count' + pipeline: + - + $match: + $or: + - + score: + $gt: 70 + $lt: 90 + - + views: + $gte: 1000 + - + $group: + _id: ~ + count: + $sum: 1 diff --git a/generator/config/stage/merge.yaml b/generator/config/stage/merge.yaml new file mode 100644 index 000000000..2e24ec74e --- /dev/null +++ b/generator/config/stage/merge.yaml @@ -0,0 +1,180 @@ +# $schema: ../schema.json +name: $merge +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/' +type: + - stage +encode: object +description: | + Writes the resulting documents of the aggregation pipeline to a collection. The stage can incorporate (insert new documents, merge documents, replace documents, keep existing documents, fail the operation, process documents with a custom update pipeline) the results into an output collection. To use the $merge stage, it must be the last stage in the pipeline. + New in MongoDB 4.2. +arguments: + - + name: into + type: + - string + - object # OutCollection + description: | + The output collection. + - + name: 'on' + type: + - string + - array # of string + optional: true + description: | + Field or fields that act as a unique identifier for a document. The identifier determines if a results document matches an existing document in the output collection. + - + name: let + type: + - object + optional: true + description: | + Specifies variables for use in the whenMatched pipeline. + - + name: whenMatched + type: + - string # WhenMatched + - pipeline + optional: true + description: | + The behavior of $merge if a result document and an existing document in the collection have the same value for the specified on field(s). + - + name: whenNotMatched + type: + - string # WhenNotMatched + optional: true + description: | + The behavior of $merge if a result document does not match an existing document in the out collection. +tests: + - + name: 'On-Demand Materialized View Initial Creation' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/#on-demand-materialized-view--initial-creation' + pipeline: + - + $group: + _id: + fiscal_year: '$fiscal_year' + dept: '$dept' + salaries: + $sum: '$salary' + - + $merge: + into: + db: 'reporting' + coll: 'budgets' + on: '_id' + whenMatched: 'replace' + whenNotMatched: 'insert' + - + name: 'On-Demand Materialized View Update Replace Data' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/#on-demand-materialized-view--update-replace-data' + pipeline: + - + $match: + fiscal_year: + $gte: 2019 + - + $group: + _id: + fiscal_year: '$fiscal_year' + dept: '$dept' + salaries: + $sum: '$salary' + - + $merge: + into: + db: 'reporting' + coll: 'budgets' + on: '_id' + whenMatched: 'replace' + whenNotMatched: 'insert' + - + name: 'Only Insert New Data' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/#only-insert-new-data' + pipeline: + - + $match: + fiscal_year: 2019 + - + $group: + _id: + fiscal_year: '$fiscal_year' + dept: '$dept' + employees: + $push: '$employee' + - + $project: + _id: 0 + dept: '$_id.dept' + fiscal_year: '$_id.fiscal_year' + employees: 1 + - + $merge: + into: + db: 'reporting' + coll: 'orgArchive' + on: + - 'dept' + - 'fiscal_year' + whenMatched: 'fail' + - + name: 'Merge Results from Multiple Collections' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/#merge-results-from-multiple-collections' + pipeline: + - + $group: + _id: '$quarter' + purchased: + $sum: '$qty' + - + $merge: + into: 'quarterlyreport' + on: '_id' + whenMatched: 'merge' + whenNotMatched: 'insert' + - + name: 'Use the Pipeline to Customize the Merge' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/#use-the-pipeline-to-customize-the-merge' + pipeline: + - + $match: + date: + $gte: !bson_utcdatetime '2019-05-07' + $lt: !bson_utcdatetime '2019-05-08' + - + $project: + _id: + $dateToString: + format: '%Y-%m' + date: '$date' + thumbsup: 1 + thumbsdown: 1 + - + $merge: + into: 'monthlytotals' + on: '_id' + whenMatched: + - + $addFields: + thumbsup: + $add: + - '$thumbsup' + - '$$new.thumbsup' + thumbsdown: + $add: + - '$thumbsdown' + - '$$new.thumbsdown' + whenNotMatched: 'insert' + - + name: 'Use Variables to Customize the Merge' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/#use-variables-to-customize-the-merge' + pipeline: + - + $merge: + into: 'cakeSales' + let: + year: '2020' + whenMatched: + - + $addFields: + salesYear: '$$year' diff --git a/generator/config/stage/out.yaml b/generator/config/stage/out.yaml new file mode 100644 index 000000000..c4cc7948d --- /dev/null +++ b/generator/config/stage/out.yaml @@ -0,0 +1,40 @@ +# $schema: ../schema.json +name: $out +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/out/' +type: + - stage +encode: single +description: | + Writes the resulting documents of the aggregation pipeline to a collection. To use the $out stage, it must be the last stage in the pipeline. +arguments: + - name: coll + type: + - string + - object # OutCollection + description: | + Target database name to write documents from $out to. +tests: + - + name: 'Output to Same Database' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/out/#output-to-same-database' + pipeline: + - + $group: + _id: '$author' + books: + $push: '$title' + - + $out: 'authors' + - + name: 'Output to a Different Database' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/out/#output-to-a-different-database' + pipeline: + - + $group: + _id: '$author' + books: + $push: '$title' + - + $out: + db: 'reporting' + coll: 'authors' diff --git a/generator/config/stage/planCacheStats.yaml b/generator/config/stage/planCacheStats.yaml new file mode 100644 index 000000000..995caa74e --- /dev/null +++ b/generator/config/stage/planCacheStats.yaml @@ -0,0 +1,24 @@ +# $schema: ../schema.json +name: $planCacheStats +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/planCacheStats/' +type: + - stage +encode: object +description: | + Returns plan cache information for a collection. +tests: + - + name: 'Return Information for All Entries in the Query Cache' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/planCacheStats/#return-information-for-all-entries-in-the-query-cache' + pipeline: + - + $planCacheStats: {} + - + name: 'Find Cache Entry Details for a Query Hash' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/planCacheStats/#find-cache-entry-details-for-a-query-hash' + pipeline: + - + $planCacheStats: {} + - + $match: + planCacheKey: 'B1435201' diff --git a/generator/config/stage/project.yaml b/generator/config/stage/project.yaml new file mode 100644 index 000000000..c7b0f7d59 --- /dev/null +++ b/generator/config/stage/project.yaml @@ -0,0 +1,124 @@ +# $schema: ../schema.json +name: $project +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/' +type: + - stage +encode: single +description: | + Reshapes each document in the stream, such as by adding new fields or removing existing fields. For each input document, outputs one document. +arguments: + - + name: specification + type: + - expression + variadic: object +tests: + - + name: 'Include Specific Fields in Output Documents' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#include-specific-fields-in-output-documents' + pipeline: + - + $project: + title: 1 + author: 1 + - + name: 'Suppress id Field in the Output Documents' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#suppress-_id-field-in-the-output-documents' + pipeline: + - + $project: + _id: 0 + title: 1 + author: 1 + - + name: 'Exclude Fields from Output Documents' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#exclude-fields-from-output-documents' + pipeline: + - + $project: + lastModified: 0 + - + name: 'Exclude Fields from Embedded Documents' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#exclude-fields-from-embedded-documents' + pipeline: + - + $project: + author.first: 0 + lastModified: 0 + - + $project: + author: + first: 0 + lastModified: 0 + - + name: 'Conditionally Exclude Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#conditionally-exclude-fields' + pipeline: + - + $project: + title: 1 + author.first: 1 + author.last: 1 + author.middle: + $cond: + if: + $eq: + - '' + - '$author.middle' + then: '$$REMOVE' + else: '$author.middle' + - + name: 'Include Specific Fields from Embedded Documents' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#include-specific-fields-from-embedded-documents' + pipeline: + - + $project: + stop.title: 1 + - + $project: + stop: + title: 1 + - + name: 'Include Computed Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#include-computed-fields' + pipeline: + - + $project: + title: 1 + isbn: + prefix: + $substr: + - '$isbn' + - 0 + - 3 + group: + $substr: + - '$isbn' + - 3 + - 2 + publisher: + $substr: + - '$isbn' + - 5 + - 4 + title: + $substr: + - '$isbn' + - 9 + - 3 + checkDigit: + $substr: + - '$isbn' + - 12 + - 1 + lastName: '$author.last' + copiesSold: '$copies' + - + name: 'Project New Array Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#project-new-array-fields' + pipeline: + - + $project: + myArray: + - '$x' + - '$y' diff --git a/generator/config/stage/redact.yaml b/generator/config/stage/redact.yaml new file mode 100644 index 000000000..07698119c --- /dev/null +++ b/generator/config/stage/redact.yaml @@ -0,0 +1,52 @@ +# $schema: ../schema.json +name: $redact +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/redact/' +type: + - stage +encode: single +description: | + Reshapes each document in the stream by restricting the content for each document based on information stored in the documents themselves. Incorporates the functionality of $project and $match. Can be used to implement field level redaction. For each input document, outputs either one or zero documents. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Evaluate Access at Every Document Level' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/redact/#evaluate-access-at-every-document-level' + pipeline: + - + $match: + year: 2014 + - + $redact: + $cond: + if: + $gt: + - + $size: + $setIntersection: + - '$tags' + - + - 'STLW' + - 'G' + - 0 + then: '$$DESCEND' + else: '$$PRUNE' + - + name: 'Exclude All Fields at a Given Level' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/redact/#exclude-all-fields-at-a-given-level' + pipeline: + - + $match: + status: 'A' + - + $redact: + $cond: + if: + $eq: + - '$level' + - 5 + then: '$$PRUNE' + else: '$$DESCEND' diff --git a/generator/config/stage/replaceRoot.yaml b/generator/config/stage/replaceRoot.yaml new file mode 100644 index 000000000..4de474e00 --- /dev/null +++ b/generator/config/stage/replaceRoot.yaml @@ -0,0 +1,71 @@ +# $schema: ../schema.json +name: $replaceRoot +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceRoot/' +type: + - stage +encode: object +description: | + Replaces a document with the specified embedded document. The operation replaces all existing fields in the input document, including the _id field. Specify a document embedded in the input document to promote the embedded document to the top level. +arguments: + - + name: newRoot + type: + - resolvesToObject +tests: + - + name: 'with an Embedded Document Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceRoot/#-replaceroot-with-an-embedded-document-field' + pipeline: + - + $replaceRoot: + newRoot: + $mergeObjects: + - + dogs: 0 + cats: 0 + birds: 0 + fish: 0 + - '$pets' + - + name: 'with a Document Nested in an Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceRoot/#-replaceroot-with-a-document-nested-in-an-array' + pipeline: + - + # The builder uses the verbose form of the $unwind operator + # $unwind: '$grades' + $unwind: + path: '$grades' + - + $match: + grades.grade: + $gte: 90 + - + $replaceRoot: + newRoot: '$grades' + - + name: 'with a newly created document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceRoot/#-replaceroot-with-a-newly-created-document' + pipeline: + - + $replaceRoot: + newRoot: + full_name: + $concat: + - '$first_name' + - ' ' + - '$last_name' + - + name: 'with a New Document Created from $$ROOT and a Default Document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceRoot/#-replaceroot-with-a-new-document-created-from---root-and-a-default-document' + pipeline: + - + $replaceRoot: + newRoot: + $mergeObjects: + - + _id: '' + name: '' + email: '' + cell: '' + home: '' + - '$$ROOT' diff --git a/generator/config/stage/replaceWith.yaml b/generator/config/stage/replaceWith.yaml new file mode 100644 index 000000000..10c5fa3a2 --- /dev/null +++ b/generator/config/stage/replaceWith.yaml @@ -0,0 +1,74 @@ +# $schema: ../schema.json +name: $replaceWith +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceWith/' +type: + - stage +encode: single +description: | + Replaces a document with the specified embedded document. The operation replaces all existing fields in the input document, including the _id field. Specify a document embedded in the input document to promote the embedded document to the top level. + Alias for $replaceRoot. +arguments: + - + name: expression + type: + - resolvesToObject +tests: + - + name: 'an Embedded Document Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceWith/#-replacewith-an-embedded-document-field' + pipeline: + - + $replaceWith: + $mergeObjects: + - + dogs: 0 + cats: 0 + birds: 0 + fish: 0 + - '$pets' + - + name: 'a Document Nested in an Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceWith/#-replacewith-a-document-nested-in-an-array' + pipeline: + - + # The builder uses the verbose form of the $unwind operator + # $unwind: '$grades' + $unwind: + path: '$grades' + - + $match: + grades.grade: + $gte: 90 + - + $replaceWith: '$grades' + - + name: 'a Newly Created Document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceWith/#-replacewith-a-newly-created-document' + pipeline: + - + $match: + status: 'C' + - + $replaceWith: + _id: '$_id' + item: '$item' + amount: + $multiply: + - '$price' + - '$quantity' + status: 'Complete' + asofDate: '$$NOW' + - + name: 'a New Document Created from $$ROOT and a Default Document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceWith/#-replacewith-a-new-document-created-from---root-and-a-default-document' + pipeline: + - + $replaceWith: + $mergeObjects: + - + _id: '' + name: '' + email: '' + cell: '' + home: '' + - '$$ROOT' diff --git a/generator/config/stage/sample.yaml b/generator/config/stage/sample.yaml new file mode 100644 index 000000000..757382aaf --- /dev/null +++ b/generator/config/stage/sample.yaml @@ -0,0 +1,23 @@ +# $schema: ../schema.json +name: $sample +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sample/' +type: + - stage +encode: object +description: | + Randomly selects the specified number of documents from its input. +arguments: + - + name: size + type: + - int + description: | + The number of documents to randomly select. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sample/#example' + pipeline: + - + $sample: + size: 3 diff --git a/generator/config/stage/search.yaml b/generator/config/stage/search.yaml new file mode 100644 index 000000000..2531e75f6 --- /dev/null +++ b/generator/config/stage/search.yaml @@ -0,0 +1,41 @@ +# $schema: ../schema.json +name: $search +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/search/' +type: + - stage +encode: single +description: | + Performs a full-text search of the field or fields in an Atlas collection. + NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments. +arguments: + - + name: search + type: + - object + +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/#aggregation-variable' + pipeline: + - + $search: + near: + path: 'released' + origin: !bson_utcdatetime '2011-09-01T00:00:00.000+00:00' + pivot: 7776000000 + - + $project: + _id: 0 + title: 1 + released: 1 + - + $limit: 5 + - + $facet: + docs: [] + meta: + - + $replaceWith: '$$SEARCH_META' + - + $limit: 1 diff --git a/generator/config/stage/searchMeta.yaml b/generator/config/stage/searchMeta.yaml new file mode 100644 index 000000000..322d048eb --- /dev/null +++ b/generator/config/stage/searchMeta.yaml @@ -0,0 +1,28 @@ +# $schema: ../schema.json +name: $searchMeta +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/searchMeta/' +type: + - stage +encode: single +description: | + Returns different types of metadata result documents for the Atlas Search query against an Atlas collection. + NOTE: $searchMeta is only available for MongoDB Atlas clusters running MongoDB v4.4.9 or higher, and is not available for self-managed deployments. +arguments: + - + name: meta + type: + - object + +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/#example' + pipeline: + - + $searchMeta: + range: + path: 'year' + gte: 1998 + lt: 1999 + count: + type: 'total' diff --git a/generator/config/stage/set.yaml b/generator/config/stage/set.yaml new file mode 100644 index 000000000..a5861aa29 --- /dev/null +++ b/generator/config/stage/set.yaml @@ -0,0 +1,73 @@ +# $schema: ../schema.json +name: $set +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/set/' +type: + - stage +encode: single +description: | + Adds new fields to documents. Outputs documents that contain all existing fields from the input documents and newly added fields. + Alias for $addFields. +arguments: + - + name: field + type: + - expression + variadic: object +tests: + - + name: 'Using Two $set Stages' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/set/#using-two--set-stages' + pipeline: + - + $set: + totalHomework: + # The $sum expression is always build as an array, even if the value is an array field name + # $sum: '$homework' + $sum: ['$homework'] + totalQuiz: + # $sum: '$quiz' + $sum: ['$quiz'] + - + $set: + totalScore: + $add: + - '$totalHomework' + - '$totalQuiz' + - '$extraCredit' + - + name: 'Adding Fields to an Embedded Document' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/set/#adding-fields-to-an-embedded-document' + pipeline: + - + $set: + specs.fuel_type: 'unleaded' + - + name: 'Overwriting an existing field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/set/#overwriting-an-existing-field' + pipeline: + - + $set: + cats: 20 + - + name: 'Add Element to an Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/set/#add-element-to-an-array' + pipeline: + - + $match: + _id: 1 + - + $set: + homework: + $concatArrays: + - '$homework' + - + - 7 + - + name: 'Creating a New Field with Existing Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/set/#creating-a-new-field-with-existing-fields' + pipeline: + - + $set: + quizAverage: + # $avg: '$quiz' + $avg: ['$quiz'] diff --git a/generator/config/stage/setWindowFields.yaml b/generator/config/stage/setWindowFields.yaml new file mode 100644 index 000000000..fba751d03 --- /dev/null +++ b/generator/config/stage/setWindowFields.yaml @@ -0,0 +1,144 @@ +# $schema: ../schema.json +name: $setWindowFields +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/' +type: + - stage +encode: object +description: | + Groups documents into windows and applies one or more operators to the documents in each window. + New in MongoDB 5.0. +arguments: + - + name: sortBy + type: + - object # SortSpec + description: | + Specifies the field(s) to sort the documents by in the partition. Uses the same syntax as the $sort stage. Default is no sorting. + - + name: output + type: + - object + description: | + Specifies the field(s) to append to the documents in the output returned by the $setWindowFields stage. Each field is set to the result returned by the window operator. + A field can contain dots to specify embedded document fields and array fields. The semantics for the embedded document dotted notation in the $setWindowFields stage are the same as the $addFields and $set stages. + - + name: partitionBy + type: + - expression + description: | + Specifies an expression to group the documents. In the $setWindowFields stage, the group of documents is known as a partition. Default is one partition for the entire collection. + optional: true +tests: + - + name: 'Use Documents Window to Obtain Cumulative Quantity for Each State' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/#use-documents-window-to-obtain-cumulative-quantity-for-each-state' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + cumulativeQuantityForState: + $sum: '$quantity' + window: + documents: ['unbounded', 'current'] + - + name: 'Use Documents Window to Obtain Cumulative Quantity for Each Year' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/#use-documents-window-to-obtain-cumulative-quantity-for-each-year' + pipeline: + - + $setWindowFields: + partitionBy: + # $year: '$orderDate' + $year: + date: '$orderDate' + sortBy: + orderDate: 1 + output: + cumulativeQuantityForYear: + $sum: '$quantity' + window: + documents: ['unbounded', 'current'] + - + name: 'Use Documents Window to Obtain Moving Average Quantity for Each Year' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/#use-documents-window-to-obtain-moving-average-quantity-for-each-year' + pipeline: + - + $setWindowFields: + partitionBy: + # $year: '$orderDate' + $year: + date: '$orderDate' + sortBy: + orderDate: 1 + output: + averageQuantity: + $avg: '$quantity' + window: + documents: [-1, 0] + - + name: 'Use Documents Window to Obtain Cumulative and Maximum Quantity for Each Year' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/#use-documents-window-to-obtain-cumulative-and-maximum-quantity-for-each-year' + pipeline: + - + $setWindowFields: + partitionBy: + # $year: '$orderDate' + $year: + date: '$orderDate' + sortBy: + orderDate: 1 + output: + cumulativeQuantityForYear: + $sum: '$quantity' + window: + documents: ['unbounded', 'current'] + maximumQuantityForYear: + $max: '$quantity' + window: + documents: ['unbounded', 'unbounded'] + - + name: 'Range Window Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/#range-window-example' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + price: 1 + output: + quantityFromSimilarOrders: + $sum: '$quantity' + window: + range: [-10, 10] + - + name: 'Use a Time Range Window with a Positive Upper Bound' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/#use-a-time-range-window-with-a-positive-upper-bound' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + recentOrders: + $push: '$orderDate' + window: + range: ['unbounded', 10] + unit: 'month' + - + name: 'Use a Time Range Window with a Negative Upper Bound' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/#use-a-time-range-window-with-a-negative-upper-bound' + pipeline: + - + $setWindowFields: + partitionBy: '$state' + sortBy: + orderDate: 1 + output: + recentOrders: + $push: '$orderDate' + window: + range: ['unbounded', -10] + unit: 'month' diff --git a/generator/config/stage/shardedDataDistribution.yaml b/generator/config/stage/shardedDataDistribution.yaml new file mode 100644 index 000000000..2f298ca0f --- /dev/null +++ b/generator/config/stage/shardedDataDistribution.yaml @@ -0,0 +1,16 @@ +# $schema: ../schema.json +name: $shardedDataDistribution +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/shardedDataDistribution/' +type: + - stage +encode: object +description: | + Provides data and size distribution information on sharded collections. + New in MongoDB 6.0.3. +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/shardedDataDistribution/#examples' + pipeline: + - + $shardedDataDistribution: {} diff --git a/generator/config/stage/skip.yaml b/generator/config/stage/skip.yaml new file mode 100644 index 000000000..2128fe226 --- /dev/null +++ b/generator/config/stage/skip.yaml @@ -0,0 +1,20 @@ +# $schema: ../schema.json +name: $skip +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/skip/' +type: + - stage +encode: single +description: | + Skips the first n documents where n is the specified skip number and passes the remaining documents unmodified to the pipeline. For each input document, outputs either zero documents (for the first n documents) or one document (if after the first n documents). +arguments: + - + name: skip + type: + - int +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/skip/#example' + pipeline: + - + $skip: 5 diff --git a/generator/config/stage/sort.yaml b/generator/config/stage/sort.yaml new file mode 100644 index 000000000..d35e23b63 --- /dev/null +++ b/generator/config/stage/sort.yaml @@ -0,0 +1,37 @@ +# $schema: ../schema.json +name: $sort +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/' +type: + - stage +encode: single +description: | + Reorders the document stream by a specified sort key. Only the order changes; the documents remain unmodified. For each input document, outputs one document. +arguments: + - + name: sort + type: + - expression + - sortSpec + variadic: object +tests: + - + name: 'Ascending Descending Sort' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/#ascending-descending-sort' + pipeline: + - + $sort: + age: -1 + posts: 1 + - + name: 'Text Score Metadata Sort' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/#text-score-metadata-sort' + pipeline: + - + $match: + $text: + $search: 'operating' + - + $sort: + score: + $meta: 'textScore' + posts: -1 diff --git a/generator/config/stage/sortByCount.yaml b/generator/config/stage/sortByCount.yaml new file mode 100644 index 000000000..a32d7aff4 --- /dev/null +++ b/generator/config/stage/sortByCount.yaml @@ -0,0 +1,25 @@ +# $schema: ../schema.json +name: $sortByCount +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortByCount/' +type: + - stage +encode: single +description: | + Groups incoming documents based on the value of a specified expression, then computes the count of documents in each distinct group. +arguments: + - + name: expression + type: + - expression +tests: + - + name: 'Example' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortByCount/#example' + pipeline: + - + # The builder uses the verbose form of the $unwind operator + # $unwind: '$tags' + $unwind: + path: '$tags' + - + $sortByCount: '$tags' diff --git a/generator/config/stage/unionWith.yaml b/generator/config/stage/unionWith.yaml new file mode 100644 index 000000000..eafa44110 --- /dev/null +++ b/generator/config/stage/unionWith.yaml @@ -0,0 +1,83 @@ +# $schema: ../schema.json +name: $unionWith +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/' +type: + - stage +encode: object +description: | + Performs a union of two collections; i.e. combines pipeline results from two collections into a single result set. + New in MongoDB 4.4. +arguments: + - + name: coll + type: + - string + description: | + The collection or view whose pipeline results you wish to include in the result set. + - + name: pipeline + type: + - pipeline + optional: true + description: | + An aggregation pipeline to apply to the specified coll. + The pipeline cannot include the $out and $merge stages. Starting in v6.0, the pipeline can contain the Atlas Search $search stage as the first stage inside the pipeline. +tests: + - + name: 'Report 1 All Sales by Year and Stores and Items' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/#report-1--all-sales-by-year-and-stores-and-items' + pipeline: + - + $set: + _id: '2017' + - + $unionWith: + coll: 'sales_2018' + pipeline: + - + $set: + _id: '2018' + - + $unionWith: + coll: 'sales_2019' + pipeline: + - + $set: + _id: '2019' + - + $unionWith: + coll: 'sales_2020' + pipeline: + - + $set: + _id: '2020' + - + $sort: + _id: 1 + store: 1 + item: 1 + - + name: 'Report 2 Aggregated Sales by Items' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/#report-2--aggregated-sales-by-items' + pipeline: + - + # Example uses the short form, the builder always generates the verbose form + # $unionWith: 'sales_2018' + $unionWith: + coll: 'sales_2018' + - + # $unionWith: 'sales_2019' + $unionWith: + coll: 'sales_2019' + - + # $unionWith: 'sales_2020' + $unionWith: + coll: 'sales_2020' + - + $group: + _id: '$item' + total: + $sum: '$quantity' + - + $sort: + total: -1 diff --git a/generator/config/stage/unset.yaml b/generator/config/stage/unset.yaml new file mode 100644 index 000000000..cef9cdd6d --- /dev/null +++ b/generator/config/stage/unset.yaml @@ -0,0 +1,42 @@ +# $schema: ../schema.json +name: $unset +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unset/' +type: + - stage +encode: single +description: | + Removes or excludes fields from documents. + Alias for $project stage that removes or excludes fields. +arguments: + - + name: field + type: + - fieldPath + variadic: array +tests: + - + name: 'Remove a Single Field' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unset/#remove-a-single-field' + pipeline: + - + # The example in the docs uses the short syntax whereas + # the aggregation builder always uses the equivalent array syntax. + # $unset: 'copies' + $unset: ['copies'] + - + name: 'Remove Top-Level Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unset/#remove-top-level-fields' + pipeline: + - + $unset: + - 'isbn' + - 'copies' + - + name: 'Remove Embedded Fields' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unset/#remove-embedded-fields' + pipeline: + - + $unset: + - 'isbn' + - 'author.first' + - 'copies.warehouse' diff --git a/generator/config/stage/unwind.yaml b/generator/config/stage/unwind.yaml new file mode 100644 index 000000000..a1f93edbc --- /dev/null +++ b/generator/config/stage/unwind.yaml @@ -0,0 +1,95 @@ +# $schema: ../schema.json +name: $unwind +link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/' +type: + - stage +encode: object +description: | + Deconstructs an array field from the input documents to output a document for each element. Each output document replaces the array with an element value. For each input document, outputs n documents where n is the number of array elements and can be zero for an empty array. +arguments: + - + name: path + type: + - arrayFieldPath + description: | + Field path to an array field. + - + name: includeArrayIndex + type: + - string + optional: true + description: | + The name of a new field to hold the array index of the element. The name cannot start with a dollar sign $. + - + name: preserveNullAndEmptyArrays + type: + - bool + optional: true + description: | + If true, if the path is null, missing, or an empty array, $unwind outputs the document. + If false, if path is null, missing, or an empty array, $unwind does not output a document. + The default value is false. +tests: + - + name: 'Unwind Array' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/#unwind-array' + pipeline: + - + # Example uses the short form, the builder always generates the verbose form + # $unwind: '$sizes' + $unwind: + path: '$sizes' + - + name: 'preserveNullAndEmptyArrays' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/#preservenullandemptyarrays' + pipeline: + - + $unwind: + path: '$sizes' + preserveNullAndEmptyArrays: true + - + name: 'includeArrayIndex' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/#includearrayindex' + pipeline: + - + $unwind: + path: '$sizes' + includeArrayIndex: 'arrayIndex' + - + name: 'Group by Unwound Values' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/#group-by-unwound-values' + pipeline: + - + $unwind: + path: '$sizes' + preserveNullAndEmptyArrays: true + - + $group: + _id: '$sizes' + averagePrice: + $avg: '$price' + - + $sort: + averagePrice: -1 + - + name: 'Unwind Embedded Arrays' + link: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/#unwind-embedded-arrays' + pipeline: + - + # Example uses the short form, the builder always generates the verbose form + # $unwind: '$items' + $unwind: + path: '$items' + - + # Example uses the short form, the builder always generates the verbose form + # $unwind: '$items.tags' + $unwind: + path: '$items.tags' + - + $group: + _id: '$items.tags' + totalSalesAmount: + $sum: + $multiply: + - '$items.price' + - '$items.quantity' diff --git a/generator/generate b/generator/generate new file mode 100755 index 000000000..d67515b70 --- /dev/null +++ b/generator/generate @@ -0,0 +1,17 @@ +#!/usr/bin/env php +add(new GenerateCommand(__DIR__ . '/../', __DIR__ . '/config')); +$application->setDefaultCommand('generate'); +$application->run(); diff --git a/generator/js2yaml.html b/generator/js2yaml.html new file mode 100644 index 000000000..a0f8654af --- /dev/null +++ b/generator/js2yaml.html @@ -0,0 +1,163 @@ + + +

Convert JS examples into Yaml

+ +
+ + +
+ +
+ + +
+ + + diff --git a/generator/src/AbstractGenerator.php b/generator/src/AbstractGenerator.php new file mode 100644 index 000000000..b0fef69aa --- /dev/null +++ b/generator/src/AbstractGenerator.php @@ -0,0 +1,111 @@ +printer = new PsrPrinter(); + } + + /** + * Split the namespace and class name from a fully qualified class name. + * + * @return array{0: string, 1: string} + */ + final protected function splitNamespaceAndClassName(string $fqcn): array + { + $parts = explode('\\', ltrim($fqcn, '\\')); + $className = array_pop($parts); + + return [implode('\\', $parts), $className]; + } + + final protected function writeFile(PhpNamespace $namespace, bool $autoGeneratedWarning = true): void + { + $classes = $namespace->getClasses(); + assert(count($classes) === 1, sprintf('Expected exactly one class in namespace "%s", got %d.', $namespace->getName(), count($classes))); + + $filename = $this->rootDir . $this->getFileName($namespace->getName(), current($classes)->getName()); + + $dirname = dirname($filename); + if (! is_dir($dirname)) { + mkdir($dirname, 0755, true); + } + + $file = new PhpFile(); + $file->setStrictTypes(); + if ($autoGeneratedWarning) { + $file->setComment('THIS FILE IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!'); + } + + $file->addNamespace($namespace); + + file_put_contents($filename, $this->printer->printFile($file)); + } + + final protected function readFile(string ...$fqcn): PhpFile|null + { + $filename = $this->rootDir . $this->getFileName(...$fqcn); + + if (! is_file($filename)) { + return null; + } + + return PhpFile::fromCode(file_get_contents($filename)); + } + + /** + * Thanks to PSR-4, the file name can be determined from the fully qualified class name. + * + * @param string ...$fqcn Fully qualified class name, merged if multiple parts + * + * @return string File name relative to the root directory + */ + private function getFileName(string ...$fqcn): string + { + $fqcn = implode('\\', $fqcn); + + // Config from composer.json autoload + $config = [ + 'MongoDB\\Tests\\' => 'tests/', + 'MongoDB\\' => 'src/', + ]; + foreach ($config as $namespace => $directory) { + if (str_starts_with($fqcn, $namespace)) { + return $directory . str_replace([$namespace, '\\'], ['', '/'], $fqcn) . '.php'; + } + } + + throw new InvalidArgumentException(sprintf('Could not determine file name for "%s"', $fqcn)); + } +} diff --git a/generator/src/Command/GenerateCommand.php b/generator/src/Command/GenerateCommand.php new file mode 100644 index 000000000..78482963e --- /dev/null +++ b/generator/src/Command/GenerateCommand.php @@ -0,0 +1,90 @@ +setName('generate'); + $this->setDescription('Generate code for mongodb/mongodb library'); + $this->setHelp('Generate code for mongodb/mongodb library'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln('Generating code for mongodb/mongodb library'); + + $expressions = $this->generateExpressionClasses($output); + $this->generateOperatorClasses($expressions, $output); + + return Command::SUCCESS; + } + + /** @return array */ + private function generateExpressionClasses(OutputInterface $output): array + { + $output->writeln('Generating expression classes'); + + $config = require $this->configDir . '/expressions.php'; + assert(is_array($config)); + + $definitions = []; + $generator = new ExpressionClassGenerator($this->rootDir); + foreach ($config as $name => $def) { + assert(is_array($def)); + assert(! array_key_exists($name, $definitions), sprintf('Duplicate expression name "%s".', $name)); + $definitions[$name] = $def = new ExpressionDefinition($name, ...$def); + $generator->generate($def); + } + + $generator = new ExpressionFactoryGenerator($this->rootDir); + $generator->generate($definitions); + + return $definitions; + } + + /** @param array $expressions */ + private function generateOperatorClasses(array $expressions, OutputInterface $output): void + { + $config = require $this->configDir . '/definitions.php'; + assert(is_array($config)); + + foreach ($config as $def) { + assert(is_array($def)); + $definition = new GeneratorDefinition(...$def); + + foreach ($definition->generators as $generatorClass) { + $output->writeln(sprintf('Generating classes for %s with %s', basename($definition->configFiles), $generatorClass)); + assert(is_a($generatorClass, OperatorGenerator::class, true)); + $generator = new $generatorClass($this->rootDir, $expressions); + $generator->generate($definition); + } + } + } +} diff --git a/generator/src/Definition/ArgumentDefinition.php b/generator/src/Definition/ArgumentDefinition.php new file mode 100644 index 000000000..e45ea0da3 --- /dev/null +++ b/generator/src/Definition/ArgumentDefinition.php @@ -0,0 +1,49 @@ + */ + public array $type, + public string|null $description = null, + public bool $optional = false, + string|null $variadic = null, + int|null $variadicMin = null, + public mixed $default = null, + ) { + assert($this->optional === false || $this->default === null, 'Optional arguments cannot have a default value'); + if (is_array($type)) { + assert(array_is_list($type), 'Type must be a list or a single string'); + foreach ($type as $t) { + assert(is_string($t), sprintf('Type must be a list of strings. Got %s', get_debug_type($type))); + } + } + + if ($variadic) { + $this->variadic = VariadicType::from($variadic); + if ($variadicMin === null) { + $this->variadicMin = $optional ? 0 : 1; + } else { + $this->variadicMin = $variadicMin; + } + } else { + $this->variadic = null; + $this->variadicMin = null; + } + } +} diff --git a/generator/src/Definition/ExpressionDefinition.php b/generator/src/Definition/ExpressionDefinition.php new file mode 100644 index 000000000..cbe37febc --- /dev/null +++ b/generator/src/Definition/ExpressionDefinition.php @@ -0,0 +1,37 @@ + */ + public array $acceptedTypes, + /** Interface to implement for operators that resolve to this type. Generated class/enum/interface. */ + public string|null $returnType = null, + public string|null $extends = null, + /** @var list */ + public array $implements = [], + public array $values = [], + public PhpObject|null $generate = null, + ) { + assert($generate === PhpObject::PhpClass || ! $extends, $name . ': Cannot specify "extends" when "generate" is not "class"'); + assert($generate === PhpObject::PhpEnum || ! $this->values, $name . ': Cannot specify "values" when "generate" is not "enum"'); + //assert($returnType === null || interface_exists($returnType), $name . ': Return type must be an interface'); + + foreach ($acceptedTypes as $acceptedType) { + assert(is_string($acceptedType), $name . ': AcceptedTypes must be an array of strings.'); + } + + if ($generate) { + $this->returnType = 'MongoDB\\Builder\\Expression\\' . ucfirst($this->name); + } + } +} diff --git a/generator/src/Definition/GeneratorDefinition.php b/generator/src/Definition/GeneratorDefinition.php new file mode 100644 index 000000000..2faa2fac3 --- /dev/null +++ b/generator/src/Definition/GeneratorDefinition.php @@ -0,0 +1,42 @@ +> */ + public array $generators, + public string $namespace, + public string $classNameSuffix = '', + public array $interfaces = [], + public string|null $parentClass = null, + ) { + assert(str_starts_with($namespace, 'MongoDB\\'), sprintf('Namespace must start with "MongoDB\\". Got "%s"', $namespace)); + assert(! str_ends_with($namespace, '\\'), sprintf('Namespace must not end with "\\". Got "%s"', $namespace)); + + assert(array_is_list($interfaces), 'Generators must be a list of class names'); + foreach ($interfaces as $interface) { + assert(is_string($interface) && class_exists($interface), sprintf('Interface "%s" does not exist', $interface)); + } + + assert(array_is_list($generators), 'Generators must be a list of class names'); + foreach ($generators as $class) { + assert(is_string($class) && is_subclass_of($class, OperatorGenerator::class), sprintf('Generator class "%s" must extend "%s"', $class, OperatorGenerator::class)); + } + } +} diff --git a/generator/src/Definition/OperatorDefinition.php b/generator/src/Definition/OperatorDefinition.php new file mode 100644 index 000000000..ce39fe4d7 --- /dev/null +++ b/generator/src/Definition/OperatorDefinition.php @@ -0,0 +1,73 @@ + */ + public readonly array $arguments; + + /** @var list */ + public readonly array $tests; + + public function __construct( + public string $name, + public string $link, + string $encode, + /** @var list */ + public array $type, + public string|null $description = null, + array $arguments = [], + array $tests = [], + ) { + $this->encode = match ($encode) { + 'single' => Encode::Single, + 'array' => Encode::Array, + 'object' => Encode::Object, + 'flat_object' => Encode::FlatObject, + 'dollar_object' => Encode::DollarObject, + 'group' => Encode::Group, + default => throw new UnexpectedValueException(sprintf('Unexpected "encode" value for operator "%s". Got "%s"', $name, $encode)), + }; + + // Convert arguments to ArgumentDefinition objects + // Optional arguments must be after required arguments + $requiredArgs = $optionalArgs = []; + foreach ($arguments as $arg) { + $arg = new ArgumentDefinition(...get_object_vars($arg)); + if ($arg->optional) { + $optionalArgs[] = $arg; + } else { + $requiredArgs[] = $arg; + } + } + + // "single" encode operators must have one required argument + if ($this->encode === Encode::Single) { + assert(count($requiredArgs) === 1, sprintf('Single encode operator "%s" must have one argument', $name)); + assert(count($optionalArgs) === 0, sprintf('Single encode operator "%s" argument cannot be optional', $name)); + } + + $this->arguments = array_merge($requiredArgs, $optionalArgs); + + $this->tests = array_map( + static fn (object $test): TestDefinition => new TestDefinition(...get_object_vars($test)), + array_values($tests), + ); + } +} diff --git a/generator/src/Definition/PhpObject.php b/generator/src/Definition/PhpObject.php new file mode 100644 index 000000000..5e815e0b6 --- /dev/null +++ b/generator/src/Definition/PhpObject.php @@ -0,0 +1,15 @@ + */ + public array $pipeline, + public string|null $link = null, + ) { + assert(array_is_list($pipeline), sprintf('Argument "%s" pipeline must be a list', $name)); + } +} diff --git a/generator/src/Definition/VariadicType.php b/generator/src/Definition/VariadicType.php new file mode 100644 index 000000000..091f654c9 --- /dev/null +++ b/generator/src/Definition/VariadicType.php @@ -0,0 +1,11 @@ + */ + public function read(string $dirname): array + { + $finder = new Finder(); + $finder->files()->in($dirname)->name('*.yaml')->sortByName(); + + $definitions = []; + foreach ($finder as $file) { + $operator = Yaml::parseFile( + $file->getPathname(), + Yaml::PARSE_OBJECT | Yaml::PARSE_OBJECT_FOR_MAP | Yaml::PARSE_CUSTOM_TAGS, + ); + $definitions[] = new OperatorDefinition(...get_object_vars($operator)); + } + + return $definitions; + } +} diff --git a/generator/src/ExpressionClassGenerator.php b/generator/src/ExpressionClassGenerator.php new file mode 100644 index 000000000..fc9119364 --- /dev/null +++ b/generator/src/ExpressionClassGenerator.php @@ -0,0 +1,96 @@ +generate) { + return; + } + + try { + $this->writeFile($this->createClassOrInterface($definition)); + } catch (Throwable $e) { + throw new RuntimeException('Failed to generate expression class for ' . $definition->name, 0, $e); + } + } + + public function createClassOrInterface(ExpressionDefinition $definition): PhpNamespace + { + [$namespace, $className] = $this->splitNamespaceAndClassName($definition->returnType); + $namespace = new PhpNamespace($namespace); + foreach ($definition->implements as $interface) { + $namespace->addUse($interface); + } + + $types = array_map( + fn (string $type): string => match ($type) { + 'list' => 'array', + default => $type, + }, + $definition->acceptedTypes, + ); + + if ($definition->generate === PhpObject::PhpClass) { + $class = $namespace->addClass($className); + $class->setImplements($definition->implements); + $class->setExtends($definition->extends); + + // Replace with promoted property in PHP 8.1 + $propertyType = Type::union(...$types); + $class->addProperty('name') + ->setType($propertyType) + ->setReadOnly() + ->setPublic(); + + $constructor = $class->addMethod('__construct'); + $constructor->addParameter('name')->setType($propertyType); + + $namespace->addUse(InvalidArgumentException::class); + $namespace->addUseFunction('sprintf'); + $namespace->addUseFunction('str_starts_with'); + $constructor->addBody(<<addBody('$this->name = $name;'); + } elseif ($definition->generate === PhpObject::PhpInterface) { + $interface = $namespace->addInterface($className); + $interface->setExtends($definition->implements); + } elseif ($definition->generate === PhpObject::PhpEnum) { + $enum = $namespace->addEnum($className); + $enum->setType('string'); + array_map( + fn (string $case) => $enum->addCase(ucfirst($case), $case), + $definition->values, + ); + } else { + throw new LogicException('Unknown generate type: ' . var_export($definition->generate, true)); + } + + return $namespace; + } +} diff --git a/generator/src/ExpressionFactoryGenerator.php b/generator/src/ExpressionFactoryGenerator.php new file mode 100644 index 000000000..b1e84c5dd --- /dev/null +++ b/generator/src/ExpressionFactoryGenerator.php @@ -0,0 +1,49 @@ + $expressions */ + public function generate(array $expressions): void + { + $this->writeFile($this->createFactoryClass($expressions)); + } + + /** @param array $expressions */ + private function createFactoryClass(array $expressions): PhpNamespace + { + $namespace = new PhpNamespace('MongoDB\\Builder\\Expression'); + $trait = $namespace->addTrait('ExpressionFactoryTrait'); + $trait->addComment('@internal'); + + // Pedantry requires methods to be ordered alphabetically + usort($expressions, fn (ExpressionDefinition $a, ExpressionDefinition $b) => $a->name <=> $b->name); + + foreach ($expressions as $expression) { + if ($expression->generate !== PhpObject::PhpClass) { + continue; + } + + $namespace->addUse($expression->returnType); + $expressionShortClassName = $this->splitNamespaceAndClassName($expression->returnType)[1]; + + $method = $trait->addMethod(lcfirst($expressionShortClassName)); + $method->setStatic(); + $method->addParameter('name')->setType('string'); + $method->addBody('return new ' . $expressionShortClassName . '($name);'); + $method->setReturnType($expression->returnType); + } + + return $namespace; + } +} diff --git a/generator/src/FluentStageFactoryGenerator.php b/generator/src/FluentStageFactoryGenerator.php new file mode 100644 index 000000000..ff7010f15 --- /dev/null +++ b/generator/src/FluentStageFactoryGenerator.php @@ -0,0 +1,134 @@ +writeFile($this->createFluentFactoryTrait($definition)); + } + + private function createFluentFactoryTrait(GeneratorDefinition $definition): PhpNamespace + { + $namespace = new PhpNamespace($definition->namespace); + $trait = $namespace->addTrait('FluentFactoryTrait'); + + $namespace->addUse(self::FACTORY_CLASS); + $namespace->addUse(StageInterface::class); + $namespace->addUse(Pipeline::class); + $namespace->addUse(stdClass::class); + + $trait->addProperty('pipeline') + ->setType('array') + ->setComment('@var list|stdClass>') + ->setValue([]); + $trait->addMethod('getPipeline') + ->setReturnType(Pipeline::class) + ->setBody(<<<'PHP' + return new Pipeline(...$this->pipeline); + PHP); + + $this->addUsesFrom(self::FACTORY_CLASS, $namespace); + $staticFactory = ClassType::from(self::FACTORY_CLASS); + assert($staticFactory instanceof ClassType); + + // Import the methods customized in the factory class + foreach ($staticFactory->getMethods() as $method) { + $this->addMethod($method, $trait); + } + + // Import the other methods provided by the generated trait + foreach ($staticFactory->getTraits() as $usedTrait) { + $this->addUsesFrom($usedTrait->getName(), $namespace); + $staticFactory = TraitType::from($usedTrait->getName()); + assert($staticFactory instanceof TraitType); + foreach ($staticFactory->getMethods() as $method) { + $this->addMethod($method, $trait); + } + } + + return $namespace; + } + + private function addMethod(Method $factoryMethod, TraitType $trait): void + { + // Non-public methods are not part of the API + if (! $factoryMethod->isPublic()) { + return; + } + + // Some methods can be overridden in the class, so we skip them + // when importing the methods provided by the trait. + if ($trait->hasMethod($factoryMethod->getName())) { + return; + } + + $method = $trait->addMethod($factoryMethod->getName()); + + $method->setComment($factoryMethod->getComment()); + $method->setParameters($factoryMethod->getParameters()); + + $args = array_map( + fn (Parameter $param): string => '$' . $param->getName(), + $factoryMethod->getParameters(), + ); + + if ($factoryMethod->isVariadic()) { + $method->setVariadic(); + $args[array_key_last($args)] = '...' . $args[array_key_last($args)]; + } + + $method->setReturnType('static'); + $method->setBody(sprintf( + <<<'PHP' + $this->pipeline[] = %s::%s(%s); + + return $this; + PHP, + (new ReflectionClass(self::FACTORY_CLASS))->getShortName(), + $factoryMethod->getName(), + implode(', ', $args), + )); + } + + private static function addUsesFrom(string $classLike, PhpNamespace $namespace): void + { + $file = PhpFile::fromCode(file_get_contents((new ReflectionClass($classLike))->getFileName())); + + foreach ($file->getNamespaces() as $ns) { + foreach ($ns->getUses() as $use) { + $namespace->addUse($use); + } + } + } +} diff --git a/generator/src/OperatorClassGenerator.php b/generator/src/OperatorClassGenerator.php new file mode 100644 index 000000000..73fcf09bf --- /dev/null +++ b/generator/src/OperatorClassGenerator.php @@ -0,0 +1,196 @@ +getOperators($definition) as $operator) { + try { + $this->writeFile($this->createClass($definition, $operator)); + } catch (Throwable $e) { + throw new RuntimeException(sprintf('Failed to generate class for operator "%s"', $operator->name), 0, $e); + } + } + } + + public function createClass(GeneratorDefinition $definition, OperatorDefinition $operator): PhpNamespace + { + $namespace = new PhpNamespace($definition->namespace); + + $interfaces = $this->getInterfaces($operator); + foreach ($interfaces as $interface) { + $namespace->addUse($interface); + } + + $class = $namespace->addClass($this->getOperatorClassName($definition, $operator)); + $class->setImplements($interfaces); + $namespace->addUse(OperatorInterface::class); + $class->addImplement(OperatorInterface::class); + + // Expose operator metadata as constants + // @todo move to encoder class + $class->addComment($operator->description); + $class->addComment('@see ' . $operator->link); + $namespace->addUse(Encode::class); + $class->addConstant('ENCODE', new Literal('Encode::' . $operator->encode->name)); + + $constuctor = $class->addMethod('__construct'); + foreach ($operator->arguments as $argument) { + $type = $this->getAcceptedTypes($argument); + foreach ($type->use as $use) { + $namespace->addUse($use); + } + + $property = $class->addProperty($argument->name); + $property->setReadOnly(); + $constuctorParam = $constuctor->addParameter($argument->name); + $constuctorParam->setType($type->native); + + if ($argument->variadic) { + $constuctor->setVariadic(); + $constuctor->addComment('@param ' . $type->doc . ' ...$' . $argument->name . rtrim(' ' . $argument->description)); + + if ($argument->variadicMin > 0) { + $namespace->addUse(InvalidArgumentException::class); + $constuctor->addBody(<<name}) < {$argument->variadicMin}) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for \${$argument->name}, got %d.', {$argument->variadicMin}, \count(\${$argument->name}))); + } + + PHP); + } + + if ($argument->variadic === VariadicType::Array) { + $property->setType('array'); + $property->addComment('@var list<' . $type->doc . '> $' . $argument->name . rtrim(' ' . $argument->description)); + // Warn that named arguments are not supported + // @see https://psalm.dev/docs/running_psalm/issues/NamedArgumentNotAllowed/ + $constuctor->addComment('@no-named-arguments'); + $namespace->addUseFunction('array_is_list'); + $namespace->addUse(InvalidArgumentException::class); + $constuctor->addBody(<<name})) { + throw new InvalidArgumentException('Expected \${$argument->name} arguments to be a list (array), named arguments are not supported'); + } + + PHP); + } elseif ($argument->variadic === VariadicType::Object) { + $namespace->addUse(stdClass::class); + $property->setType(stdClass::class); + $property->addComment('@var stdClass<' . $type->doc . '> $' . $argument->name . rtrim(' ' . $argument->description)); + $namespace->addUseFunction('is_string'); + $namespace->addUse(InvalidArgumentException::class); + $constuctor->addBody(<<name} as \$key => \$value) { + if (! is_string(\$key)) { + throw new InvalidArgumentException('Expected \${$argument->name} arguments to be a map (object), named arguments (:) or array unpacking ...[\'\' => ] must be used'); + } + } + + \${$argument->name} = (object) \${$argument->name}; + PHP); + } + } else { + // Non-variadic arguments + $property->addComment('@var ' . $type->doc . ' $' . $argument->name . rtrim(' ' . $argument->description)); + $property->setType($type->native); + $constuctor->addComment('@param ' . $type->doc . ' $' . $argument->name . rtrim(' ' . $argument->description)); + + if ($argument->optional) { + // We use a special Optional::Undefined type to differentiate between null and undefined + $constuctorParam->setDefaultValue(new Literal('Optional::Undefined')); + } elseif ($argument->default !== null) { + $constuctorParam->setDefaultValue($argument->default); + } + + // List type must be validated with array_is_list() + if ($type->list) { + $namespace->addUseFunction('is_array'); + $namespace->addUseFunction('array_is_list'); + $namespace->addUse(InvalidArgumentException::class); + $constuctor->addBody(<<name}) && ! array_is_list(\${$argument->name})) { + throw new InvalidArgumentException('Expected \${$argument->name} argument to be a list, got an associative array.'); + } + + PHP); + } + + if ($type->query) { + $namespace->addUseFunction('is_array'); + $namespace->addUse(QueryObject::class); + $constuctor->addBody(<<name})) { + \${$argument->name} = QueryObject::create(\${$argument->name}); + } + + PHP); + } + + if ($type->javascript) { + $namespace->addUseFunction('is_string'); + $namespace->addUse(Javascript::class); + $constuctor->addBody(<<name})) { + \${$argument->name} = new Javascript(\${$argument->name}); + } + + PHP); + } + } + + // Set property from constructor argument + $constuctor->addBody('$this->' . $argument->name . ' = $' . $argument->name . ';'); + } + + $class->addMethod('getOperator') + ->setReturnType('string') + ->setBody('return ' . var_export($operator->name, true) . ';'); + + return $namespace; + } + + /** + * Operator classes interfaces are defined by their return type as a MongoDB expression. + * + * @return list + */ + private function getInterfaces(OperatorDefinition $definition): array + { + $interfaces = []; + + foreach ($definition->type as $type) { + $interfaces[] = $interface = $this->getType($type)->returnType; + assert(interface_exists($interface), sprintf('"%s" is not an interface.', $interface)); + } + + return $interfaces; + } +} diff --git a/generator/src/OperatorFactoryGenerator.php b/generator/src/OperatorFactoryGenerator.php new file mode 100644 index 000000000..0bc27620f --- /dev/null +++ b/generator/src/OperatorFactoryGenerator.php @@ -0,0 +1,96 @@ +writeFile($this->createFactoryTrait($definition)); + } + + private function createFactoryTrait(GeneratorDefinition $definition): PhpNamespace + { + $namespace = new PhpNamespace($definition->namespace); + $trait = $namespace->addTrait('FactoryTrait'); + $trait->addComment('@internal'); + + // Pedantry requires methods to be ordered alphabetically + $operators = $this->getOperators($definition); + usort($operators, fn (OperatorDefinition $a, OperatorDefinition $b) => strcasecmp($a->name, $b->name)); + + foreach ($operators as $operator) { + try { + $this->addMethod($definition, $operator, $namespace, $trait); + } catch (Throwable $e) { + throw new RuntimeException(sprintf('Failed to generate class for operator "%s"', $operator->name), 0, $e); + } + } + + return $namespace; + } + + private function addMethod(GeneratorDefinition $definition, OperatorDefinition $operator, PhpNamespace $namespace, TraitType $trait): void + { + $operatorClassName = '\\' . $definition->namespace . '\\' . $this->getOperatorClassName($definition, $operator); + $namespace->addUse($operatorClassName); + + $method = $trait->addMethod(ltrim($operator->name, '$')); + $method->setStatic(); + $method->addComment($operator->description); + $method->addComment('@see ' . $operator->link); + $args = []; + foreach ($operator->arguments as $argument) { + $type = $this->getAcceptedTypes($argument); + foreach ($type->use as $use) { + $namespace->addUse($use); + } + + $parameter = $method->addParameter($argument->name); + $parameter->setType($type->native); + if ($argument->variadic) { + if ($argument->variadic === VariadicType::Array) { + // Warn that named arguments are not supported + // @see https://psalm.dev/docs/running_psalm/issues/NamedArgumentNotAllowed/ + $method->addComment('@no-named-arguments'); + } + + $method->setVariadic(); + $method->addComment('@param ' . $type->doc . ' ...$' . $argument->name . rtrim(' ' . $argument->description)); + $args[] = '...$' . $argument->name; + } else { + if ($argument->optional) { + $parameter->setDefaultValue(new Literal('Optional::Undefined')); + } elseif ($argument->default !== null) { + $parameter->setDefaultValue($argument->default); + } + + $method->addComment('@param ' . $type->doc . ' $' . $argument->name . rtrim(' ' . $argument->description)); + $args[] = '$' . $argument->name; + } + } + + $operatorShortClassName = ltrim(str_replace($definition->namespace, '', $operatorClassName), '\\'); + $method->addBody('return new ' . $operatorShortClassName . '(' . implode(', ', $args) . ');'); + $method->setReturnType($operatorClassName); + } +} diff --git a/generator/src/OperatorGenerator.php b/generator/src/OperatorGenerator.php new file mode 100644 index 000000000..0b9c60748 --- /dev/null +++ b/generator/src/OperatorGenerator.php @@ -0,0 +1,154 @@ + */ + private array $expressions, + ) { + parent::__construct($rootDir); + + $this->yamlReader = new YamlReader(); + } + + abstract public function generate(GeneratorDefinition $definition): void; + + /** @return list */ + final protected function getOperators(GeneratorDefinition $definition): array + { + // Remove unsupported operators + return array_filter( + $this->yamlReader->read($definition->configFiles), + fn (OperatorDefinition $operator): bool => ! in_array($operator->name, ['$'], true), + ); + } + + final protected function getOperatorClassName(GeneratorDefinition $definition, OperatorDefinition $operator): string + { + return ucfirst(ltrim($operator->name, '$')) . $definition->classNameSuffix; + } + + final protected function getType(string $type): ExpressionDefinition + { + assert(array_key_exists($type, $this->expressions), sprintf('Invalid expression type "%s".', $type)); + + return $this->expressions[$type]; + } + + /** + * Expression types can contain class names, interface, native types or "list". + * PHPDoc types are more precise than native types, so we use them systematically even if redundant. + * + * @return object{native:string,doc:string,use:list,list:bool,query:bool,javascript:bool} + */ + final protected function getAcceptedTypes(ArgumentDefinition $arg): stdClass + { + $nativeTypes = []; + + foreach ($arg->type as $type) { + $type = $this->getType($type); + $nativeTypes = array_merge($nativeTypes, $type->acceptedTypes); + + if (isset($type->returnType)) { + $nativeTypes[] = $type->returnType; + } + } + + if ($arg->optional) { + $use[] = '\\' . Optional::class; + $nativeTypes[] = Optional::class; + } + + $docTypes = $nativeTypes = array_unique($nativeTypes); + $use = []; + + foreach ($nativeTypes as $key => $typeName) { + if (interface_exists($typeName) || class_exists($typeName)) { + $use[] = $nativeTypes[$key] = '\\' . $typeName; + $docTypes[$key] = $this->splitNamespaceAndClassName($typeName)[1]; + // A union cannot contain both object and a class type + if (in_array('object', $nativeTypes, true)) { + unset($nativeTypes[$key]); + } + } + } + + // If an array is expected, but not an object, we can check for a list + $listCheck = in_array('\\' . PackedArray::class, $nativeTypes, true) + && ! in_array('\\' . Document::class, $nativeTypes, true); + + // If the argument is a query, we need to convert it to a QueryObject + $isQuery = in_array('query', $arg->type, true); + + // If the argument is code, we need to convert it to a Javascript object + $isJavascript = in_array('javascript', $arg->type, true); + + // mixed can only be used as a standalone type + if (in_array('mixed', $nativeTypes, true)) { + $nativeTypes = ['mixed']; + } + + usort($nativeTypes, self::sortTypesCallback(...)); + usort($docTypes, self::sortTypesCallback(...)); + sort($use); + + return (object) [ + 'native' => Type::union(...array_unique($nativeTypes)), + 'doc' => Type::union(...array_unique($docTypes)), + 'use' => array_unique($use), + 'list' => $listCheck, + 'query' => $isQuery, + 'javascript' => $isJavascript, + ]; + } + + /** + * usort() callback for sorting types. + * "Optional" is always first, for documentation of optional parameters, + * then types are sorted alphabetically. + */ + private static function sortTypesCallback(string $type1, string $type2): int + { + if ($type1 === 'Optional' || $type1 === '\\' . Optional::class) { + return -1; + } + + if ($type2 === 'Optional' || $type2 === '\\' . Optional::class) { + return 1; + } + + return $type1 <=> $type2; + } +} diff --git a/generator/src/OperatorTestGenerator.php b/generator/src/OperatorTestGenerator.php new file mode 100644 index 000000000..a8e6e91cc --- /dev/null +++ b/generator/src/OperatorTestGenerator.php @@ -0,0 +1,170 @@ +createExpectedClass($definition); + + foreach ($this->getOperators($definition) as $operator) { + // Skip operators without tests + if (! $operator->tests) { + continue; + } + + try { + $this->writeFile($this->createClass($definition, $operator, $dataNamespace->getClasses()[self::DATA_ENUM]), false); + } catch (Throwable $e) { + throw new RuntimeException(sprintf('Failed to generate class for operator "%s"', $operator->name), 0, $e); + } + } + + $this->writeFile($dataNamespace); + } + + public function createExpectedClass(GeneratorDefinition $definition): PhpNamespace + { + $dataNamespace = str_replace('MongoDB', 'MongoDB\\Tests', $definition->namespace); + + $namespace = new PhpNamespace($dataNamespace); + $enum = $namespace->addEnum(self::DATA_ENUM); + $enum->setType('string'); + + return $namespace; + } + + public function createClass(GeneratorDefinition $definition, OperatorDefinition $operator, EnumType $dataEnum): PhpNamespace + { + $testNamespace = str_replace('MongoDB', 'MongoDB\\Tests', $definition->namespace); + $testClass = $this->getOperatorClassName($definition, $operator) . 'Test'; + + $namespace = $this->readFile($testNamespace, $testClass)?->getNamespaces()[$testNamespace] ?? null; + $namespace ??= new PhpNamespace($testNamespace); + + $class = $namespace->getClasses()[$testClass] ?? null; + $class ??= $namespace->addClass($testClass); + $namespace->addUse(PipelineTestCase::class); + $class->setExtends(PipelineTestCase::class); + $namespace->addUse(Pipeline::class); + $class->setComment('Test ' . $operator->name . ' ' . basename($definition->configFiles)); + + foreach ($operator->tests as $test) { + $testName = 'test' . str_replace([' ', '-'], '', ucwords(str_replace('$', '', $test->name))); + $caseName = str_replace([' ', '-'], '', ucwords(str_replace('$', '', $operator->name . ' ' . $test->name))); + + $pipeline = $this->convertYamlTaggedValues($test->pipeline); + + // Wrap the pipeline array into a document + $json = Document::fromPHP(['pipeline' => $pipeline])->toCanonicalExtendedJSON(); + // Unwrap the pipeline array and reformat for prettier JSON + $json = json_encode(json_decode($json)->pipeline, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $case = $dataEnum->addCase($caseName, new Literal('<<<\'JSON\'' . "\n" . $json . "\n" . 'JSON')); + $case->setComment($test->name); + if ($test->link) { + $case->addComment(''); + $case->addComment('@see ' . $test->link); + } + + $caseName = self::DATA_ENUM . '::' . $caseName; + + if ($class->hasMethod($testName)) { + $testMethod = $class->getMethod($testName); + } else { + $testMethod = $class->addMethod($testName); + $testMethod->setBody(<<assertSamePipeline({$caseName}, \$pipeline); + PHP); + } + + $testMethod->setPublic(); + $testMethod->setReturnType(Type::Void); + } + + $methods = $class->getMethods(); + ksort($methods); + $class->setMethods($methods); + + return $namespace; + } + + private function convertYamlTaggedValues(mixed $object): mixed + { + if ($object instanceof TaggedValue) { + $value = $object->getValue(); + + return match ($object->getTag()) { + 'bson_regex' => new Regex(...(array) $value), + 'bson_int128' => new Int64($value), + 'bson_decimal128' => new Decimal128($value), + 'bson_utcdatetime' => new UTCDateTime(is_numeric($value) ? $value : new DateTimeImmutable($value)), + 'bson_binary' => new Binary(base64_decode($value)), + default => throw new InvalidArgumentException(sprintf('Yaml tag "%s" is not supported.', $object->getTag())), + }; + } + + if (is_array($object)) { + foreach ($object as $key => $value) { + $object[$key] = $this->convertYamlTaggedValues($value); + } + + return $object; + } + + if (is_object($object)) { + foreach (get_object_vars($object) as $key => $value) { + $object->{$key} = $this->convertYamlTaggedValues($value); + } + + return $object; + } + + return $object; + } +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist index ace2991ac..de542ac64 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -12,10 +12,15 @@ benchmark/src src examples + generator/src + generator/config tests tools rector.php + + src/Builder/(Accumulator|Expression|Query|Projection|Stage)/*\.php + diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 8589b8024..6c323fe23 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + @@ -41,6 +41,145 @@ + + + + + + + + + + + + + + + + name]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + recursiveEncode($value)]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0]]> + + @@ -57,6 +196,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Builder/Accumulator.php b/src/Builder/Accumulator.php new file mode 100644 index 000000000..75d460bd1 --- /dev/null +++ b/src/Builder/Accumulator.php @@ -0,0 +1,44 @@ +|stdClass $operator Window operator to use in the $setWindowFields stage. + * @param Optional|array{string|int,string|int} $documents A window where the lower and upper boundaries are specified relative to the position of the current document read from the collection. + * @param Optional|array{string|numeric,string|numeric} $range Arguments passed to the init function. + * @param Optional|non-empty-string $unit Specifies the units for time range window boundaries. If omitted, default numeric range window boundaries are used. + */ + public static function outputWindow( + Document|Serializable|WindowInterface|stdClass|array $operator, + Optional|array $documents = Optional::Undefined, + Optional|array $range = Optional::Undefined, + Optional|TimeUnit|string $unit = Optional::Undefined, + ): OutputWindow { + return new OutputWindow($operator, $documents, $range, $unit); + } + + private function __construct() + { + // This class cannot be instantiated + } +} diff --git a/src/Builder/Accumulator/AccumulatorAccumulator.php b/src/Builder/Accumulator/AccumulatorAccumulator.php new file mode 100644 index 000000000..a12b57c7c --- /dev/null +++ b/src/Builder/Accumulator/AccumulatorAccumulator.php @@ -0,0 +1,111 @@ +init = $init; + if (is_string($accumulate)) { + $accumulate = new Javascript($accumulate); + } + + $this->accumulate = $accumulate; + if (is_array($accumulateArgs) && ! array_is_list($accumulateArgs)) { + throw new InvalidArgumentException('Expected $accumulateArgs argument to be a list, got an associative array.'); + } + + $this->accumulateArgs = $accumulateArgs; + if (is_string($merge)) { + $merge = new Javascript($merge); + } + + $this->merge = $merge; + $this->lang = $lang; + if (is_array($initArgs) && ! array_is_list($initArgs)) { + throw new InvalidArgumentException('Expected $initArgs argument to be a list, got an associative array.'); + } + + $this->initArgs = $initArgs; + if (is_string($finalize)) { + $finalize = new Javascript($finalize); + } + + $this->finalize = $finalize; + } + + public function getOperator(): string + { + return '$accumulator'; + } +} diff --git a/src/Builder/Accumulator/AddToSetAccumulator.php b/src/Builder/Accumulator/AddToSetAccumulator.php new file mode 100644 index 000000000..08bda8cea --- /dev/null +++ b/src/Builder/Accumulator/AddToSetAccumulator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$addToSet'; + } +} diff --git a/src/Builder/Accumulator/AvgAccumulator.php b/src/Builder/Accumulator/AvgAccumulator.php new file mode 100644 index 000000000..da0b7730a --- /dev/null +++ b/src/Builder/Accumulator/AvgAccumulator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$avg'; + } +} diff --git a/src/Builder/Accumulator/BottomAccumulator.php b/src/Builder/Accumulator/BottomAccumulator.php new file mode 100644 index 000000000..c8408bafc --- /dev/null +++ b/src/Builder/Accumulator/BottomAccumulator.php @@ -0,0 +1,53 @@ +sortBy = $sortBy; + $this->output = $output; + } + + public function getOperator(): string + { + return '$bottom'; + } +} diff --git a/src/Builder/Accumulator/BottomNAccumulator.php b/src/Builder/Accumulator/BottomNAccumulator.php new file mode 100644 index 000000000..47e51852a --- /dev/null +++ b/src/Builder/Accumulator/BottomNAccumulator.php @@ -0,0 +1,61 @@ +n = $n; + $this->sortBy = $sortBy; + $this->output = $output; + } + + public function getOperator(): string + { + return '$bottomN'; + } +} diff --git a/src/Builder/Accumulator/CountAccumulator.php b/src/Builder/Accumulator/CountAccumulator.php new file mode 100644 index 000000000..384996417 --- /dev/null +++ b/src/Builder/Accumulator/CountAccumulator.php @@ -0,0 +1,35 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$covariancePop'; + } +} diff --git a/src/Builder/Accumulator/CovarianceSampAccumulator.php b/src/Builder/Accumulator/CovarianceSampAccumulator.php new file mode 100644 index 000000000..1266b263e --- /dev/null +++ b/src/Builder/Accumulator/CovarianceSampAccumulator.php @@ -0,0 +1,50 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$covarianceSamp'; + } +} diff --git a/src/Builder/Accumulator/DenseRankAccumulator.php b/src/Builder/Accumulator/DenseRankAccumulator.php new file mode 100644 index 000000000..7dea1de67 --- /dev/null +++ b/src/Builder/Accumulator/DenseRankAccumulator.php @@ -0,0 +1,33 @@ +input = $input; + $this->unit = $unit; + } + + public function getOperator(): string + { + return '$derivative'; + } +} diff --git a/src/Builder/Accumulator/DocumentNumberAccumulator.php b/src/Builder/Accumulator/DocumentNumberAccumulator.php new file mode 100644 index 000000000..4318dbd81 --- /dev/null +++ b/src/Builder/Accumulator/DocumentNumberAccumulator.php @@ -0,0 +1,33 @@ +input = $input; + $this->N = $N; + $this->alpha = $alpha; + } + + public function getOperator(): string + { + return '$expMovingAvg'; + } +} diff --git a/src/Builder/Accumulator/FactoryTrait.php b/src/Builder/Accumulator/FactoryTrait.php new file mode 100644 index 000000000..e3ab1f24e --- /dev/null +++ b/src/Builder/Accumulator/FactoryTrait.php @@ -0,0 +1,550 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$first'; + } +} diff --git a/src/Builder/Accumulator/FirstNAccumulator.php b/src/Builder/Accumulator/FirstNAccumulator.php new file mode 100644 index 000000000..0100f1aed --- /dev/null +++ b/src/Builder/Accumulator/FirstNAccumulator.php @@ -0,0 +1,53 @@ +input = $input; + $this->n = $n; + } + + public function getOperator(): string + { + return '$firstN'; + } +} diff --git a/src/Builder/Accumulator/IntegralAccumulator.php b/src/Builder/Accumulator/IntegralAccumulator.php new file mode 100644 index 000000000..5a5280ba2 --- /dev/null +++ b/src/Builder/Accumulator/IntegralAccumulator.php @@ -0,0 +1,59 @@ +input = $input; + $this->unit = $unit; + } + + public function getOperator(): string + { + return '$integral'; + } +} diff --git a/src/Builder/Accumulator/LastAccumulator.php b/src/Builder/Accumulator/LastAccumulator.php new file mode 100644 index 000000000..ab01fecdb --- /dev/null +++ b/src/Builder/Accumulator/LastAccumulator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$last'; + } +} diff --git a/src/Builder/Accumulator/LastNAccumulator.php b/src/Builder/Accumulator/LastNAccumulator.php new file mode 100644 index 000000000..d0752f58d --- /dev/null +++ b/src/Builder/Accumulator/LastNAccumulator.php @@ -0,0 +1,59 @@ +input = $input; + $this->n = $n; + } + + public function getOperator(): string + { + return '$lastN'; + } +} diff --git a/src/Builder/Accumulator/LinearFillAccumulator.php b/src/Builder/Accumulator/LinearFillAccumulator.php new file mode 100644 index 000000000..e203f7ff9 --- /dev/null +++ b/src/Builder/Accumulator/LinearFillAccumulator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$linearFill'; + } +} diff --git a/src/Builder/Accumulator/LocfAccumulator.php b/src/Builder/Accumulator/LocfAccumulator.php new file mode 100644 index 000000000..e133cc4c9 --- /dev/null +++ b/src/Builder/Accumulator/LocfAccumulator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$locf'; + } +} diff --git a/src/Builder/Accumulator/MaxAccumulator.php b/src/Builder/Accumulator/MaxAccumulator.php new file mode 100644 index 000000000..0258eb82b --- /dev/null +++ b/src/Builder/Accumulator/MaxAccumulator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$max'; + } +} diff --git a/src/Builder/Accumulator/MaxNAccumulator.php b/src/Builder/Accumulator/MaxNAccumulator.php new file mode 100644 index 000000000..c952eedfa --- /dev/null +++ b/src/Builder/Accumulator/MaxNAccumulator.php @@ -0,0 +1,57 @@ +input = $input; + $this->n = $n; + } + + public function getOperator(): string + { + return '$maxN'; + } +} diff --git a/src/Builder/Accumulator/MedianAccumulator.php b/src/Builder/Accumulator/MedianAccumulator.php new file mode 100644 index 000000000..a81070700 --- /dev/null +++ b/src/Builder/Accumulator/MedianAccumulator.php @@ -0,0 +1,53 @@ +input = $input; + $this->method = $method; + } + + public function getOperator(): string + { + return '$median'; + } +} diff --git a/src/Builder/Accumulator/MergeObjectsAccumulator.php b/src/Builder/Accumulator/MergeObjectsAccumulator.php new file mode 100644 index 000000000..7f163e023 --- /dev/null +++ b/src/Builder/Accumulator/MergeObjectsAccumulator.php @@ -0,0 +1,43 @@ +document = $document; + } + + public function getOperator(): string + { + return '$mergeObjects'; + } +} diff --git a/src/Builder/Accumulator/MinAccumulator.php b/src/Builder/Accumulator/MinAccumulator.php new file mode 100644 index 000000000..9194c8866 --- /dev/null +++ b/src/Builder/Accumulator/MinAccumulator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$min'; + } +} diff --git a/src/Builder/Accumulator/MinNAccumulator.php b/src/Builder/Accumulator/MinNAccumulator.php new file mode 100644 index 000000000..dab804cc5 --- /dev/null +++ b/src/Builder/Accumulator/MinNAccumulator.php @@ -0,0 +1,57 @@ +input = $input; + $this->n = $n; + } + + public function getOperator(): string + { + return '$minN'; + } +} diff --git a/src/Builder/Accumulator/PercentileAccumulator.php b/src/Builder/Accumulator/PercentileAccumulator.php new file mode 100644 index 000000000..85128f024 --- /dev/null +++ b/src/Builder/Accumulator/PercentileAccumulator.php @@ -0,0 +1,79 @@ +input = $input; + if (is_array($p) && ! array_is_list($p)) { + throw new InvalidArgumentException('Expected $p argument to be a list, got an associative array.'); + } + + $this->p = $p; + $this->method = $method; + } + + public function getOperator(): string + { + return '$percentile'; + } +} diff --git a/src/Builder/Accumulator/PushAccumulator.php b/src/Builder/Accumulator/PushAccumulator.php new file mode 100644 index 000000000..3eb095670 --- /dev/null +++ b/src/Builder/Accumulator/PushAccumulator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$push'; + } +} diff --git a/src/Builder/Accumulator/RankAccumulator.php b/src/Builder/Accumulator/RankAccumulator.php new file mode 100644 index 000000000..dd274dceb --- /dev/null +++ b/src/Builder/Accumulator/RankAccumulator.php @@ -0,0 +1,33 @@ +output = $output; + $this->by = $by; + $this->default = $default; + } + + public function getOperator(): string + { + return '$shift'; + } +} diff --git a/src/Builder/Accumulator/StdDevPopAccumulator.php b/src/Builder/Accumulator/StdDevPopAccumulator.php new file mode 100644 index 000000000..01fe39dff --- /dev/null +++ b/src/Builder/Accumulator/StdDevPopAccumulator.php @@ -0,0 +1,45 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$stdDevPop'; + } +} diff --git a/src/Builder/Accumulator/StdDevSampAccumulator.php b/src/Builder/Accumulator/StdDevSampAccumulator.php new file mode 100644 index 000000000..54adb4170 --- /dev/null +++ b/src/Builder/Accumulator/StdDevSampAccumulator.php @@ -0,0 +1,45 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$stdDevSamp'; + } +} diff --git a/src/Builder/Accumulator/SumAccumulator.php b/src/Builder/Accumulator/SumAccumulator.php new file mode 100644 index 000000000..faf0ace44 --- /dev/null +++ b/src/Builder/Accumulator/SumAccumulator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$sum'; + } +} diff --git a/src/Builder/Accumulator/TopAccumulator.php b/src/Builder/Accumulator/TopAccumulator.php new file mode 100644 index 000000000..135b8eb0a --- /dev/null +++ b/src/Builder/Accumulator/TopAccumulator.php @@ -0,0 +1,54 @@ +sortBy = $sortBy; + $this->output = $output; + } + + public function getOperator(): string + { + return '$top'; + } +} diff --git a/src/Builder/Accumulator/TopNAccumulator.php b/src/Builder/Accumulator/TopNAccumulator.php new file mode 100644 index 000000000..ab8009121 --- /dev/null +++ b/src/Builder/Accumulator/TopNAccumulator.php @@ -0,0 +1,61 @@ +n = $n; + $this->sortBy = $sortBy; + $this->output = $output; + } + + public function getOperator(): string + { + return '$topN'; + } +} diff --git a/src/Builder/BuilderEncoder.php b/src/Builder/BuilderEncoder.php new file mode 100644 index 000000000..8b25325ce --- /dev/null +++ b/src/Builder/BuilderEncoder.php @@ -0,0 +1,104 @@ + */ +class BuilderEncoder implements Encoder +{ + /** @template-use EncodeIfSupported */ + use EncodeIfSupported; + + /** @var array> */ + private array $defaultEncoders = [ + Pipeline::class => PipelineEncoder::class, + Variable::class => VariableEncoder::class, + DictionaryInterface::class => DictionaryEncoder::class, + FieldPathInterface::class => FieldPathEncoder::class, + CombinedFieldQuery::class => CombinedFieldQueryEncoder::class, + QueryObject::class => QueryEncoder::class, + OutputWindow::class => OutputWindowEncoder::class, + OperatorInterface::class => OperatorEncoder::class, + ]; + + /** @var array */ + private array $cachedEncoders = []; + + /** @param array> $customEncoders */ + public function __construct(private readonly array $customEncoders = []) + { + } + + /** @psalm-assert-if-true object $value */ + public function canEncode(mixed $value): bool + { + if (! is_object($value)) { + return false; + } + + return (bool) $this->getEncoderFor($value)?->canEncode($value); + } + + public function encode(mixed $value): stdClass|array|string|int + { + $encoder = $this->getEncoderFor($value); + + if (! $encoder?->canEncode($value)) { + throw UnsupportedValueException::invalidEncodableValue($value); + } + + return $encoder->encode($value); + } + + private function getEncoderFor(object $value): ExpressionEncoder|null + { + $valueClass = $value::class; + if (array_key_exists($valueClass, $this->cachedEncoders)) { + return $this->cachedEncoders[$valueClass]; + } + + $encoderList = $this->customEncoders + $this->defaultEncoders; + + // First attempt: match class name exactly + if (isset($encoderList[$valueClass])) { + return $this->cachedEncoders[$valueClass] = new $encoderList[$valueClass]($this); + } + + // Second attempt: catch child classes + foreach ($encoderList as $className => $encoderClass) { + if ($value instanceof $className) { + return $this->cachedEncoders[$valueClass] = new $encoderClass($this); + } + } + + return $this->cachedEncoders[$valueClass] = null; + } +} diff --git a/src/Builder/Encoder/AbstractExpressionEncoder.php b/src/Builder/Encoder/AbstractExpressionEncoder.php new file mode 100644 index 000000000..49d7a34f1 --- /dev/null +++ b/src/Builder/Encoder/AbstractExpressionEncoder.php @@ -0,0 +1,53 @@ + + */ +abstract class AbstractExpressionEncoder implements ExpressionEncoder +{ + final public function __construct(protected readonly BuilderEncoder $encoder) + { + } + + /** + * Nested arrays and objects must be encoded recursively. + * + * @psalm-param T $value + * + * @psalm-return (T is stdClass ? stdClass : (T is array ? array : mixed)) + * + * @template T + */ + final protected function recursiveEncode(mixed $value): mixed + { + if (is_array($value)) { + foreach ($value as $key => $val) { + $value[$key] = $this->recursiveEncode($val); + } + + return $value; + } + + if ($value instanceof stdClass) { + foreach (get_object_vars($value) as $key => $val) { + $value->{$key} = $this->recursiveEncode($val); + } + + return $value; + } + + return $this->encoder->encodeIfSupported($value); + } +} diff --git a/src/Builder/Encoder/CombinedFieldQueryEncoder.php b/src/Builder/Encoder/CombinedFieldQueryEncoder.php new file mode 100644 index 000000000..4ad42396b --- /dev/null +++ b/src/Builder/Encoder/CombinedFieldQueryEncoder.php @@ -0,0 +1,52 @@ + */ +class CombinedFieldQueryEncoder extends AbstractExpressionEncoder +{ + /** @template-use EncodeIfSupported */ + use EncodeIfSupported; + + public function canEncode(mixed $value): bool + { + return $value instanceof CombinedFieldQuery; + } + + public function encode(mixed $value): stdClass + { + if (! $this->canEncode($value)) { + throw UnsupportedValueException::invalidEncodableValue($value); + } + + $result = new stdClass(); + foreach ($value->fieldQueries as $filter) { + $filter = $this->recursiveEncode($filter); + if (is_object($filter)) { + $filter = get_object_vars($filter); + } elseif (! is_array($filter)) { + throw new LogicException(sprintf('Query filters must an array or an object. Got "%s"', get_debug_type($filter))); + } + + foreach ($filter as $key => $filterValue) { + $result->{$key} = $filterValue; + } + } + + return $result; + } +} diff --git a/src/Builder/Encoder/DictionaryEncoder.php b/src/Builder/Encoder/DictionaryEncoder.php new file mode 100644 index 000000000..078db76ed --- /dev/null +++ b/src/Builder/Encoder/DictionaryEncoder.php @@ -0,0 +1,31 @@ + */ +class DictionaryEncoder extends AbstractExpressionEncoder +{ + /** @template-use EncodeIfSupported */ + use EncodeIfSupported; + + public function canEncode(mixed $value): bool + { + return $value instanceof DictionaryInterface; + } + + public function encode(mixed $value): string|int|array|stdClass + { + if (! $this->canEncode($value)) { + throw UnsupportedValueException::invalidEncodableValue($value); + } + + return $value->getValue(); + } +} diff --git a/src/Builder/Encoder/ExpressionEncoder.php b/src/Builder/Encoder/ExpressionEncoder.php new file mode 100644 index 000000000..1fe94790e --- /dev/null +++ b/src/Builder/Encoder/ExpressionEncoder.php @@ -0,0 +1,19 @@ + + */ +interface ExpressionEncoder extends Encoder +{ + public function __construct(BuilderEncoder $encoder); +} diff --git a/src/Builder/Encoder/FieldPathEncoder.php b/src/Builder/Encoder/FieldPathEncoder.php new file mode 100644 index 000000000..8fe7381c2 --- /dev/null +++ b/src/Builder/Encoder/FieldPathEncoder.php @@ -0,0 +1,31 @@ + */ +class FieldPathEncoder extends AbstractExpressionEncoder +{ + /** @template-use EncodeIfSupported */ + use EncodeIfSupported; + + public function canEncode(mixed $value): bool + { + return $value instanceof FieldPathInterface; + } + + public function encode(mixed $value): string + { + if (! $this->canEncode($value)) { + throw UnsupportedValueException::invalidEncodableValue($value); + } + + // TODO: needs method because interfaces can't have properties + return '$' . $value->name; + } +} diff --git a/src/Builder/Encoder/OperatorEncoder.php b/src/Builder/Encoder/OperatorEncoder.php new file mode 100644 index 000000000..f52ad392a --- /dev/null +++ b/src/Builder/Encoder/OperatorEncoder.php @@ -0,0 +1,164 @@ + */ +class OperatorEncoder extends AbstractExpressionEncoder +{ + /** @template-use EncodeIfSupported */ + use EncodeIfSupported; + + public function canEncode(mixed $value): bool + { + return $value instanceof OperatorInterface; + } + + public function encode(mixed $value): stdClass + { + if (! $this->canEncode($value)) { + throw UnsupportedValueException::invalidEncodableValue($value); + } + + switch ($value::ENCODE) { + case Encode::Single: + return $this->encodeAsSingle($value); + + case Encode::Array: + return $this->encodeAsArray($value); + + case Encode::Object: + case Encode::FlatObject: + return $this->encodeAsObject($value); + + case Encode::DollarObject: + return $this->encodeAsDollarObject($value); + + case Encode::Group: + assert($value instanceof GroupStage); + + return $this->encodeAsGroup($value); + } + + throw new LogicException(sprintf('Class "%s" does not have a valid ENCODE constant.', $value::class)); + } + + /** + * Encode the value as an array of properties, in the order they are defined in the class. + */ + private function encodeAsArray(OperatorInterface $value): stdClass + { + $result = []; + /** @var mixed $val */ + foreach (get_object_vars($value) as $val) { + // Skip optional arguments. For example, the $slice expression operator has an optional argument + // in the middle of the array. + if ($val === Optional::Undefined) { + continue; + } + + $result[] = $this->recursiveEncode($val); + } + + return $this->wrap($value, $result); + } + + private function encodeAsDollarObject(OperatorInterface $value): stdClass + { + $result = new stdClass(); + foreach (get_object_vars($value) as $key => $val) { + // Skip optional arguments. If they have a default value, it is resolved by the server. + if ($val === Optional::Undefined) { + continue; + } + + $val = $this->recursiveEncode($val); + + if ($key === 'geometry') { + if (is_object($val) && property_exists($val, '$geometry')) { + $result->{'$geometry'} = $val->{'$geometry'}; + } elseif (is_array($val) && array_key_exists('$geometry', $val)) { + $result->{'$geometry'} = $val['$geometry']; + } else { + $result->{'$geometry'} = $val; + } + } else { + $result->{'$' . $key} = $val; + } + } + + return $this->wrap($value, $result); + } + + /** + * $group stage have a specific encoding because the _id argument is required and others are variadic + */ + private function encodeAsGroup(GroupStage $value): stdClass + { + $result = new stdClass(); + $result->_id = $this->recursiveEncode($value->_id); + + foreach (get_object_vars($value->field) as $key => $val) { + $result->{$key} = $this->recursiveEncode($val); + } + + return $this->wrap($value, $result); + } + + private function encodeAsObject(OperatorInterface $value): stdClass + { + $result = new stdClass(); + foreach (get_object_vars($value) as $key => $val) { + // Skip optional arguments. If they have a default value, it is resolved by the server. + if ($val === Optional::Undefined) { + continue; + } + + $result->{$key} = $this->recursiveEncode($val); + } + + return $value::ENCODE === Encode::FlatObject + ? $result + : $this->wrap($value, $result); + } + + /** + * Get the unique property of the operator as value + */ + private function encodeAsSingle(OperatorInterface $value): stdClass + { + foreach (get_object_vars($value) as $val) { + $result = $this->recursiveEncode($val); + + return $this->wrap($value, $result); + } + + throw new LogicException(sprintf('Class "%s" does not have a single property.', $value::class)); + } + + private function wrap(OperatorInterface $value, mixed $result): stdClass + { + $object = new stdClass(); + $object->{$value->getOperator()} = $result; + + return $object; + } +} diff --git a/src/Builder/Encoder/OutputWindowEncoder.php b/src/Builder/Encoder/OutputWindowEncoder.php new file mode 100644 index 000000000..9db567386 --- /dev/null +++ b/src/Builder/Encoder/OutputWindowEncoder.php @@ -0,0 +1,60 @@ + */ +class OutputWindowEncoder extends AbstractExpressionEncoder +{ + /** @template-use EncodeIfSupported */ + use EncodeIfSupported; + + public function canEncode(mixed $value): bool + { + return $value instanceof OutputWindow; + } + + public function encode(mixed $value): stdClass + { + if (! $this->canEncode($value)) { + throw UnsupportedValueException::invalidEncodableValue($value); + } + + $result = $this->recursiveEncode($value->operator); + + // Transform the result into an stdClass if a document is provided + if (! $value->operator instanceof WindowInterface) { + if (! is_first_key_operator($result)) { + $firstKey = array_key_first((array) $result); + + throw new LogicException(sprintf('Expected OutputWindow::$operator to be an operator. Got "%s"', $firstKey ?? 'null')); + } + + $result = (object) $result; + } + + if (! $result instanceof stdClass) { + throw new LogicException(sprintf('Expected OutputWindow::$operator to be an stdClass, array or WindowInterface. Got "%s"', get_debug_type($result))); + } + + if ($value->window !== Optional::Undefined) { + $result->window = $this->recursiveEncode($value->window); + } + + return $result; + } +} diff --git a/src/Builder/Encoder/PipelineEncoder.php b/src/Builder/Encoder/PipelineEncoder.php new file mode 100644 index 000000000..f0b319e9c --- /dev/null +++ b/src/Builder/Encoder/PipelineEncoder.php @@ -0,0 +1,37 @@ +, Pipeline> */ +class PipelineEncoder extends AbstractExpressionEncoder +{ + /** @template-use EncodeIfSupported, Pipeline> */ + use EncodeIfSupported; + + /** @psalm-assert-if-true Pipeline $value */ + public function canEncode(mixed $value): bool + { + return $value instanceof Pipeline; + } + + /** @return list */ + public function encode(mixed $value): array + { + if (! $this->canEncode($value)) { + throw UnsupportedValueException::invalidEncodableValue($value); + } + + $encoded = []; + foreach ($value->getIterator() as $stage) { + $encoded[] = $this->encoder->encodeIfSupported($stage); + } + + return $encoded; + } +} diff --git a/src/Builder/Encoder/QueryEncoder.php b/src/Builder/Encoder/QueryEncoder.php new file mode 100644 index 000000000..d5890506c --- /dev/null +++ b/src/Builder/Encoder/QueryEncoder.php @@ -0,0 +1,57 @@ + */ +class QueryEncoder extends AbstractExpressionEncoder +{ + /** @template-use EncodeIfSupported */ + use EncodeIfSupported; + + public function canEncode(mixed $value): bool + { + return $value instanceof QueryObject; + } + + public function encode(mixed $value): stdClass + { + if (! $this->canEncode($value)) { + throw UnsupportedValueException::invalidEncodableValue($value); + } + + $result = new stdClass(); + foreach ($value->queries as $key => $value) { + if ($value instanceof QueryInterface) { + // The sub-objects is merged into the main object, replacing duplicate keys + foreach (get_object_vars($this->recursiveEncode($value)) as $subKey => $subValue) { + if (property_exists($result, $subKey)) { + throw new LogicException(sprintf('Duplicate key "%s" in query object', $subKey)); + } + + $result->{$subKey} = $subValue; + } + } else { + if (property_exists($result, (string) $key)) { + throw new LogicException(sprintf('Duplicate key "%s" in query object', $key)); + } + + $result->{$key} = $this->encoder->encodeIfSupported($value); + } + } + + return $result; + } +} diff --git a/src/Builder/Encoder/VariableEncoder.php b/src/Builder/Encoder/VariableEncoder.php new file mode 100644 index 000000000..726acb9ee --- /dev/null +++ b/src/Builder/Encoder/VariableEncoder.php @@ -0,0 +1,31 @@ + */ +class VariableEncoder extends AbstractExpressionEncoder +{ + /** @template-use EncodeIfSupported */ + use EncodeIfSupported; + + public function canEncode(mixed $value): bool + { + return $value instanceof Variable; + } + + public function encode(mixed $value): string + { + if (! $this->canEncode($value)) { + throw UnsupportedValueException::invalidEncodableValue($value); + } + + // TODO: needs method because interfaces can't have properties + return '$$' . $value->name; + } +} diff --git a/src/Builder/Expression.php b/src/Builder/Expression.php new file mode 100644 index 000000000..9e4779145 --- /dev/null +++ b/src/Builder/Expression.php @@ -0,0 +1,21 @@ +value = $value; + } + + public function getOperator(): string + { + return '$abs'; + } +} diff --git a/src/Builder/Expression/AcosOperator.php b/src/Builder/Expression/AcosOperator.php new file mode 100644 index 000000000..38a12b6c4 --- /dev/null +++ b/src/Builder/Expression/AcosOperator.php @@ -0,0 +1,46 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$acos'; + } +} diff --git a/src/Builder/Expression/AcoshOperator.php b/src/Builder/Expression/AcoshOperator.php new file mode 100644 index 000000000..ba956df43 --- /dev/null +++ b/src/Builder/Expression/AcoshOperator.php @@ -0,0 +1,46 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$acosh'; + } +} diff --git a/src/Builder/Expression/AddOperator.php b/src/Builder/Expression/AddOperator.php new file mode 100644 index 000000000..580731680 --- /dev/null +++ b/src/Builder/Expression/AddOperator.php @@ -0,0 +1,53 @@ + $expression The arguments can be any valid expression as long as they resolve to either all numbers or to numbers and a date. */ + public readonly array $expression; + + /** + * @param Decimal128|Int64|ResolvesToDate|ResolvesToNumber|UTCDateTime|float|int ...$expression The arguments can be any valid expression as long as they resolve to either all numbers or to numbers and a date. + * @no-named-arguments + */ + public function __construct(Decimal128|Int64|UTCDateTime|ResolvesToDate|ResolvesToNumber|float|int ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$add'; + } +} diff --git a/src/Builder/Expression/AllElementsTrueOperator.php b/src/Builder/Expression/AllElementsTrueOperator.php new file mode 100644 index 000000000..dbfebe75c --- /dev/null +++ b/src/Builder/Expression/AllElementsTrueOperator.php @@ -0,0 +1,48 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$allElementsTrue'; + } +} diff --git a/src/Builder/Expression/AndOperator.php b/src/Builder/Expression/AndOperator.php new file mode 100644 index 000000000..c4f3d7e6c --- /dev/null +++ b/src/Builder/Expression/AndOperator.php @@ -0,0 +1,56 @@ + $expression */ + public readonly array $expression; + + /** + * @param Decimal128|ExpressionInterface|Int64|ResolvesToBool|ResolvesToNull|ResolvesToNumber|ResolvesToString|Type|array|bool|float|int|null|stdClass|string ...$expression + * @no-named-arguments + */ + public function __construct( + Decimal128|Int64|Type|ResolvesToBool|ResolvesToNull|ResolvesToNumber|ResolvesToString|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression, + ) { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$and'; + } +} diff --git a/src/Builder/Expression/AnyElementTrueOperator.php b/src/Builder/Expression/AnyElementTrueOperator.php new file mode 100644 index 000000000..68cef3242 --- /dev/null +++ b/src/Builder/Expression/AnyElementTrueOperator.php @@ -0,0 +1,48 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$anyElementTrue'; + } +} diff --git a/src/Builder/Expression/ArrayElemAtOperator.php b/src/Builder/Expression/ArrayElemAtOperator.php new file mode 100644 index 000000000..e7fd09073 --- /dev/null +++ b/src/Builder/Expression/ArrayElemAtOperator.php @@ -0,0 +1,53 @@ +array = $array; + $this->idx = $idx; + } + + public function getOperator(): string + { + return '$arrayElemAt'; + } +} diff --git a/src/Builder/Expression/ArrayFieldPath.php b/src/Builder/Expression/ArrayFieldPath.php new file mode 100644 index 000000000..f475beb1c --- /dev/null +++ b/src/Builder/Expression/ArrayFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/ArrayToObjectOperator.php b/src/Builder/Expression/ArrayToObjectOperator.php new file mode 100644 index 000000000..4c6935875 --- /dev/null +++ b/src/Builder/Expression/ArrayToObjectOperator.php @@ -0,0 +1,48 @@ +array = $array; + } + + public function getOperator(): string + { + return '$arrayToObject'; + } +} diff --git a/src/Builder/Expression/AsinOperator.php b/src/Builder/Expression/AsinOperator.php new file mode 100644 index 000000000..f46031231 --- /dev/null +++ b/src/Builder/Expression/AsinOperator.php @@ -0,0 +1,46 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$asin'; + } +} diff --git a/src/Builder/Expression/AsinhOperator.php b/src/Builder/Expression/AsinhOperator.php new file mode 100644 index 000000000..ab466a0d8 --- /dev/null +++ b/src/Builder/Expression/AsinhOperator.php @@ -0,0 +1,46 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$asinh'; + } +} diff --git a/src/Builder/Expression/Atan2Operator.php b/src/Builder/Expression/Atan2Operator.php new file mode 100644 index 000000000..c08e7fbf8 --- /dev/null +++ b/src/Builder/Expression/Atan2Operator.php @@ -0,0 +1,53 @@ +y = $y; + $this->x = $x; + } + + public function getOperator(): string + { + return '$atan2'; + } +} diff --git a/src/Builder/Expression/AtanOperator.php b/src/Builder/Expression/AtanOperator.php new file mode 100644 index 000000000..b6d99973d --- /dev/null +++ b/src/Builder/Expression/AtanOperator.php @@ -0,0 +1,46 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$atan'; + } +} diff --git a/src/Builder/Expression/AtanhOperator.php b/src/Builder/Expression/AtanhOperator.php new file mode 100644 index 000000000..38b9208f6 --- /dev/null +++ b/src/Builder/Expression/AtanhOperator.php @@ -0,0 +1,46 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$atanh'; + } +} diff --git a/src/Builder/Expression/AvgOperator.php b/src/Builder/Expression/AvgOperator.php new file mode 100644 index 000000000..898bbe252 --- /dev/null +++ b/src/Builder/Expression/AvgOperator.php @@ -0,0 +1,53 @@ + $expression */ + public readonly array $expression; + + /** + * @param Decimal128|Int64|ResolvesToNumber|float|int ...$expression + * @no-named-arguments + */ + public function __construct(Decimal128|Int64|ResolvesToNumber|float|int ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$avg'; + } +} diff --git a/src/Builder/Expression/BinDataFieldPath.php b/src/Builder/Expression/BinDataFieldPath.php new file mode 100644 index 000000000..77c2d39ea --- /dev/null +++ b/src/Builder/Expression/BinDataFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/BinarySizeOperator.php b/src/Builder/Expression/BinarySizeOperator.php new file mode 100644 index 000000000..28293cc88 --- /dev/null +++ b/src/Builder/Expression/BinarySizeOperator.php @@ -0,0 +1,39 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$binarySize'; + } +} diff --git a/src/Builder/Expression/BitAndOperator.php b/src/Builder/Expression/BitAndOperator.php new file mode 100644 index 000000000..97b910a3f --- /dev/null +++ b/src/Builder/Expression/BitAndOperator.php @@ -0,0 +1,52 @@ + $expression */ + public readonly array $expression; + + /** + * @param Int64|ResolvesToInt|ResolvesToLong|int ...$expression + * @no-named-arguments + */ + public function __construct(Int64|ResolvesToInt|ResolvesToLong|int ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$bitAnd'; + } +} diff --git a/src/Builder/Expression/BitNotOperator.php b/src/Builder/Expression/BitNotOperator.php new file mode 100644 index 000000000..d75322971 --- /dev/null +++ b/src/Builder/Expression/BitNotOperator.php @@ -0,0 +1,40 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$bitNot'; + } +} diff --git a/src/Builder/Expression/BitOrOperator.php b/src/Builder/Expression/BitOrOperator.php new file mode 100644 index 000000000..f14ac9583 --- /dev/null +++ b/src/Builder/Expression/BitOrOperator.php @@ -0,0 +1,52 @@ + $expression */ + public readonly array $expression; + + /** + * @param Int64|ResolvesToInt|ResolvesToLong|int ...$expression + * @no-named-arguments + */ + public function __construct(Int64|ResolvesToInt|ResolvesToLong|int ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$bitOr'; + } +} diff --git a/src/Builder/Expression/BitXorOperator.php b/src/Builder/Expression/BitXorOperator.php new file mode 100644 index 000000000..5382065b6 --- /dev/null +++ b/src/Builder/Expression/BitXorOperator.php @@ -0,0 +1,52 @@ + $expression */ + public readonly array $expression; + + /** + * @param Int64|ResolvesToInt|ResolvesToLong|int ...$expression + * @no-named-arguments + */ + public function __construct(Int64|ResolvesToInt|ResolvesToLong|int ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$bitXor'; + } +} diff --git a/src/Builder/Expression/BoolFieldPath.php b/src/Builder/Expression/BoolFieldPath.php new file mode 100644 index 000000000..1dff80de0 --- /dev/null +++ b/src/Builder/Expression/BoolFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/BsonSizeOperator.php b/src/Builder/Expression/BsonSizeOperator.php new file mode 100644 index 000000000..bdfb6f694 --- /dev/null +++ b/src/Builder/Expression/BsonSizeOperator.php @@ -0,0 +1,41 @@ +object = $object; + } + + public function getOperator(): string + { + return '$bsonSize'; + } +} diff --git a/src/Builder/Expression/CaseOperator.php b/src/Builder/Expression/CaseOperator.php new file mode 100644 index 000000000..463a3b741 --- /dev/null +++ b/src/Builder/Expression/CaseOperator.php @@ -0,0 +1,49 @@ +case = $case; + $this->then = $then; + } + + public function getOperator(): string + { + return '$case'; + } +} diff --git a/src/Builder/Expression/CeilOperator.php b/src/Builder/Expression/CeilOperator.php new file mode 100644 index 000000000..95a01aa14 --- /dev/null +++ b/src/Builder/Expression/CeilOperator.php @@ -0,0 +1,40 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$ceil'; + } +} diff --git a/src/Builder/Expression/CmpOperator.php b/src/Builder/Expression/CmpOperator.php new file mode 100644 index 000000000..78354cb20 --- /dev/null +++ b/src/Builder/Expression/CmpOperator.php @@ -0,0 +1,48 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$cmp'; + } +} diff --git a/src/Builder/Expression/ConcatArraysOperator.php b/src/Builder/Expression/ConcatArraysOperator.php new file mode 100644 index 000000000..f8b790d52 --- /dev/null +++ b/src/Builder/Expression/ConcatArraysOperator.php @@ -0,0 +1,52 @@ + $array */ + public readonly array $array; + + /** + * @param BSONArray|PackedArray|ResolvesToArray|array ...$array + * @no-named-arguments + */ + public function __construct(PackedArray|ResolvesToArray|BSONArray|array ...$array) + { + if (\count($array) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $array, got %d.', 1, \count($array))); + } + + if (! array_is_list($array)) { + throw new InvalidArgumentException('Expected $array arguments to be a list (array), named arguments are not supported'); + } + + $this->array = $array; + } + + public function getOperator(): string + { + return '$concatArrays'; + } +} diff --git a/src/Builder/Expression/ConcatOperator.php b/src/Builder/Expression/ConcatOperator.php new file mode 100644 index 000000000..d9f0d9168 --- /dev/null +++ b/src/Builder/Expression/ConcatOperator.php @@ -0,0 +1,50 @@ + $expression */ + public readonly array $expression; + + /** + * @param ResolvesToString|string ...$expression + * @no-named-arguments + */ + public function __construct(ResolvesToString|string ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$concat'; + } +} diff --git a/src/Builder/Expression/CondOperator.php b/src/Builder/Expression/CondOperator.php new file mode 100644 index 000000000..1d3a44c58 --- /dev/null +++ b/src/Builder/Expression/CondOperator.php @@ -0,0 +1,54 @@ +if = $if; + $this->then = $then; + $this->else = $else; + } + + public function getOperator(): string + { + return '$cond'; + } +} diff --git a/src/Builder/Expression/ConvertOperator.php b/src/Builder/Expression/ConvertOperator.php new file mode 100644 index 000000000..f8cb5210e --- /dev/null +++ b/src/Builder/Expression/ConvertOperator.php @@ -0,0 +1,70 @@ +input = $input; + $this->to = $to; + $this->onError = $onError; + $this->onNull = $onNull; + } + + public function getOperator(): string + { + return '$convert'; + } +} diff --git a/src/Builder/Expression/CosOperator.php b/src/Builder/Expression/CosOperator.php new file mode 100644 index 000000000..d3fe010e2 --- /dev/null +++ b/src/Builder/Expression/CosOperator.php @@ -0,0 +1,44 @@ + resolves to a 128-bit decimal value. + */ + public readonly Decimal128|Int64|ResolvesToNumber|float|int $expression; + + /** + * @param Decimal128|Int64|ResolvesToNumber|float|int $expression $cos takes any valid expression that resolves to a number. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the result to radians. + * By default $cos returns values as a double. $cos can also return values as a 128-bit decimal as long as the resolves to a 128-bit decimal value. + */ + public function __construct(Decimal128|Int64|ResolvesToNumber|float|int $expression) + { + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$cos'; + } +} diff --git a/src/Builder/Expression/CoshOperator.php b/src/Builder/Expression/CoshOperator.php new file mode 100644 index 000000000..53530064f --- /dev/null +++ b/src/Builder/Expression/CoshOperator.php @@ -0,0 +1,44 @@ + resolves to a 128-bit decimal value. + */ + public readonly Decimal128|Int64|ResolvesToNumber|float|int $expression; + + /** + * @param Decimal128|Int64|ResolvesToNumber|float|int $expression $cosh takes any valid expression that resolves to a number, measured in radians. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the value to radians. + * By default $cosh returns values as a double. $cosh can also return values as a 128-bit decimal if the resolves to a 128-bit decimal value. + */ + public function __construct(Decimal128|Int64|ResolvesToNumber|float|int $expression) + { + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$cosh'; + } +} diff --git a/src/Builder/Expression/DateAddOperator.php b/src/Builder/Expression/DateAddOperator.php new file mode 100644 index 000000000..b68484663 --- /dev/null +++ b/src/Builder/Expression/DateAddOperator.php @@ -0,0 +1,63 @@ +startDate = $startDate; + $this->unit = $unit; + $this->amount = $amount; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$dateAdd'; + } +} diff --git a/src/Builder/Expression/DateDiffOperator.php b/src/Builder/Expression/DateDiffOperator.php new file mode 100644 index 000000000..a13e484fc --- /dev/null +++ b/src/Builder/Expression/DateDiffOperator.php @@ -0,0 +1,68 @@ +startDate = $startDate; + $this->endDate = $endDate; + $this->unit = $unit; + $this->timezone = $timezone; + $this->startOfWeek = $startOfWeek; + } + + public function getOperator(): string + { + return '$dateDiff'; + } +} diff --git a/src/Builder/Expression/DateFieldPath.php b/src/Builder/Expression/DateFieldPath.php new file mode 100644 index 000000000..d32988537 --- /dev/null +++ b/src/Builder/Expression/DateFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/DateFromPartsOperator.php b/src/Builder/Expression/DateFromPartsOperator.php new file mode 100644 index 000000000..f8b7ad639 --- /dev/null +++ b/src/Builder/Expression/DateFromPartsOperator.php @@ -0,0 +1,102 @@ +year = $year; + $this->isoWeekYear = $isoWeekYear; + $this->month = $month; + $this->isoWeek = $isoWeek; + $this->day = $day; + $this->isoDayOfWeek = $isoDayOfWeek; + $this->hour = $hour; + $this->minute = $minute; + $this->second = $second; + $this->millisecond = $millisecond; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$dateFromParts'; + } +} diff --git a/src/Builder/Expression/DateFromStringOperator.php b/src/Builder/Expression/DateFromStringOperator.php new file mode 100644 index 000000000..5a3f808f0 --- /dev/null +++ b/src/Builder/Expression/DateFromStringOperator.php @@ -0,0 +1,79 @@ +dateString = $dateString; + $this->format = $format; + $this->timezone = $timezone; + $this->onError = $onError; + $this->onNull = $onNull; + } + + public function getOperator(): string + { + return '$dateFromString'; + } +} diff --git a/src/Builder/Expression/DateSubtractOperator.php b/src/Builder/Expression/DateSubtractOperator.php new file mode 100644 index 000000000..90efa4846 --- /dev/null +++ b/src/Builder/Expression/DateSubtractOperator.php @@ -0,0 +1,63 @@ +startDate = $startDate; + $this->unit = $unit; + $this->amount = $amount; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$dateSubtract'; + } +} diff --git a/src/Builder/Expression/DateToPartsOperator.php b/src/Builder/Expression/DateToPartsOperator.php new file mode 100644 index 000000000..c0f637259 --- /dev/null +++ b/src/Builder/Expression/DateToPartsOperator.php @@ -0,0 +1,55 @@ +date = $date; + $this->timezone = $timezone; + $this->iso8601 = $iso8601; + } + + public function getOperator(): string + { + return '$dateToParts'; + } +} diff --git a/src/Builder/Expression/DateToStringOperator.php b/src/Builder/Expression/DateToStringOperator.php new file mode 100644 index 000000000..e33c519fd --- /dev/null +++ b/src/Builder/Expression/DateToStringOperator.php @@ -0,0 +1,72 @@ +date = $date; + $this->format = $format; + $this->timezone = $timezone; + $this->onNull = $onNull; + } + + public function getOperator(): string + { + return '$dateToString'; + } +} diff --git a/src/Builder/Expression/DateTruncOperator.php b/src/Builder/Expression/DateTruncOperator.php new file mode 100644 index 000000000..78be17040 --- /dev/null +++ b/src/Builder/Expression/DateTruncOperator.php @@ -0,0 +1,82 @@ +date = $date; + $this->unit = $unit; + $this->binSize = $binSize; + $this->timezone = $timezone; + $this->startOfWeek = $startOfWeek; + } + + public function getOperator(): string + { + return '$dateTrunc'; + } +} diff --git a/src/Builder/Expression/DayOfMonthOperator.php b/src/Builder/Expression/DayOfMonthOperator.php new file mode 100644 index 000000000..a1a06db6b --- /dev/null +++ b/src/Builder/Expression/DayOfMonthOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$dayOfMonth'; + } +} diff --git a/src/Builder/Expression/DayOfWeekOperator.php b/src/Builder/Expression/DayOfWeekOperator.php new file mode 100644 index 000000000..8b439c02c --- /dev/null +++ b/src/Builder/Expression/DayOfWeekOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$dayOfWeek'; + } +} diff --git a/src/Builder/Expression/DayOfYearOperator.php b/src/Builder/Expression/DayOfYearOperator.php new file mode 100644 index 000000000..d97be270d --- /dev/null +++ b/src/Builder/Expression/DayOfYearOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$dayOfYear'; + } +} diff --git a/src/Builder/Expression/DecimalFieldPath.php b/src/Builder/Expression/DecimalFieldPath.php new file mode 100644 index 000000000..2a9136675 --- /dev/null +++ b/src/Builder/Expression/DecimalFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/DegreesToRadiansOperator.php b/src/Builder/Expression/DegreesToRadiansOperator.php new file mode 100644 index 000000000..a9329e16e --- /dev/null +++ b/src/Builder/Expression/DegreesToRadiansOperator.php @@ -0,0 +1,44 @@ + resolves to a 128-bit decimal value. + */ + public readonly Decimal128|Int64|ResolvesToNumber|float|int $expression; + + /** + * @param Decimal128|Int64|ResolvesToNumber|float|int $expression $degreesToRadians takes any valid expression that resolves to a number. + * By default $degreesToRadians returns values as a double. $degreesToRadians can also return values as a 128-bit decimal as long as the resolves to a 128-bit decimal value. + */ + public function __construct(Decimal128|Int64|ResolvesToNumber|float|int $expression) + { + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$degreesToRadians'; + } +} diff --git a/src/Builder/Expression/DivideOperator.php b/src/Builder/Expression/DivideOperator.php new file mode 100644 index 000000000..bda2f503a --- /dev/null +++ b/src/Builder/Expression/DivideOperator.php @@ -0,0 +1,47 @@ +dividend = $dividend; + $this->divisor = $divisor; + } + + public function getOperator(): string + { + return '$divide'; + } +} diff --git a/src/Builder/Expression/DoubleFieldPath.php b/src/Builder/Expression/DoubleFieldPath.php new file mode 100644 index 000000000..2af25b87c --- /dev/null +++ b/src/Builder/Expression/DoubleFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/EqOperator.php b/src/Builder/Expression/EqOperator.php new file mode 100644 index 000000000..5da15a3c3 --- /dev/null +++ b/src/Builder/Expression/EqOperator.php @@ -0,0 +1,48 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$eq'; + } +} diff --git a/src/Builder/Expression/ExpOperator.php b/src/Builder/Expression/ExpOperator.php new file mode 100644 index 000000000..a168a6acd --- /dev/null +++ b/src/Builder/Expression/ExpOperator.php @@ -0,0 +1,40 @@ +exponent = $exponent; + } + + public function getOperator(): string + { + return '$exp'; + } +} diff --git a/src/Builder/Expression/ExpressionFactoryTrait.php b/src/Builder/Expression/ExpressionFactoryTrait.php new file mode 100644 index 000000000..3f5f04339 --- /dev/null +++ b/src/Builder/Expression/ExpressionFactoryTrait.php @@ -0,0 +1,105 @@ + resolves to a 128-bit decimal value. + */ + public static function cos(Decimal128|Int64|ResolvesToNumber|float|int $expression): CosOperator + { + return new CosOperator($expression); + } + + /** + * Returns the hyperbolic cosine of a value that is measured in radians. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/cosh/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $expression $cosh takes any valid expression that resolves to a number, measured in radians. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the value to radians. + * By default $cosh returns values as a double. $cosh can also return values as a 128-bit decimal if the resolves to a 128-bit decimal value. + */ + public static function cosh(Decimal128|Int64|ResolvesToNumber|float|int $expression): CoshOperator + { + return new CoshOperator($expression); + } + + /** + * Adds a number of time units to a date object. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateAdd/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $startDate The beginning date, in UTC, for the addition operation. The startDate can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param ResolvesToString|TimeUnit|string $unit The unit used to measure the amount of time added to the startDate. + * @param Int64|ResolvesToInt|ResolvesToLong|int $amount + * @param Optional|ResolvesToString|string $timezone The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function dateAdd( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $startDate, + ResolvesToString|TimeUnit|string $unit, + Int64|ResolvesToInt|ResolvesToLong|int $amount, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): DateAddOperator { + return new DateAddOperator($startDate, $unit, $amount, $timezone); + } + + /** + * Returns the difference between two dates. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateDiff/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $startDate The start of the time period. The startDate can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $endDate The end of the time period. The endDate can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param ResolvesToString|TimeUnit|string $unit The time measurement unit between the startDate and endDate + * @param Optional|ResolvesToString|string $timezone The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + * @param Optional|ResolvesToString|string $startOfWeek Used when the unit is equal to week. Defaults to Sunday. The startOfWeek parameter is an expression that resolves to a case insensitive string + */ + public static function dateDiff( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $startDate, + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $endDate, + ResolvesToString|TimeUnit|string $unit, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + Optional|ResolvesToString|string $startOfWeek = Optional::Undefined, + ): DateDiffOperator { + return new DateDiffOperator($startDate, $endDate, $unit, $timezone, $startOfWeek); + } + + /** + * Constructs a BSON Date object given the date's constituent parts. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateFromParts/ + * @param Optional|Decimal128|Int64|ResolvesToNumber|float|int $year Calendar year. Can be any expression that evaluates to a number. + * @param Optional|Decimal128|Int64|ResolvesToNumber|float|int $isoWeekYear ISO Week Date Year. Can be any expression that evaluates to a number. + * @param Optional|Decimal128|Int64|ResolvesToNumber|float|int $month Month. Defaults to 1. + * @param Optional|Decimal128|Int64|ResolvesToNumber|float|int $isoWeek Week of year. Defaults to 1. + * @param Optional|Decimal128|Int64|ResolvesToNumber|float|int $day Day of month. Defaults to 1. + * @param Optional|Decimal128|Int64|ResolvesToNumber|float|int $isoDayOfWeek Day of week (Monday 1 - Sunday 7). Defaults to 1. + * @param Optional|Decimal128|Int64|ResolvesToNumber|float|int $hour Hour. Defaults to 0. + * @param Optional|Decimal128|Int64|ResolvesToNumber|float|int $minute Minute. Defaults to 0. + * @param Optional|Decimal128|Int64|ResolvesToNumber|float|int $second Second. Defaults to 0. + * @param Optional|Decimal128|Int64|ResolvesToNumber|float|int $millisecond Millisecond. Defaults to 0. + * @param Optional|ResolvesToString|string $timezone The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function dateFromParts( + Optional|Decimal128|Int64|ResolvesToNumber|float|int $year = Optional::Undefined, + Optional|Decimal128|Int64|ResolvesToNumber|float|int $isoWeekYear = Optional::Undefined, + Optional|Decimal128|Int64|ResolvesToNumber|float|int $month = Optional::Undefined, + Optional|Decimal128|Int64|ResolvesToNumber|float|int $isoWeek = Optional::Undefined, + Optional|Decimal128|Int64|ResolvesToNumber|float|int $day = Optional::Undefined, + Optional|Decimal128|Int64|ResolvesToNumber|float|int $isoDayOfWeek = Optional::Undefined, + Optional|Decimal128|Int64|ResolvesToNumber|float|int $hour = Optional::Undefined, + Optional|Decimal128|Int64|ResolvesToNumber|float|int $minute = Optional::Undefined, + Optional|Decimal128|Int64|ResolvesToNumber|float|int $second = Optional::Undefined, + Optional|Decimal128|Int64|ResolvesToNumber|float|int $millisecond = Optional::Undefined, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): DateFromPartsOperator { + return new DateFromPartsOperator($year, $isoWeekYear, $month, $isoWeek, $day, $isoDayOfWeek, $hour, $minute, $second, $millisecond, $timezone); + } + + /** + * Converts a date/time string to a date object. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateFromString/ + * @param ResolvesToString|string $dateString The date/time string to convert to a date object. + * @param Optional|ResolvesToString|string $format The date format specification of the dateString. The format can be any expression that evaluates to a string literal, containing 0 or more format specifiers. + * If unspecified, $dateFromString uses "%Y-%m-%dT%H:%M:%S.%LZ" as the default format but accepts a variety of formats and attempts to parse the dateString if possible. + * @param Optional|ResolvesToString|string $timezone The time zone to use to format the date. + * @param Optional|ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $onError If $dateFromString encounters an error while parsing the given dateString, it outputs the result value of the provided onError expression. This result value can be of any type. + * If you do not specify onError, $dateFromString throws an error if it cannot parse dateString. + * @param Optional|ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $onNull If the dateString provided to $dateFromString is null or missing, it outputs the result value of the provided onNull expression. This result value can be of any type. + * If you do not specify onNull and dateString is null or missing, then $dateFromString outputs null. + */ + public static function dateFromString( + ResolvesToString|string $dateString, + Optional|ResolvesToString|string $format = Optional::Undefined, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + Optional|Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $onError = Optional::Undefined, + Optional|Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $onNull = Optional::Undefined, + ): DateFromStringOperator { + return new DateFromStringOperator($dateString, $format, $timezone, $onError, $onNull); + } + + /** + * Subtracts a number of time units from a date object. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateSubtract/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $startDate The beginning date, in UTC, for the addition operation. The startDate can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param ResolvesToString|TimeUnit|string $unit The unit used to measure the amount of time added to the startDate. + * @param Int64|ResolvesToInt|ResolvesToLong|int $amount + * @param Optional|ResolvesToString|string $timezone The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function dateSubtract( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $startDate, + ResolvesToString|TimeUnit|string $unit, + Int64|ResolvesToInt|ResolvesToLong|int $amount, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): DateSubtractOperator { + return new DateSubtractOperator($startDate, $unit, $amount, $timezone); + } + + /** + * Returns a document containing the constituent parts of a date. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToParts/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The input date for which to return parts. date can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + * @param Optional|bool $iso8601 If set to true, modifies the output document to use ISO week date fields. Defaults to false. + */ + public static function dateToParts( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + Optional|bool $iso8601 = Optional::Undefined, + ): DateToPartsOperator { + return new DateToPartsOperator($date, $timezone, $iso8601); + } + + /** + * Returns the date as a formatted string. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateToString/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to convert to string. Must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $format The date format specification of the dateString. The format can be any expression that evaluates to a string literal, containing 0 or more format specifiers. + * If unspecified, $dateFromString uses "%Y-%m-%dT%H:%M:%S.%LZ" as the default format but accepts a variety of formats and attempts to parse the dateString if possible. + * @param Optional|ResolvesToString|string $timezone The time zone to use to format the date. + * @param Optional|ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $onNull The value to return if the date is null or missing. + * If unspecified, $dateToString returns null if the date is null or missing. + */ + public static function dateToString( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $format = Optional::Undefined, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + Optional|Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $onNull = Optional::Undefined, + ): DateToStringOperator { + return new DateToStringOperator($date, $format, $timezone, $onNull); + } + + /** + * Truncates a date. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/dateTrunc/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to truncate, specified in UTC. The date can be any expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param ResolvesToString|TimeUnit|string $unit The unit of time, specified as an expression that must resolve to one of these strings: year, quarter, week, month, day, hour, minute, second. + * Together, binSize and unit specify the time period used in the $dateTrunc calculation. + * @param Optional|Decimal128|Int64|ResolvesToNumber|float|int $binSize The numeric time value, specified as an expression that must resolve to a positive non-zero number. Defaults to 1. + * Together, binSize and unit specify the time period used in the $dateTrunc calculation. + * @param Optional|ResolvesToString|string $timezone The timezone to carry out the operation. $timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + * @param Optional|string $startOfWeek The start of the week. Used when + * unit is week. Defaults to Sunday. + */ + public static function dateTrunc( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + ResolvesToString|TimeUnit|string $unit, + Optional|Decimal128|Int64|ResolvesToNumber|float|int $binSize = Optional::Undefined, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + Optional|string $startOfWeek = Optional::Undefined, + ): DateTruncOperator { + return new DateTruncOperator($date, $unit, $binSize, $timezone, $startOfWeek); + } + + /** + * Returns the day of the month for a date as a number between 1 and 31. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfMonth/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function dayOfMonth( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): DayOfMonthOperator { + return new DayOfMonthOperator($date, $timezone); + } + + /** + * Returns the day of the week for a date as a number between 1 (Sunday) and 7 (Saturday). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfWeek/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function dayOfWeek( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): DayOfWeekOperator { + return new DayOfWeekOperator($date, $timezone); + } + + /** + * Returns the day of the year for a date as a number between 1 and 366 (leap year). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/dayOfYear/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function dayOfYear( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): DayOfYearOperator { + return new DayOfYearOperator($date, $timezone); + } + + /** + * Converts a value from degrees to radians. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/degreesToRadians/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $expression $degreesToRadians takes any valid expression that resolves to a number. + * By default $degreesToRadians returns values as a double. $degreesToRadians can also return values as a 128-bit decimal as long as the resolves to a 128-bit decimal value. + */ + public static function degreesToRadians( + Decimal128|Int64|ResolvesToNumber|float|int $expression, + ): DegreesToRadiansOperator { + return new DegreesToRadiansOperator($expression); + } + + /** + * Returns the result of dividing the first number by the second. Accepts two argument expressions. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/divide/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $dividend The first argument is the dividend, and the second argument is the divisor; i.e. the first argument is divided by the second argument. + * @param Decimal128|Int64|ResolvesToNumber|float|int $divisor + */ + public static function divide( + Decimal128|Int64|ResolvesToNumber|float|int $dividend, + Decimal128|Int64|ResolvesToNumber|float|int $divisor, + ): DivideOperator { + return new DivideOperator($dividend, $divisor); + } + + /** + * Returns true if the values are equivalent. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/eq/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression1 + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression2 + */ + public static function eq( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression1, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression2, + ): EqOperator { + return new EqOperator($expression1, $expression2); + } + + /** + * Raises e to the specified exponent. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/exp/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $exponent + */ + public static function exp(Decimal128|Int64|ResolvesToNumber|float|int $exponent): ExpOperator + { + return new ExpOperator($exponent); + } + + /** + * Selects a subset of the array to return an array with only the elements that match the filter condition. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/ + * @param BSONArray|PackedArray|ResolvesToArray|array $input + * @param ResolvesToBool|bool $cond An expression that resolves to a boolean value used to determine if an element should be included in the output array. The expression references each element of the input array individually with the variable name specified in as. + * @param Optional|string $as A name for the variable that represents each individual element of the input array. If no name is specified, the variable name defaults to this. + * @param Optional|ResolvesToInt|int $limit A number expression that restricts the number of matching array elements that $filter returns. You cannot specify a limit less than 1. The matching array elements are returned in the order they appear in the input array. + * If the specified limit is greater than the number of matching array elements, $filter returns all matching array elements. If the limit is null, $filter returns all matching array elements. + */ + public static function filter( + PackedArray|ResolvesToArray|BSONArray|array $input, + ResolvesToBool|bool $cond, + Optional|string $as = Optional::Undefined, + Optional|ResolvesToInt|int $limit = Optional::Undefined, + ): FilterOperator { + return new FilterOperator($input, $cond, $as, $limit); + } + + /** + * Returns the result of an expression for the first document in an array. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/first/ + * @param BSONArray|PackedArray|ResolvesToArray|array $expression + */ + public static function first(PackedArray|ResolvesToArray|BSONArray|array $expression): FirstOperator + { + return new FirstOperator($expression); + } + + /** + * Returns a specified number of elements from the beginning of an array. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/firstN-array-element/ + * @param ResolvesToInt|int $n An expression that resolves to a positive integer. The integer specifies the number of array elements that $firstN returns. + * @param BSONArray|PackedArray|ResolvesToArray|array $input An expression that resolves to the array from which to return n elements. + */ + public static function firstN( + ResolvesToInt|int $n, + PackedArray|ResolvesToArray|BSONArray|array $input, + ): FirstNOperator { + return new FirstNOperator($n, $input); + } + + /** + * Returns the largest integer less than or equal to the specified number. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/floor/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $expression + */ + public static function floor(Decimal128|Int64|ResolvesToNumber|float|int $expression): FloorOperator + { + return new FloorOperator($expression); + } + + /** + * Defines a custom function. + * New in MongoDB 4.4. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/function/ + * @param Javascript|string $body The function definition. You can specify the function definition as either BSON\JavaScript or string. + * function(arg1, arg2, ...) { ... } + * @param BSONArray|PackedArray|array $args Arguments passed to the function body. If the body function does not take an argument, you can specify an empty array [ ]. + * @param string $lang + */ + public static function function( + Javascript|string $body, + PackedArray|BSONArray|array $args = [], + string $lang = 'js', + ): FunctionOperator { + return new FunctionOperator($body, $args, $lang); + } + + /** + * Returns the value of a specified field from a document. You can use $getField to retrieve the value of fields with names that contain periods (.) or start with dollar signs ($). + * New in MongoDB 5.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/getField/ + * @param ResolvesToString|string $field Field in the input object for which you want to return a value. field can be any valid expression that resolves to a string constant. + * If field begins with a dollar sign ($), place the field name inside of a $literal expression to return its value. + * @param Optional|ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $input Default: $$CURRENT + * A valid expression that contains the field for which you want to return a value. input must resolve to an object, missing, null, or undefined. If omitted, defaults to the document currently being processed in the pipeline ($$CURRENT). + */ + public static function getField( + ResolvesToString|string $field, + Optional|Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $input = Optional::Undefined, + ): GetFieldOperator { + return new GetFieldOperator($field, $input); + } + + /** + * Returns true if the first value is greater than the second. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/gt/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression1 + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression2 + */ + public static function gt( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression1, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression2, + ): GtOperator { + return new GtOperator($expression1, $expression2); + } + + /** + * Returns true if the first value is greater than or equal to the second. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/gte/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression1 + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression2 + */ + public static function gte( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression1, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression2, + ): GteOperator { + return new GteOperator($expression1, $expression2); + } + + /** + * Returns the hour for a date as a number between 0 and 23. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/hour/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function hour( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): HourOperator { + return new HourOperator($date, $timezone); + } + + /** + * Returns either the non-null result of the first expression or the result of the second expression if the first expression results in a null result. Null result encompasses instances of undefined values or missing fields. Accepts two expressions as arguments. The result of the second expression can be null. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/ifNull/ + * @no-named-arguments + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$expression + */ + public static function ifNull( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression, + ): IfNullOperator { + return new IfNullOperator(...$expression); + } + + /** + * Returns a boolean indicating whether a specified value is in an array. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/in/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression Any valid expression expression. + * @param BSONArray|PackedArray|ResolvesToArray|array $array Any valid expression that resolves to an array. + */ + public static function in( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + PackedArray|ResolvesToArray|BSONArray|array $array, + ): InOperator { + return new InOperator($expression, $array); + } + + /** + * Searches an array for an occurrence of a specified value and returns the array index of the first occurrence. Array indexes start at zero. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexOfArray/ + * @param BSONArray|PackedArray|ResolvesToArray|array $array Can be any valid expression as long as it resolves to an array. + * If the array expression resolves to a value of null or refers to a field that is missing, $indexOfArray returns null. + * If the array expression does not resolve to an array or null nor refers to a missing field, $indexOfArray returns an error. + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $search + * @param Optional|ResolvesToInt|int $start An integer, or a number that can be represented as integers (such as 2.0), that specifies the starting index position for the search. Can be any valid expression that resolves to a non-negative integral number. + * If unspecified, the starting index position for the search is the beginning of the string. + * @param Optional|ResolvesToInt|int $end An integer, or a number that can be represented as integers (such as 2.0), that specifies the ending index position for the search. Can be any valid expression that resolves to a non-negative integral number. If you specify a index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + * If unspecified, the ending index position for the search is the end of the string. + */ + public static function indexOfArray( + PackedArray|ResolvesToArray|BSONArray|array $array, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $search, + Optional|ResolvesToInt|int $start = Optional::Undefined, + Optional|ResolvesToInt|int $end = Optional::Undefined, + ): IndexOfArrayOperator { + return new IndexOfArrayOperator($array, $search, $start, $end); + } + + /** + * Searches a string for an occurrence of a substring and returns the UTF-8 byte index of the first occurrence. If the substring is not found, returns -1. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexOfBytes/ + * @param ResolvesToString|string $string Can be any valid expression as long as it resolves to a string. + * If the string expression resolves to a value of null or refers to a field that is missing, $indexOfBytes returns null. + * If the string expression does not resolve to a string or null nor refers to a missing field, $indexOfBytes returns an error. + * @param ResolvesToString|string $substring Can be any valid expression as long as it resolves to a string. + * @param Optional|ResolvesToInt|int $start An integer, or a number that can be represented as integers (such as 2.0), that specifies the starting index position for the search. Can be any valid expression that resolves to a non-negative integral number. + * If unspecified, the starting index position for the search is the beginning of the string. + * @param Optional|ResolvesToInt|int $end An integer, or a number that can be represented as integers (such as 2.0), that specifies the ending index position for the search. Can be any valid expression that resolves to a non-negative integral number. If you specify a index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + * If unspecified, the ending index position for the search is the end of the string. + */ + public static function indexOfBytes( + ResolvesToString|string $string, + ResolvesToString|string $substring, + Optional|ResolvesToInt|int $start = Optional::Undefined, + Optional|ResolvesToInt|int $end = Optional::Undefined, + ): IndexOfBytesOperator { + return new IndexOfBytesOperator($string, $substring, $start, $end); + } + + /** + * Searches a string for an occurrence of a substring and returns the UTF-8 code point index of the first occurrence. If the substring is not found, returns -1 + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexOfCP/ + * @param ResolvesToString|string $string Can be any valid expression as long as it resolves to a string. + * If the string expression resolves to a value of null or refers to a field that is missing, $indexOfCP returns null. + * If the string expression does not resolve to a string or null nor refers to a missing field, $indexOfCP returns an error. + * @param ResolvesToString|string $substring Can be any valid expression as long as it resolves to a string. + * @param Optional|ResolvesToInt|int $start An integer, or a number that can be represented as integers (such as 2.0), that specifies the starting index position for the search. Can be any valid expression that resolves to a non-negative integral number. + * If unspecified, the starting index position for the search is the beginning of the string. + * @param Optional|ResolvesToInt|int $end An integer, or a number that can be represented as integers (such as 2.0), that specifies the ending index position for the search. Can be any valid expression that resolves to a non-negative integral number. If you specify a index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + * If unspecified, the ending index position for the search is the end of the string. + */ + public static function indexOfCP( + ResolvesToString|string $string, + ResolvesToString|string $substring, + Optional|ResolvesToInt|int $start = Optional::Undefined, + Optional|ResolvesToInt|int $end = Optional::Undefined, + ): IndexOfCPOperator { + return new IndexOfCPOperator($string, $substring, $start, $end); + } + + /** + * Determines if the operand is an array. Returns a boolean. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/isArray/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function isArray( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): IsArrayOperator { + return new IsArrayOperator($expression); + } + + /** + * Returns boolean true if the specified expression resolves to an integer, decimal, double, or long. + * Returns boolean false if the expression resolves to any other BSON type, null, or a missing field. + * New in MongoDB 4.4. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/isNumber/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function isNumber( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): IsNumberOperator { + return new IsNumberOperator($expression); + } + + /** + * Returns the weekday number in ISO 8601 format, ranging from 1 (for Monday) to 7 (for Sunday). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/isoDayOfWeek/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function isoDayOfWeek( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): IsoDayOfWeekOperator { + return new IsoDayOfWeekOperator($date, $timezone); + } + + /** + * Returns the week number in ISO 8601 format, ranging from 1 to 53. Week numbers start at 1 with the week (Monday through Sunday) that contains the year's first Thursday. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/isoWeek/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function isoWeek( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): IsoWeekOperator { + return new IsoWeekOperator($date, $timezone); + } + + /** + * Returns the year number in ISO 8601 format. The year starts with the Monday of week 1 (ISO 8601) and ends with the Sunday of the last week (ISO 8601). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/isoWeekYear/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function isoWeekYear( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): IsoWeekYearOperator { + return new IsoWeekYearOperator($date, $timezone); + } + + /** + * Returns the result of an expression for the last document in an array. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/last/ + * @param BSONArray|PackedArray|ResolvesToArray|array $expression + */ + public static function last(PackedArray|ResolvesToArray|BSONArray|array $expression): LastOperator + { + return new LastOperator($expression); + } + + /** + * Returns a specified number of elements from the end of an array. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/lastN-array-element/ + * @param ResolvesToInt|int $n An expression that resolves to a positive integer. The integer specifies the number of array elements that $firstN returns. + * @param BSONArray|PackedArray|ResolvesToArray|array $input An expression that resolves to the array from which to return n elements. + */ + public static function lastN( + ResolvesToInt|int $n, + PackedArray|ResolvesToArray|BSONArray|array $input, + ): LastNOperator { + return new LastNOperator($n, $input); + } + + /** + * Defines variables for use within the scope of a subexpression and returns the result of the subexpression. Accepts named parameters. + * Accepts any number of argument expressions. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/let/ + * @param Document|Serializable|array|stdClass $vars Assignment block for the variables accessible in the in expression. To assign a variable, specify a string for the variable name and assign a valid expression for the value. + * The variable assignments have no meaning outside the in expression, not even within the vars block itself. + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $in The expression to evaluate. + */ + public static function let( + Document|Serializable|stdClass|array $vars, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $in, + ): LetOperator { + return new LetOperator($vars, $in); + } + + /** + * Return a value without parsing. Use for values that the aggregation pipeline may interpret as an expression. For example, use a $literal expression to a string that starts with a dollar sign ($) to avoid parsing as a field path. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/literal/ + * @param Type|array|bool|float|int|null|stdClass|string $value If the value is an expression, $literal does not evaluate the expression but instead returns the unparsed expression. + */ + public static function literal(Type|stdClass|array|bool|float|int|null|string $value): LiteralOperator + { + return new LiteralOperator($value); + } + + /** + * Calculates the natural log of a number. + * $ln is equivalent to $log: [ , Math.E ] expression, where Math.E is a JavaScript representation for Euler's number e. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/ln/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $number Any valid expression as long as it resolves to a non-negative number. For more information on expressions, see Expressions. + */ + public static function ln(Decimal128|Int64|ResolvesToNumber|float|int $number): LnOperator + { + return new LnOperator($number); + } + + /** + * Calculates the log of a number in the specified base. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/log/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $number Any valid expression as long as it resolves to a non-negative number. + * @param Decimal128|Int64|ResolvesToNumber|float|int $base Any valid expression as long as it resolves to a positive number greater than 1. + */ + public static function log( + Decimal128|Int64|ResolvesToNumber|float|int $number, + Decimal128|Int64|ResolvesToNumber|float|int $base, + ): LogOperator { + return new LogOperator($number, $base); + } + + /** + * Calculates the log base 10 of a number. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/log10/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $number Any valid expression as long as it resolves to a non-negative number. + */ + public static function log10(Decimal128|Int64|ResolvesToNumber|float|int $number): Log10Operator + { + return new Log10Operator($number); + } + + /** + * Returns true if the first value is less than the second. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/lt/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression1 + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression2 + */ + public static function lt( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression1, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression2, + ): LtOperator { + return new LtOperator($expression1, $expression2); + } + + /** + * Returns true if the first value is less than or equal to the second. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/lte/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression1 + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression2 + */ + public static function lte( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression1, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression2, + ): LteOperator { + return new LteOperator($expression1, $expression2); + } + + /** + * Removes whitespace or the specified characters from the beginning of a string. + * New in MongoDB 4.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/ltrim/ + * @param ResolvesToString|string $input The string to trim. The argument can be any valid expression that resolves to a string. + * @param Optional|ResolvesToString|string $chars The character(s) to trim from the beginning of the input. + * The argument can be any valid expression that resolves to a string. The $ltrim operator breaks down the string into individual UTF code point to trim from input. + * If unspecified, $ltrim removes whitespace characters, including the null character. + */ + public static function ltrim( + ResolvesToString|string $input, + Optional|ResolvesToString|string $chars = Optional::Undefined, + ): LtrimOperator { + return new LtrimOperator($input, $chars); + } + + /** + * Applies a subexpression to each element of an array and returns the array of resulting values in order. Accepts named parameters. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/map/ + * @param BSONArray|PackedArray|ResolvesToArray|array $input An expression that resolves to an array. + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $in An expression that is applied to each element of the input array. The expression references each element individually with the variable name specified in as. + * @param Optional|ResolvesToString|string $as A name for the variable that represents each individual element of the input array. If no name is specified, the variable name defaults to this. + */ + public static function map( + PackedArray|ResolvesToArray|BSONArray|array $input, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $in, + Optional|ResolvesToString|string $as = Optional::Undefined, + ): MapOperator { + return new MapOperator($input, $in, $as); + } + + /** + * Returns the maximum value that results from applying an expression to each document. + * Changed in MongoDB 5.0: Available in the $setWindowFields stage. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/max/ + * @no-named-arguments + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$expression + */ + public static function max( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression, + ): MaxOperator { + return new MaxOperator(...$expression); + } + + /** + * Returns the n largest values in an array. Distinct from the $maxN accumulator. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/maxN-array-element/ + * @param BSONArray|PackedArray|ResolvesToArray|array $input An expression that resolves to the array from which to return the maximal n elements. + * @param ResolvesToInt|int $n An expression that resolves to a positive integer. The integer specifies the number of array elements that $maxN returns. + */ + public static function maxN( + PackedArray|ResolvesToArray|BSONArray|array $input, + ResolvesToInt|int $n, + ): MaxNOperator { + return new MaxNOperator($input, $n); + } + + /** + * Returns an approximation of the median, the 50th percentile, as a scalar value. + * New in MongoDB 7.0. + * This operator is available as an accumulator in these stages: + * $group + * $setWindowFields + * It is also available as an aggregation expression. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/median/ + * @param BSONArray|Decimal128|Int64|PackedArray|ResolvesToNumber|array|float|int $input $median calculates the 50th percentile value of this data. input must be a field name or an expression that evaluates to a numeric type. If the expression cannot be converted to a numeric type, the $median calculation ignores it. + * @param string $method The method that mongod uses to calculate the 50th percentile value. The method must be 'approximate'. + */ + public static function median( + Decimal128|Int64|PackedArray|ResolvesToNumber|BSONArray|array|float|int $input, + string $method, + ): MedianOperator { + return new MedianOperator($input, $method); + } + + /** + * Combines multiple documents into a single document. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/mergeObjects/ + * @no-named-arguments + * @param Document|ResolvesToObject|Serializable|array|stdClass ...$document Any valid expression that resolves to a document. + */ + public static function mergeObjects( + Document|Serializable|ResolvesToObject|stdClass|array ...$document, + ): MergeObjectsOperator { + return new MergeObjectsOperator(...$document); + } + + /** + * Access available per-document metadata related to the aggregation operation. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/meta/ + * @param string $keyword + */ + public static function meta(string $keyword): MetaOperator + { + return new MetaOperator($keyword); + } + + /** + * Returns the milliseconds of a date as a number between 0 and 999. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/millisecond/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function millisecond( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): MillisecondOperator { + return new MillisecondOperator($date, $timezone); + } + + /** + * Returns the minimum value that results from applying an expression to each document. + * Changed in MongoDB 5.0: Available in the $setWindowFields stage. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/min/ + * @no-named-arguments + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$expression + */ + public static function min( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression, + ): MinOperator { + return new MinOperator(...$expression); + } + + /** + * Returns the n smallest values in an array. Distinct from the $minN accumulator. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/minN-array-element/ + * @param BSONArray|PackedArray|ResolvesToArray|array $input An expression that resolves to the array from which to return the maximal n elements. + * @param ResolvesToInt|int $n An expression that resolves to a positive integer. The integer specifies the number of array elements that $maxN returns. + */ + public static function minN( + PackedArray|ResolvesToArray|BSONArray|array $input, + ResolvesToInt|int $n, + ): MinNOperator { + return new MinNOperator($input, $n); + } + + /** + * Returns the minute for a date as a number between 0 and 59. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/minute/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function minute( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): MinuteOperator { + return new MinuteOperator($date, $timezone); + } + + /** + * Returns the remainder of the first number divided by the second. Accepts two argument expressions. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/mod/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $dividend The first argument is the dividend, and the second argument is the divisor; i.e. first argument is divided by the second argument. + * @param Decimal128|Int64|ResolvesToNumber|float|int $divisor + */ + public static function mod( + Decimal128|Int64|ResolvesToNumber|float|int $dividend, + Decimal128|Int64|ResolvesToNumber|float|int $divisor, + ): ModOperator { + return new ModOperator($dividend, $divisor); + } + + /** + * Returns the month for a date as a number between 1 (January) and 12 (December). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/month/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function month( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): MonthOperator { + return new MonthOperator($date, $timezone); + } + + /** + * Multiplies numbers to return the product. Accepts any number of argument expressions. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/multiply/ + * @no-named-arguments + * @param Decimal128|Int64|ResolvesToNumber|float|int ...$expression The arguments can be any valid expression as long as they resolve to numbers. + * Starting in MongoDB 6.1 you can optimize the $multiply operation. To improve performance, group references at the end of the argument list. + */ + public static function multiply(Decimal128|Int64|ResolvesToNumber|float|int ...$expression): MultiplyOperator + { + return new MultiplyOperator(...$expression); + } + + /** + * Returns true if the values are not equivalent. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/ne/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression1 + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression2 + */ + public static function ne( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression1, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression2, + ): NeOperator { + return new NeOperator($expression1, $expression2); + } + + /** + * Returns the boolean value that is the opposite of its argument expression. Accepts a single argument expression. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/not/ + * @param ExpressionInterface|ResolvesToBool|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function not( + Type|ResolvesToBool|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): NotOperator { + return new NotOperator($expression); + } + + /** + * Converts a document to an array of documents representing key-value pairs. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/objectToArray/ + * @param Document|ResolvesToObject|Serializable|array|stdClass $object Any valid expression as long as it resolves to a document object. $objectToArray applies to the top-level fields of its argument. If the argument is a document that itself contains embedded document fields, the $objectToArray does not recursively apply to the embedded document fields. + */ + public static function objectToArray( + Document|Serializable|ResolvesToObject|stdClass|array $object, + ): ObjectToArrayOperator { + return new ObjectToArrayOperator($object); + } + + /** + * Returns true when any of its expressions evaluates to true. Accepts any number of argument expressions. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/or/ + * @no-named-arguments + * @param ExpressionInterface|ResolvesToBool|Type|array|bool|float|int|null|stdClass|string ...$expression + */ + public static function or( + Type|ResolvesToBool|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression, + ): OrOperator { + return new OrOperator(...$expression); + } + + /** + * Returns an array of scalar values that correspond to specified percentile values. + * New in MongoDB 7.0. + * + * This operator is available as an accumulator in these stages: + * $group + * + * $setWindowFields + * + * It is also available as an aggregation expression. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/percentile/ + * @param BSONArray|Decimal128|Int64|PackedArray|ResolvesToNumber|array|float|int $input $percentile calculates the percentile values of this data. input must be a field name or an expression that evaluates to a numeric type. If the expression cannot be converted to a numeric type, the $percentile calculation ignores it. + * @param BSONArray|PackedArray|ResolvesToArray|array $p $percentile calculates a percentile value for each element in p. The elements represent percentages and must evaluate to numeric values in the range 0.0 to 1.0, inclusive. + * $percentile returns results in the same order as the elements in p. + * @param string $method The method that mongod uses to calculate the percentile value. The method must be 'approximate'. + */ + public static function percentile( + Decimal128|Int64|PackedArray|ResolvesToNumber|BSONArray|array|float|int $input, + PackedArray|ResolvesToArray|BSONArray|array $p, + string $method, + ): PercentileOperator { + return new PercentileOperator($input, $p, $method); + } + + /** + * Raises a number to the specified exponent. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/pow/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $number + * @param Decimal128|Int64|ResolvesToNumber|float|int $exponent + */ + public static function pow( + Decimal128|Int64|ResolvesToNumber|float|int $number, + Decimal128|Int64|ResolvesToNumber|float|int $exponent, + ): PowOperator { + return new PowOperator($number, $exponent); + } + + /** + * Converts a value from radians to degrees. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/radiansToDegrees/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $expression + */ + public static function radiansToDegrees( + Decimal128|Int64|ResolvesToNumber|float|int $expression, + ): RadiansToDegreesOperator { + return new RadiansToDegreesOperator($expression); + } + + /** + * Returns a random float between 0 and 1 + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/rand/ + */ + public static function rand(): RandOperator + { + return new RandOperator(); + } + + /** + * Outputs an array containing a sequence of integers according to user-defined inputs. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/range/ + * @param ResolvesToInt|int $start An integer that specifies the start of the sequence. Can be any valid expression that resolves to an integer. + * @param ResolvesToInt|int $end An integer that specifies the exclusive upper limit of the sequence. Can be any valid expression that resolves to an integer. + * @param Optional|ResolvesToInt|int $step An integer that specifies the increment value. Can be any valid expression that resolves to a non-zero integer. Defaults to 1. + */ + public static function range( + ResolvesToInt|int $start, + ResolvesToInt|int $end, + Optional|ResolvesToInt|int $step = Optional::Undefined, + ): RangeOperator { + return new RangeOperator($start, $end, $step); + } + + /** + * Applies an expression to each element in an array and combines them into a single value. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/reduce/ + * @param BSONArray|PackedArray|ResolvesToArray|array $input Can be any valid expression that resolves to an array. + * If the argument resolves to a value of null or refers to a missing field, $reduce returns null. + * If the argument does not resolve to an array or null nor refers to a missing field, $reduce returns an error. + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $initialValue The initial cumulative value set before in is applied to the first element of the input array. + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $in A valid expression that $reduce applies to each element in the input array in left-to-right order. Wrap the input value with $reverseArray to yield the equivalent of applying the combining expression from right-to-left. + * During evaluation of the in expression, two variables will be available: + * - value is the variable that represents the cumulative value of the expression. + * - this is the variable that refers to the element being processed. + */ + public static function reduce( + PackedArray|ResolvesToArray|BSONArray|array $input, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $initialValue, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $in, + ): ReduceOperator { + return new ReduceOperator($input, $initialValue, $in); + } + + /** + * Applies a regular expression (regex) to a string and returns information on the first matched substring. + * New in MongoDB 4.2. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexFind/ + * @param ResolvesToString|string $input The string on which you wish to apply the regex pattern. Can be a string or any valid expression that resolves to a string. + * @param Regex|ResolvesToString|string $regex The regex pattern to apply. Can be any valid expression that resolves to either a string or regex pattern //. When using the regex //, you can also specify the regex options i and m (but not the s or x options) + * @param Optional|string $options + */ + public static function regexFind( + ResolvesToString|string $input, + Regex|ResolvesToString|string $regex, + Optional|string $options = Optional::Undefined, + ): RegexFindOperator { + return new RegexFindOperator($input, $regex, $options); + } + + /** + * Applies a regular expression (regex) to a string and returns information on the all matched substrings. + * New in MongoDB 4.2. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexFindAll/ + * @param ResolvesToString|string $input The string on which you wish to apply the regex pattern. Can be a string or any valid expression that resolves to a string. + * @param Regex|ResolvesToString|string $regex The regex pattern to apply. Can be any valid expression that resolves to either a string or regex pattern //. When using the regex //, you can also specify the regex options i and m (but not the s or x options) + * @param Optional|string $options + */ + public static function regexFindAll( + ResolvesToString|string $input, + Regex|ResolvesToString|string $regex, + Optional|string $options = Optional::Undefined, + ): RegexFindAllOperator { + return new RegexFindAllOperator($input, $regex, $options); + } + + /** + * Applies a regular expression (regex) to a string and returns a boolean that indicates if a match is found or not. + * New in MongoDB 4.2. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/regexMatch/ + * @param ResolvesToString|string $input The string on which you wish to apply the regex pattern. Can be a string or any valid expression that resolves to a string. + * @param Regex|ResolvesToString|string $regex The regex pattern to apply. Can be any valid expression that resolves to either a string or regex pattern //. When using the regex //, you can also specify the regex options i and m (but not the s or x options) + * @param Optional|string $options + */ + public static function regexMatch( + ResolvesToString|string $input, + Regex|ResolvesToString|string $regex, + Optional|string $options = Optional::Undefined, + ): RegexMatchOperator { + return new RegexMatchOperator($input, $regex, $options); + } + + /** + * Replaces all instances of a search string in an input string with a replacement string. + * $replaceAll is both case-sensitive and diacritic-sensitive, and ignores any collation present on a collection. + * New in MongoDB 4.4. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceAll/ + * @param ResolvesToNull|ResolvesToString|null|string $input The string on which you wish to apply the find. Can be any valid expression that resolves to a string or a null. If input refers to a field that is missing, $replaceAll returns null. + * @param ResolvesToNull|ResolvesToString|null|string $find The string to search for within the given input. Can be any valid expression that resolves to a string or a null. If find refers to a field that is missing, $replaceAll returns null. + * @param ResolvesToNull|ResolvesToString|null|string $replacement The string to use to replace all matched instances of find in input. Can be any valid expression that resolves to a string or a null. + */ + public static function replaceAll( + ResolvesToNull|ResolvesToString|null|string $input, + ResolvesToNull|ResolvesToString|null|string $find, + ResolvesToNull|ResolvesToString|null|string $replacement, + ): ReplaceAllOperator { + return new ReplaceAllOperator($input, $find, $replacement); + } + + /** + * Replaces the first instance of a matched string in a given input. + * New in MongoDB 4.4. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceOne/ + * @param ResolvesToNull|ResolvesToString|null|string $input The string on which you wish to apply the find. Can be any valid expression that resolves to a string or a null. If input refers to a field that is missing, $replaceAll returns null. + * @param ResolvesToNull|ResolvesToString|null|string $find The string to search for within the given input. Can be any valid expression that resolves to a string or a null. If find refers to a field that is missing, $replaceAll returns null. + * @param ResolvesToNull|ResolvesToString|null|string $replacement The string to use to replace all matched instances of find in input. Can be any valid expression that resolves to a string or a null. + */ + public static function replaceOne( + ResolvesToNull|ResolvesToString|null|string $input, + ResolvesToNull|ResolvesToString|null|string $find, + ResolvesToNull|ResolvesToString|null|string $replacement, + ): ReplaceOneOperator { + return new ReplaceOneOperator($input, $find, $replacement); + } + + /** + * Returns an array with the elements in reverse order. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/reverseArray/ + * @param BSONArray|PackedArray|ResolvesToArray|array $expression The argument can be any valid expression as long as it resolves to an array. + */ + public static function reverseArray(PackedArray|ResolvesToArray|BSONArray|array $expression): ReverseArrayOperator + { + return new ReverseArrayOperator($expression); + } + + /** + * Rounds a number to to a whole integer or to a specified decimal place. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/round/ + * @param Decimal128|Int64|ResolvesToDecimal|ResolvesToDouble|ResolvesToInt|ResolvesToLong|float|int $number Can be any valid expression that resolves to a number. Specifically, the expression must resolve to an integer, double, decimal, or long. + * $round returns an error if the expression resolves to a non-numeric data type. + * @param Optional|ResolvesToInt|int $place Can be any valid expression that resolves to an integer between -20 and 100, exclusive. + */ + public static function round( + Decimal128|Int64|ResolvesToDecimal|ResolvesToDouble|ResolvesToInt|ResolvesToLong|float|int $number, + Optional|ResolvesToInt|int $place = Optional::Undefined, + ): RoundOperator { + return new RoundOperator($number, $place); + } + + /** + * Removes whitespace characters, including null, or the specified characters from the end of a string. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/rtrim/ + * @param ResolvesToString|string $input The string to trim. The argument can be any valid expression that resolves to a string. + * @param Optional|ResolvesToString|string $chars The character(s) to trim from the beginning of the input. + * The argument can be any valid expression that resolves to a string. The $ltrim operator breaks down the string into individual UTF code point to trim from input. + * If unspecified, $ltrim removes whitespace characters, including the null character. + */ + public static function rtrim( + ResolvesToString|string $input, + Optional|ResolvesToString|string $chars = Optional::Undefined, + ): RtrimOperator { + return new RtrimOperator($input, $chars); + } + + /** + * Returns the seconds for a date as a number between 0 and 60 (leap seconds). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/second/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function second( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): SecondOperator { + return new SecondOperator($date, $timezone); + } + + /** + * Returns a set with elements that appear in the first set but not in the second set; i.e. performs a relative complement of the second set relative to the first. Accepts exactly two argument expressions. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/setDifference/ + * @param BSONArray|PackedArray|ResolvesToArray|array $expression1 The arguments can be any valid expression as long as they each resolve to an array. + * @param BSONArray|PackedArray|ResolvesToArray|array $expression2 The arguments can be any valid expression as long as they each resolve to an array. + */ + public static function setDifference( + PackedArray|ResolvesToArray|BSONArray|array $expression1, + PackedArray|ResolvesToArray|BSONArray|array $expression2, + ): SetDifferenceOperator { + return new SetDifferenceOperator($expression1, $expression2); + } + + /** + * Returns true if the input sets have the same distinct elements. Accepts two or more argument expressions. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/setEquals/ + * @no-named-arguments + * @param BSONArray|PackedArray|ResolvesToArray|array ...$expression + */ + public static function setEquals(PackedArray|ResolvesToArray|BSONArray|array ...$expression): SetEqualsOperator + { + return new SetEqualsOperator(...$expression); + } + + /** + * Adds, updates, or removes a specified field in a document. You can use $setField to add, update, or remove fields with names that contain periods (.) or start with dollar signs ($). + * New in MongoDB 5.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/setField/ + * @param ResolvesToString|string $field Field in the input object that you want to add, update, or remove. field can be any valid expression that resolves to a string constant. + * @param Document|ResolvesToObject|Serializable|array|stdClass $input Document that contains the field that you want to add or update. input must resolve to an object, missing, null, or undefined. + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $value The value that you want to assign to field. value can be any valid expression. + * Set to $$REMOVE to remove field from the input document. + */ + public static function setField( + ResolvesToString|string $field, + Document|Serializable|ResolvesToObject|stdClass|array $input, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $value, + ): SetFieldOperator { + return new SetFieldOperator($field, $input, $value); + } + + /** + * Returns a set with elements that appear in all of the input sets. Accepts any number of argument expressions. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/setIntersection/ + * @no-named-arguments + * @param BSONArray|PackedArray|ResolvesToArray|array ...$expression + */ + public static function setIntersection( + PackedArray|ResolvesToArray|BSONArray|array ...$expression, + ): SetIntersectionOperator { + return new SetIntersectionOperator(...$expression); + } + + /** + * Returns true if all elements of the first set appear in the second set, including when the first set equals the second set; i.e. not a strict subset. Accepts exactly two argument expressions. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/setIsSubset/ + * @param BSONArray|PackedArray|ResolvesToArray|array $expression1 + * @param BSONArray|PackedArray|ResolvesToArray|array $expression2 + */ + public static function setIsSubset( + PackedArray|ResolvesToArray|BSONArray|array $expression1, + PackedArray|ResolvesToArray|BSONArray|array $expression2, + ): SetIsSubsetOperator { + return new SetIsSubsetOperator($expression1, $expression2); + } + + /** + * Returns a set with elements that appear in any of the input sets. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/setUnion/ + * @no-named-arguments + * @param BSONArray|PackedArray|ResolvesToArray|array ...$expression + */ + public static function setUnion(PackedArray|ResolvesToArray|BSONArray|array ...$expression): SetUnionOperator + { + return new SetUnionOperator(...$expression); + } + + /** + * Returns the sine of a value that is measured in radians. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sin/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $expression $sin takes any valid expression that resolves to a number. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the result to radians. + * By default $sin returns values as a double. $sin can also return values as a 128-bit decimal as long as the expression resolves to a 128-bit decimal value. + */ + public static function sin(Decimal128|Int64|ResolvesToNumber|float|int $expression): SinOperator + { + return new SinOperator($expression); + } + + /** + * Returns the hyperbolic sine of a value that is measured in radians. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sinh/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $expression $sinh takes any valid expression that resolves to a number, measured in radians. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the value to radians. + * By default $sinh returns values as a double. $sinh can also return values as a 128-bit decimal if the expression resolves to a 128-bit decimal value. + */ + public static function sinh(Decimal128|Int64|ResolvesToNumber|float|int $expression): SinhOperator + { + return new SinhOperator($expression); + } + + /** + * Returns the number of elements in the array. Accepts a single expression as argument. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/size/ + * @param BSONArray|PackedArray|ResolvesToArray|array $expression The argument for $size can be any expression as long as it resolves to an array. + */ + public static function size(PackedArray|ResolvesToArray|BSONArray|array $expression): SizeOperator + { + return new SizeOperator($expression); + } + + /** + * Returns a subset of an array. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/slice/ + * @param BSONArray|PackedArray|ResolvesToArray|array $expression Any valid expression as long as it resolves to an array. + * @param ResolvesToInt|int $n Any valid expression as long as it resolves to an integer. If position is specified, n must resolve to a positive integer. + * If positive, $slice returns up to the first n elements in the array. If the position is specified, $slice returns the first n elements starting from the position. + * If negative, $slice returns up to the last n elements in the array. n cannot resolve to a negative number if is specified. + * @param Optional|ResolvesToInt|int $position Any valid expression as long as it resolves to an integer. + * If positive, $slice determines the starting position from the start of the array. If position is greater than the number of elements, the $slice returns an empty array. + * If negative, $slice determines the starting position from the end of the array. If the absolute value of the is greater than the number of elements, the starting position is the start of the array. + */ + public static function slice( + PackedArray|ResolvesToArray|BSONArray|array $expression, + ResolvesToInt|int $n, + Optional|ResolvesToInt|int $position = Optional::Undefined, + ): SliceOperator { + return new SliceOperator($expression, $n, $position); + } + + /** + * Sorts the elements of an array. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/ + * @param BSONArray|PackedArray|ResolvesToArray|array $input The array to be sorted. + * The result is null if the expression: is missing, evaluates to null, or evaluates to undefined + * If the expression evaluates to any other non-array value, the document returns an error. + * @param Document|Serializable|Sort|array|int|stdClass $sortBy The document specifies a sort ordering. + */ + public static function sortArray( + PackedArray|ResolvesToArray|BSONArray|array $input, + Document|Serializable|Sort|stdClass|array|int $sortBy, + ): SortArrayOperator { + return new SortArrayOperator($input, $sortBy); + } + + /** + * Splits a string into substrings based on a delimiter. Returns an array of substrings. If the delimiter is not found within the string, returns an array containing the original string. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/split/ + * @param ResolvesToString|string $string The string to be split. string expression can be any valid expression as long as it resolves to a string. + * @param ResolvesToString|string $delimiter The delimiter to use when splitting the string expression. delimiter can be any valid expression as long as it resolves to a string. + */ + public static function split(ResolvesToString|string $string, ResolvesToString|string $delimiter): SplitOperator + { + return new SplitOperator($string, $delimiter); + } + + /** + * Calculates the square root. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sqrt/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $number The argument can be any valid expression as long as it resolves to a non-negative number. + */ + public static function sqrt(Decimal128|Int64|ResolvesToNumber|float|int $number): SqrtOperator + { + return new SqrtOperator($number); + } + + /** + * Calculates the population standard deviation of the input values. Use if the values encompass the entire population of data you want to represent and do not wish to generalize about a larger population. $stdDevPop ignores non-numeric values. + * If the values represent only a sample of a population of data from which to generalize about the population, use $stdDevSamp instead. + * Changed in MongoDB 5.0: Available in the $setWindowFields stage. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/stdDevPop/ + * @no-named-arguments + * @param Decimal128|Int64|ResolvesToNumber|float|int ...$expression + */ + public static function stdDevPop(Decimal128|Int64|ResolvesToNumber|float|int ...$expression): StdDevPopOperator + { + return new StdDevPopOperator(...$expression); + } + + /** + * Calculates the sample standard deviation of the input values. Use if the values encompass a sample of a population of data from which to generalize about the population. $stdDevSamp ignores non-numeric values. + * If the values represent the entire population of data or you do not wish to generalize about a larger population, use $stdDevPop instead. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/stdDevSamp/ + * @no-named-arguments + * @param Decimal128|Int64|ResolvesToNumber|float|int ...$expression + */ + public static function stdDevSamp(Decimal128|Int64|ResolvesToNumber|float|int ...$expression): StdDevSampOperator + { + return new StdDevSampOperator(...$expression); + } + + /** + * Performs case-insensitive string comparison and returns: 0 if two strings are equivalent, 1 if the first string is greater than the second, and -1 if the first string is less than the second. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/strcasecmp/ + * @param ResolvesToString|string $expression1 + * @param ResolvesToString|string $expression2 + */ + public static function strcasecmp( + ResolvesToString|string $expression1, + ResolvesToString|string $expression2, + ): StrcasecmpOperator { + return new StrcasecmpOperator($expression1, $expression2); + } + + /** + * Returns the number of UTF-8 encoded bytes in a string. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenBytes/ + * @param ResolvesToString|string $expression + */ + public static function strLenBytes(ResolvesToString|string $expression): StrLenBytesOperator + { + return new StrLenBytesOperator($expression); + } + + /** + * Returns the number of UTF-8 code points in a string. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenCP/ + * @param ResolvesToString|string $expression + */ + public static function strLenCP(ResolvesToString|string $expression): StrLenCPOperator + { + return new StrLenCPOperator($expression); + } + + /** + * Deprecated. Use $substrBytes or $substrCP. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/substr/ + * @param ResolvesToString|string $string + * @param ResolvesToInt|int $start If start is a negative number, $substr returns an empty string "". + * @param ResolvesToInt|int $length If length is a negative number, $substr returns a substring that starts at the specified index and includes the rest of the string. + */ + public static function substr( + ResolvesToString|string $string, + ResolvesToInt|int $start, + ResolvesToInt|int $length, + ): SubstrOperator { + return new SubstrOperator($string, $start, $length); + } + + /** + * Returns the substring of a string. Starts with the character at the specified UTF-8 byte index (zero-based) in the string and continues for the specified number of bytes. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrBytes/ + * @param ResolvesToString|string $string + * @param ResolvesToInt|int $start If start is a negative number, $substr returns an empty string "". + * @param ResolvesToInt|int $length If length is a negative number, $substr returns a substring that starts at the specified index and includes the rest of the string. + */ + public static function substrBytes( + ResolvesToString|string $string, + ResolvesToInt|int $start, + ResolvesToInt|int $length, + ): SubstrBytesOperator { + return new SubstrBytesOperator($string, $start, $length); + } + + /** + * Returns the substring of a string. Starts with the character at the specified UTF-8 code point (CP) index (zero-based) in the string and continues for the number of code points specified. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrCP/ + * @param ResolvesToString|string $string + * @param ResolvesToInt|int $start If start is a negative number, $substr returns an empty string "". + * @param ResolvesToInt|int $length If length is a negative number, $substr returns a substring that starts at the specified index and includes the rest of the string. + */ + public static function substrCP( + ResolvesToString|string $string, + ResolvesToInt|int $start, + ResolvesToInt|int $length, + ): SubstrCPOperator { + return new SubstrCPOperator($string, $start, $length); + } + + /** + * Returns the result of subtracting the second value from the first. If the two values are numbers, return the difference. If the two values are dates, return the difference in milliseconds. If the two values are a date and a number in milliseconds, return the resulting date. Accepts two argument expressions. If the two values are a date and a number, specify the date argument first as it is not meaningful to subtract a date from a number. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/subtract/ + * @param Decimal128|Int64|ResolvesToDate|ResolvesToNumber|UTCDateTime|float|int $expression1 + * @param Decimal128|Int64|ResolvesToDate|ResolvesToNumber|UTCDateTime|float|int $expression2 + */ + public static function subtract( + Decimal128|Int64|UTCDateTime|ResolvesToDate|ResolvesToNumber|float|int $expression1, + Decimal128|Int64|UTCDateTime|ResolvesToDate|ResolvesToNumber|float|int $expression2, + ): SubtractOperator { + return new SubtractOperator($expression1, $expression2); + } + + /** + * Returns a sum of numerical values. Ignores non-numeric values. + * Changed in MongoDB 5.0: Available in the $setWindowFields stage. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sum/ + * @no-named-arguments + * @param BSONArray|Decimal128|Int64|PackedArray|ResolvesToArray|ResolvesToNumber|array|float|int ...$expression + */ + public static function sum( + Decimal128|Int64|PackedArray|ResolvesToArray|ResolvesToNumber|BSONArray|array|float|int ...$expression, + ): SumOperator { + return new SumOperator(...$expression); + } + + /** + * Evaluates a series of case expressions. When it finds an expression which evaluates to true, $switch executes a specified expression and breaks out of the control flow. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/switch/ + * @param BSONArray|PackedArray|array $branches An array of control branch documents. Each branch is a document with the following fields: + * - case Can be any valid expression that resolves to a boolean. If the result is not a boolean, it is coerced to a boolean value. More information about how MongoDB evaluates expressions as either true or false can be found here. + * - then Can be any valid expression. + * The branches array must contain at least one branch document. + * @param Optional|ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $default The path to take if no branch case expression evaluates to true. + * Although optional, if default is unspecified and no branch case evaluates to true, $switch returns an error. + */ + public static function switch( + PackedArray|BSONArray|array $branches, + Optional|Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $default = Optional::Undefined, + ): SwitchOperator { + return new SwitchOperator($branches, $default); + } + + /** + * Returns the tangent of a value that is measured in radians. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/tan/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $expression $tan takes any valid expression that resolves to a number. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the result to radians. + * By default $tan returns values as a double. $tan can also return values as a 128-bit decimal as long as the expression resolves to a 128-bit decimal value. + */ + public static function tan(Decimal128|Int64|ResolvesToNumber|float|int $expression): TanOperator + { + return new TanOperator($expression); + } + + /** + * Returns the hyperbolic tangent of a value that is measured in radians. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/tanh/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $expression $tanh takes any valid expression that resolves to a number, measured in radians. If the expression returns a value in degrees, use the $degreesToRadians operator to convert the value to radians. + * By default $tanh returns values as a double. $tanh can also return values as a 128-bit decimal if the expression resolves to a 128-bit decimal value. + */ + public static function tanh(Decimal128|Int64|ResolvesToNumber|float|int $expression): TanhOperator + { + return new TanhOperator($expression); + } + + /** + * Converts value to a boolean. + * New in MongoDB 4.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toBool/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function toBool( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): ToBoolOperator { + return new ToBoolOperator($expression); + } + + /** + * Converts value to a Date. + * New in MongoDB 4.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDate/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function toDate( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): ToDateOperator { + return new ToDateOperator($expression); + } + + /** + * Converts value to a Decimal128. + * New in MongoDB 4.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDecimal/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function toDecimal( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): ToDecimalOperator { + return new ToDecimalOperator($expression); + } + + /** + * Converts value to a double. + * New in MongoDB 4.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toDouble/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function toDouble( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): ToDoubleOperator { + return new ToDoubleOperator($expression); + } + + /** + * Computes and returns the hash value of the input expression using the same hash function that MongoDB uses to create a hashed index. A hash function maps a key or string to a fixed-size numeric value. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toHashedIndexKey/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $value key or string to hash + */ + public static function toHashedIndexKey( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $value, + ): ToHashedIndexKeyOperator { + return new ToHashedIndexKeyOperator($value); + } + + /** + * Converts value to an integer. + * New in MongoDB 4.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toInt/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function toInt( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): ToIntOperator { + return new ToIntOperator($expression); + } + + /** + * Converts value to a long. + * New in MongoDB 4.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLong/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function toLong( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): ToLongOperator { + return new ToLongOperator($expression); + } + + /** + * Converts a string to lowercase. Accepts a single argument expression. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLower/ + * @param ResolvesToString|string $expression + */ + public static function toLower(ResolvesToString|string $expression): ToLowerOperator + { + return new ToLowerOperator($expression); + } + + /** + * Converts value to an ObjectId. + * New in MongoDB 4.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toObjectId/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function toObjectId( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): ToObjectIdOperator { + return new ToObjectIdOperator($expression); + } + + /** + * Converts value to a string. + * New in MongoDB 4.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toString/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function toString( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): ToStringOperator { + return new ToStringOperator($expression); + } + + /** + * Converts a string to uppercase. Accepts a single argument expression. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/toUpper/ + * @param ResolvesToString|string $expression + */ + public static function toUpper(ResolvesToString|string $expression): ToUpperOperator + { + return new ToUpperOperator($expression); + } + + /** + * Removes whitespace or the specified characters from the beginning and end of a string. + * New in MongoDB 4.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/trim/ + * @param ResolvesToString|string $input The string to trim. The argument can be any valid expression that resolves to a string. + * @param Optional|ResolvesToString|string $chars The character(s) to trim from the beginning of the input. + * The argument can be any valid expression that resolves to a string. The $ltrim operator breaks down the string into individual UTF code point to trim from input. + * If unspecified, $ltrim removes whitespace characters, including the null character. + */ + public static function trim( + ResolvesToString|string $input, + Optional|ResolvesToString|string $chars = Optional::Undefined, + ): TrimOperator { + return new TrimOperator($input, $chars); + } + + /** + * Truncates a number to a whole integer or to a specified decimal place. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/trunc/ + * @param Decimal128|Int64|ResolvesToNumber|float|int $number Can be any valid expression that resolves to a number. Specifically, the expression must resolve to an integer, double, decimal, or long. + * $trunc returns an error if the expression resolves to a non-numeric data type. + * @param Optional|ResolvesToInt|int $place Can be any valid expression that resolves to an integer between -20 and 100, exclusive. e.g. -20 < place < 100. Defaults to 0. + */ + public static function trunc( + Decimal128|Int64|ResolvesToNumber|float|int $number, + Optional|ResolvesToInt|int $place = Optional::Undefined, + ): TruncOperator { + return new TruncOperator($number, $place); + } + + /** + * Returns the incrementing ordinal from a timestamp as a long. + * New in MongoDB 5.1. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/tsIncrement/ + * @param ResolvesToTimestamp|Timestamp|int $expression + */ + public static function tsIncrement(Timestamp|ResolvesToTimestamp|int $expression): TsIncrementOperator + { + return new TsIncrementOperator($expression); + } + + /** + * Returns the seconds from a timestamp as a long. + * New in MongoDB 5.1. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/tsSecond/ + * @param ResolvesToTimestamp|Timestamp|int $expression + */ + public static function tsSecond(Timestamp|ResolvesToTimestamp|int $expression): TsSecondOperator + { + return new TsSecondOperator($expression); + } + + /** + * Return the BSON data type of the field. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/type/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function type( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): TypeOperator { + return new TypeOperator($expression); + } + + /** + * You can use $unsetField to remove fields with names that contain periods (.) or that start with dollar signs ($). + * $unsetField is an alias for $setField using $$REMOVE to remove fields. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/unsetField/ + * @param ResolvesToString|string $field Field in the input object that you want to add, update, or remove. field can be any valid expression that resolves to a string constant. + * @param Document|ResolvesToObject|Serializable|array|stdClass $input Document that contains the field that you want to add or update. input must resolve to an object, missing, null, or undefined. + */ + public static function unsetField( + ResolvesToString|string $field, + Document|Serializable|ResolvesToObject|stdClass|array $input, + ): UnsetFieldOperator { + return new UnsetFieldOperator($field, $input); + } + + /** + * Returns the week number for a date as a number between 0 (the partial week that precedes the first Sunday of the year) and 53 (leap year). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/week/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function week( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): WeekOperator { + return new WeekOperator($date, $timezone); + } + + /** + * Returns the year for a date as a number (e.g. 2014). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/year/ + * @param ObjectId|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|Timestamp|UTCDateTime|int $date The date to which the operator is applied. date must be a valid expression that resolves to a Date, a Timestamp, or an ObjectID. + * @param Optional|ResolvesToString|string $timezone The timezone of the operation result. timezone must be a valid expression that resolves to a string formatted as either an Olson Timezone Identifier or a UTC Offset. If no timezone is provided, the result is displayed in UTC. + */ + public static function year( + ObjectId|Timestamp|UTCDateTime|ResolvesToDate|ResolvesToObjectId|ResolvesToTimestamp|int $date, + Optional|ResolvesToString|string $timezone = Optional::Undefined, + ): YearOperator { + return new YearOperator($date, $timezone); + } + + /** + * Merge two arrays together. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/zip/ + * @param BSONArray|PackedArray|ResolvesToArray|array $inputs An array of expressions that resolve to arrays. The elements of these input arrays combine to form the arrays of the output array. + * If any of the inputs arrays resolves to a value of null or refers to a missing field, $zip returns null. + * If any of the inputs arrays does not resolve to an array or null nor refers to a missing field, $zip returns an error. + * @param Optional|bool $useLongestLength A boolean which specifies whether the length of the longest array determines the number of arrays in the output array. + * The default value is false: the shortest array length determines the number of arrays in the output array. + * @param Optional|BSONArray|PackedArray|array $defaults An array of default element values to use if the input arrays have different lengths. You must specify useLongestLength: true along with this field, or else $zip will return an error. + * If useLongestLength: true but defaults is empty or not specified, $zip uses null as the default value. + * If specifying a non-empty defaults, you must specify a default for each input array or else $zip will return an error. + */ + public static function zip( + PackedArray|ResolvesToArray|BSONArray|array $inputs, + Optional|bool $useLongestLength = Optional::Undefined, + Optional|PackedArray|BSONArray|array $defaults = Optional::Undefined, + ): ZipOperator { + return new ZipOperator($inputs, $useLongestLength, $defaults); + } +} diff --git a/src/Builder/Expression/FieldPath.php b/src/Builder/Expression/FieldPath.php new file mode 100644 index 000000000..4563994b9 --- /dev/null +++ b/src/Builder/Expression/FieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/FilterOperator.php b/src/Builder/Expression/FilterOperator.php new file mode 100644 index 000000000..6fc372e06 --- /dev/null +++ b/src/Builder/Expression/FilterOperator.php @@ -0,0 +1,72 @@ +input = $input; + $this->cond = $cond; + $this->as = $as; + $this->limit = $limit; + } + + public function getOperator(): string + { + return '$filter'; + } +} diff --git a/src/Builder/Expression/FirstNOperator.php b/src/Builder/Expression/FirstNOperator.php new file mode 100644 index 000000000..7aeb04eba --- /dev/null +++ b/src/Builder/Expression/FirstNOperator.php @@ -0,0 +1,53 @@ +n = $n; + if (is_array($input) && ! array_is_list($input)) { + throw new InvalidArgumentException('Expected $input argument to be a list, got an associative array.'); + } + + $this->input = $input; + } + + public function getOperator(): string + { + return '$firstN'; + } +} diff --git a/src/Builder/Expression/FirstOperator.php b/src/Builder/Expression/FirstOperator.php new file mode 100644 index 000000000..5453af68f --- /dev/null +++ b/src/Builder/Expression/FirstOperator.php @@ -0,0 +1,48 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$first'; + } +} diff --git a/src/Builder/Expression/FloorOperator.php b/src/Builder/Expression/FloorOperator.php new file mode 100644 index 000000000..4dfac2de1 --- /dev/null +++ b/src/Builder/Expression/FloorOperator.php @@ -0,0 +1,40 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$floor'; + } +} diff --git a/src/Builder/Expression/FunctionOperator.php b/src/Builder/Expression/FunctionOperator.php new file mode 100644 index 000000000..2a59fd6a3 --- /dev/null +++ b/src/Builder/Expression/FunctionOperator.php @@ -0,0 +1,69 @@ +body = $body; + if (is_array($args) && ! array_is_list($args)) { + throw new InvalidArgumentException('Expected $args argument to be a list, got an associative array.'); + } + + $this->args = $args; + $this->lang = $lang; + } + + public function getOperator(): string + { + return '$function'; + } +} diff --git a/src/Builder/Expression/GetFieldOperator.php b/src/Builder/Expression/GetFieldOperator.php new file mode 100644 index 000000000..45007b3fd --- /dev/null +++ b/src/Builder/Expression/GetFieldOperator.php @@ -0,0 +1,58 @@ +field = $field; + $this->input = $input; + } + + public function getOperator(): string + { + return '$getField'; + } +} diff --git a/src/Builder/Expression/GtOperator.php b/src/Builder/Expression/GtOperator.php new file mode 100644 index 000000000..1f4287eaa --- /dev/null +++ b/src/Builder/Expression/GtOperator.php @@ -0,0 +1,48 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$gt'; + } +} diff --git a/src/Builder/Expression/GteOperator.php b/src/Builder/Expression/GteOperator.php new file mode 100644 index 000000000..8ed633ae8 --- /dev/null +++ b/src/Builder/Expression/GteOperator.php @@ -0,0 +1,48 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$gte'; + } +} diff --git a/src/Builder/Expression/HourOperator.php b/src/Builder/Expression/HourOperator.php new file mode 100644 index 000000000..2d0598589 --- /dev/null +++ b/src/Builder/Expression/HourOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$hour'; + } +} diff --git a/src/Builder/Expression/IfNullOperator.php b/src/Builder/Expression/IfNullOperator.php new file mode 100644 index 000000000..f0a6a9ced --- /dev/null +++ b/src/Builder/Expression/IfNullOperator.php @@ -0,0 +1,53 @@ + $expression */ + public readonly array $expression; + + /** + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$expression + * @no-named-arguments + */ + public function __construct(Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$ifNull'; + } +} diff --git a/src/Builder/Expression/InOperator.php b/src/Builder/Expression/InOperator.php new file mode 100644 index 000000000..90ddab23b --- /dev/null +++ b/src/Builder/Expression/InOperator.php @@ -0,0 +1,58 @@ +expression = $expression; + if (is_array($array) && ! array_is_list($array)) { + throw new InvalidArgumentException('Expected $array argument to be a list, got an associative array.'); + } + + $this->array = $array; + } + + public function getOperator(): string + { + return '$in'; + } +} diff --git a/src/Builder/Expression/IndexOfArrayOperator.php b/src/Builder/Expression/IndexOfArrayOperator.php new file mode 100644 index 000000000..d04497fb5 --- /dev/null +++ b/src/Builder/Expression/IndexOfArrayOperator.php @@ -0,0 +1,85 @@ + index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + * If unspecified, the ending index position for the search is the end of the string. + */ + public readonly Optional|ResolvesToInt|int $end; + + /** + * @param BSONArray|PackedArray|ResolvesToArray|array $array Can be any valid expression as long as it resolves to an array. + * If the array expression resolves to a value of null or refers to a field that is missing, $indexOfArray returns null. + * If the array expression does not resolve to an array or null nor refers to a missing field, $indexOfArray returns an error. + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $search + * @param Optional|ResolvesToInt|int $start An integer, or a number that can be represented as integers (such as 2.0), that specifies the starting index position for the search. Can be any valid expression that resolves to a non-negative integral number. + * If unspecified, the starting index position for the search is the beginning of the string. + * @param Optional|ResolvesToInt|int $end An integer, or a number that can be represented as integers (such as 2.0), that specifies the ending index position for the search. Can be any valid expression that resolves to a non-negative integral number. If you specify a index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + * If unspecified, the ending index position for the search is the end of the string. + */ + public function __construct( + PackedArray|ResolvesToArray|BSONArray|array $array, + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $search, + Optional|ResolvesToInt|int $start = Optional::Undefined, + Optional|ResolvesToInt|int $end = Optional::Undefined, + ) { + if (is_array($array) && ! array_is_list($array)) { + throw new InvalidArgumentException('Expected $array argument to be a list, got an associative array.'); + } + + $this->array = $array; + $this->search = $search; + $this->start = $start; + $this->end = $end; + } + + public function getOperator(): string + { + return '$indexOfArray'; + } +} diff --git a/src/Builder/Expression/IndexOfBytesOperator.php b/src/Builder/Expression/IndexOfBytesOperator.php new file mode 100644 index 000000000..ce778fa25 --- /dev/null +++ b/src/Builder/Expression/IndexOfBytesOperator.php @@ -0,0 +1,72 @@ + index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + * If unspecified, the ending index position for the search is the end of the string. + */ + public readonly Optional|ResolvesToInt|int $end; + + /** + * @param ResolvesToString|string $string Can be any valid expression as long as it resolves to a string. + * If the string expression resolves to a value of null or refers to a field that is missing, $indexOfBytes returns null. + * If the string expression does not resolve to a string or null nor refers to a missing field, $indexOfBytes returns an error. + * @param ResolvesToString|string $substring Can be any valid expression as long as it resolves to a string. + * @param Optional|ResolvesToInt|int $start An integer, or a number that can be represented as integers (such as 2.0), that specifies the starting index position for the search. Can be any valid expression that resolves to a non-negative integral number. + * If unspecified, the starting index position for the search is the beginning of the string. + * @param Optional|ResolvesToInt|int $end An integer, or a number that can be represented as integers (such as 2.0), that specifies the ending index position for the search. Can be any valid expression that resolves to a non-negative integral number. If you specify a index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + * If unspecified, the ending index position for the search is the end of the string. + */ + public function __construct( + ResolvesToString|string $string, + ResolvesToString|string $substring, + Optional|ResolvesToInt|int $start = Optional::Undefined, + Optional|ResolvesToInt|int $end = Optional::Undefined, + ) { + $this->string = $string; + $this->substring = $substring; + $this->start = $start; + $this->end = $end; + } + + public function getOperator(): string + { + return '$indexOfBytes'; + } +} diff --git a/src/Builder/Expression/IndexOfCPOperator.php b/src/Builder/Expression/IndexOfCPOperator.php new file mode 100644 index 000000000..c4fd80d88 --- /dev/null +++ b/src/Builder/Expression/IndexOfCPOperator.php @@ -0,0 +1,72 @@ + index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + * If unspecified, the ending index position for the search is the end of the string. + */ + public readonly Optional|ResolvesToInt|int $end; + + /** + * @param ResolvesToString|string $string Can be any valid expression as long as it resolves to a string. + * If the string expression resolves to a value of null or refers to a field that is missing, $indexOfCP returns null. + * If the string expression does not resolve to a string or null nor refers to a missing field, $indexOfCP returns an error. + * @param ResolvesToString|string $substring Can be any valid expression as long as it resolves to a string. + * @param Optional|ResolvesToInt|int $start An integer, or a number that can be represented as integers (such as 2.0), that specifies the starting index position for the search. Can be any valid expression that resolves to a non-negative integral number. + * If unspecified, the starting index position for the search is the beginning of the string. + * @param Optional|ResolvesToInt|int $end An integer, or a number that can be represented as integers (such as 2.0), that specifies the ending index position for the search. Can be any valid expression that resolves to a non-negative integral number. If you specify a index value, you should also specify a index value; otherwise, $indexOfArray uses the value as the index value instead of the value. + * If unspecified, the ending index position for the search is the end of the string. + */ + public function __construct( + ResolvesToString|string $string, + ResolvesToString|string $substring, + Optional|ResolvesToInt|int $start = Optional::Undefined, + Optional|ResolvesToInt|int $end = Optional::Undefined, + ) { + $this->string = $string; + $this->substring = $substring; + $this->start = $start; + $this->end = $end; + } + + public function getOperator(): string + { + return '$indexOfCP'; + } +} diff --git a/src/Builder/Expression/IntFieldPath.php b/src/Builder/Expression/IntFieldPath.php new file mode 100644 index 000000000..dc9baeecb --- /dev/null +++ b/src/Builder/Expression/IntFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/IsArrayOperator.php b/src/Builder/Expression/IsArrayOperator.php new file mode 100644 index 000000000..bb15c0102 --- /dev/null +++ b/src/Builder/Expression/IsArrayOperator.php @@ -0,0 +1,41 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$isArray'; + } +} diff --git a/src/Builder/Expression/IsNumberOperator.php b/src/Builder/Expression/IsNumberOperator.php new file mode 100644 index 000000000..9d778f66f --- /dev/null +++ b/src/Builder/Expression/IsNumberOperator.php @@ -0,0 +1,43 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$isNumber'; + } +} diff --git a/src/Builder/Expression/IsoDayOfWeekOperator.php b/src/Builder/Expression/IsoDayOfWeekOperator.php new file mode 100644 index 000000000..ac55ea6ba --- /dev/null +++ b/src/Builder/Expression/IsoDayOfWeekOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$isoDayOfWeek'; + } +} diff --git a/src/Builder/Expression/IsoWeekOperator.php b/src/Builder/Expression/IsoWeekOperator.php new file mode 100644 index 000000000..af6cca475 --- /dev/null +++ b/src/Builder/Expression/IsoWeekOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$isoWeek'; + } +} diff --git a/src/Builder/Expression/IsoWeekYearOperator.php b/src/Builder/Expression/IsoWeekYearOperator.php new file mode 100644 index 000000000..a23d90696 --- /dev/null +++ b/src/Builder/Expression/IsoWeekYearOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$isoWeekYear'; + } +} diff --git a/src/Builder/Expression/JavascriptFieldPath.php b/src/Builder/Expression/JavascriptFieldPath.php new file mode 100644 index 000000000..e371ab7a9 --- /dev/null +++ b/src/Builder/Expression/JavascriptFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/LastNOperator.php b/src/Builder/Expression/LastNOperator.php new file mode 100644 index 000000000..f1bbaab11 --- /dev/null +++ b/src/Builder/Expression/LastNOperator.php @@ -0,0 +1,53 @@ +n = $n; + if (is_array($input) && ! array_is_list($input)) { + throw new InvalidArgumentException('Expected $input argument to be a list, got an associative array.'); + } + + $this->input = $input; + } + + public function getOperator(): string + { + return '$lastN'; + } +} diff --git a/src/Builder/Expression/LastOperator.php b/src/Builder/Expression/LastOperator.php new file mode 100644 index 000000000..ea622018c --- /dev/null +++ b/src/Builder/Expression/LastOperator.php @@ -0,0 +1,48 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$last'; + } +} diff --git a/src/Builder/Expression/LetOperator.php b/src/Builder/Expression/LetOperator.php new file mode 100644 index 000000000..541b0b8ff --- /dev/null +++ b/src/Builder/Expression/LetOperator.php @@ -0,0 +1,55 @@ +vars = $vars; + $this->in = $in; + } + + public function getOperator(): string + { + return '$let'; + } +} diff --git a/src/Builder/Expression/LiteralOperator.php b/src/Builder/Expression/LiteralOperator.php new file mode 100644 index 000000000..95e942b2f --- /dev/null +++ b/src/Builder/Expression/LiteralOperator.php @@ -0,0 +1,40 @@ +value = $value; + } + + public function getOperator(): string + { + return '$literal'; + } +} diff --git a/src/Builder/Expression/LnOperator.php b/src/Builder/Expression/LnOperator.php new file mode 100644 index 000000000..43d0de15b --- /dev/null +++ b/src/Builder/Expression/LnOperator.php @@ -0,0 +1,41 @@ +, Math.E ] expression, where Math.E is a JavaScript representation for Euler's number e. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/ln/ + */ +class LnOperator implements ResolvesToDouble, OperatorInterface +{ + public const ENCODE = Encode::Single; + + /** @var Decimal128|Int64|ResolvesToNumber|float|int $number Any valid expression as long as it resolves to a non-negative number. For more information on expressions, see Expressions. */ + public readonly Decimal128|Int64|ResolvesToNumber|float|int $number; + + /** + * @param Decimal128|Int64|ResolvesToNumber|float|int $number Any valid expression as long as it resolves to a non-negative number. For more information on expressions, see Expressions. + */ + public function __construct(Decimal128|Int64|ResolvesToNumber|float|int $number) + { + $this->number = $number; + } + + public function getOperator(): string + { + return '$ln'; + } +} diff --git a/src/Builder/Expression/Log10Operator.php b/src/Builder/Expression/Log10Operator.php new file mode 100644 index 000000000..ccdc67666 --- /dev/null +++ b/src/Builder/Expression/Log10Operator.php @@ -0,0 +1,40 @@ +number = $number; + } + + public function getOperator(): string + { + return '$log10'; + } +} diff --git a/src/Builder/Expression/LogOperator.php b/src/Builder/Expression/LogOperator.php new file mode 100644 index 000000000..8359e1a53 --- /dev/null +++ b/src/Builder/Expression/LogOperator.php @@ -0,0 +1,47 @@ +number = $number; + $this->base = $base; + } + + public function getOperator(): string + { + return '$log'; + } +} diff --git a/src/Builder/Expression/LongFieldPath.php b/src/Builder/Expression/LongFieldPath.php new file mode 100644 index 000000000..7d18c015b --- /dev/null +++ b/src/Builder/Expression/LongFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/LtOperator.php b/src/Builder/Expression/LtOperator.php new file mode 100644 index 000000000..a3cbad988 --- /dev/null +++ b/src/Builder/Expression/LtOperator.php @@ -0,0 +1,48 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$lt'; + } +} diff --git a/src/Builder/Expression/LteOperator.php b/src/Builder/Expression/LteOperator.php new file mode 100644 index 000000000..10e3d299a --- /dev/null +++ b/src/Builder/Expression/LteOperator.php @@ -0,0 +1,48 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$lte'; + } +} diff --git a/src/Builder/Expression/LtrimOperator.php b/src/Builder/Expression/LtrimOperator.php new file mode 100644 index 000000000..a0f4b7a5a --- /dev/null +++ b/src/Builder/Expression/LtrimOperator.php @@ -0,0 +1,53 @@ +input = $input; + $this->chars = $chars; + } + + public function getOperator(): string + { + return '$ltrim'; + } +} diff --git a/src/Builder/Expression/MapOperator.php b/src/Builder/Expression/MapOperator.php new file mode 100644 index 000000000..98274395c --- /dev/null +++ b/src/Builder/Expression/MapOperator.php @@ -0,0 +1,65 @@ +input = $input; + $this->in = $in; + $this->as = $as; + } + + public function getOperator(): string + { + return '$map'; + } +} diff --git a/src/Builder/Expression/MaxNOperator.php b/src/Builder/Expression/MaxNOperator.php new file mode 100644 index 000000000..2cda849d7 --- /dev/null +++ b/src/Builder/Expression/MaxNOperator.php @@ -0,0 +1,53 @@ +input = $input; + $this->n = $n; + } + + public function getOperator(): string + { + return '$maxN'; + } +} diff --git a/src/Builder/Expression/MaxOperator.php b/src/Builder/Expression/MaxOperator.php new file mode 100644 index 000000000..0573929b6 --- /dev/null +++ b/src/Builder/Expression/MaxOperator.php @@ -0,0 +1,54 @@ + $expression */ + public readonly array $expression; + + /** + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$expression + * @no-named-arguments + */ + public function __construct(Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$max'; + } +} diff --git a/src/Builder/Expression/MedianOperator.php b/src/Builder/Expression/MedianOperator.php new file mode 100644 index 000000000..f9c4f458d --- /dev/null +++ b/src/Builder/Expression/MedianOperator.php @@ -0,0 +1,62 @@ +input = $input; + $this->method = $method; + } + + public function getOperator(): string + { + return '$median'; + } +} diff --git a/src/Builder/Expression/MergeObjectsOperator.php b/src/Builder/Expression/MergeObjectsOperator.php new file mode 100644 index 000000000..162f2cd49 --- /dev/null +++ b/src/Builder/Expression/MergeObjectsOperator.php @@ -0,0 +1,53 @@ + $document Any valid expression that resolves to a document. */ + public readonly array $document; + + /** + * @param Document|ResolvesToObject|Serializable|array|stdClass ...$document Any valid expression that resolves to a document. + * @no-named-arguments + */ + public function __construct(Document|Serializable|ResolvesToObject|stdClass|array ...$document) + { + if (\count($document) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $document, got %d.', 1, \count($document))); + } + + if (! array_is_list($document)) { + throw new InvalidArgumentException('Expected $document arguments to be a list (array), named arguments are not supported'); + } + + $this->document = $document; + } + + public function getOperator(): string + { + return '$mergeObjects'; + } +} diff --git a/src/Builder/Expression/MetaOperator.php b/src/Builder/Expression/MetaOperator.php new file mode 100644 index 000000000..a02b54ee9 --- /dev/null +++ b/src/Builder/Expression/MetaOperator.php @@ -0,0 +1,38 @@ +keyword = $keyword; + } + + public function getOperator(): string + { + return '$meta'; + } +} diff --git a/src/Builder/Expression/MillisecondOperator.php b/src/Builder/Expression/MillisecondOperator.php new file mode 100644 index 000000000..63b53b460 --- /dev/null +++ b/src/Builder/Expression/MillisecondOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$millisecond'; + } +} diff --git a/src/Builder/Expression/MinNOperator.php b/src/Builder/Expression/MinNOperator.php new file mode 100644 index 000000000..f47976cb5 --- /dev/null +++ b/src/Builder/Expression/MinNOperator.php @@ -0,0 +1,53 @@ +input = $input; + $this->n = $n; + } + + public function getOperator(): string + { + return '$minN'; + } +} diff --git a/src/Builder/Expression/MinOperator.php b/src/Builder/Expression/MinOperator.php new file mode 100644 index 000000000..c3fa2ed4b --- /dev/null +++ b/src/Builder/Expression/MinOperator.php @@ -0,0 +1,54 @@ + $expression */ + public readonly array $expression; + + /** + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$expression + * @no-named-arguments + */ + public function __construct(Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$min'; + } +} diff --git a/src/Builder/Expression/MinuteOperator.php b/src/Builder/Expression/MinuteOperator.php new file mode 100644 index 000000000..7ab514ed0 --- /dev/null +++ b/src/Builder/Expression/MinuteOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$minute'; + } +} diff --git a/src/Builder/Expression/ModOperator.php b/src/Builder/Expression/ModOperator.php new file mode 100644 index 000000000..1a01019d7 --- /dev/null +++ b/src/Builder/Expression/ModOperator.php @@ -0,0 +1,47 @@ +dividend = $dividend; + $this->divisor = $divisor; + } + + public function getOperator(): string + { + return '$mod'; + } +} diff --git a/src/Builder/Expression/MonthOperator.php b/src/Builder/Expression/MonthOperator.php new file mode 100644 index 000000000..531bc946a --- /dev/null +++ b/src/Builder/Expression/MonthOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$month'; + } +} diff --git a/src/Builder/Expression/MultiplyOperator.php b/src/Builder/Expression/MultiplyOperator.php new file mode 100644 index 000000000..9f15aeb39 --- /dev/null +++ b/src/Builder/Expression/MultiplyOperator.php @@ -0,0 +1,56 @@ + $expression The arguments can be any valid expression as long as they resolve to numbers. + * Starting in MongoDB 6.1 you can optimize the $multiply operation. To improve performance, group references at the end of the argument list. + */ + public readonly array $expression; + + /** + * @param Decimal128|Int64|ResolvesToNumber|float|int ...$expression The arguments can be any valid expression as long as they resolve to numbers. + * Starting in MongoDB 6.1 you can optimize the $multiply operation. To improve performance, group references at the end of the argument list. + * @no-named-arguments + */ + public function __construct(Decimal128|Int64|ResolvesToNumber|float|int ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$multiply'; + } +} diff --git a/src/Builder/Expression/NeOperator.php b/src/Builder/Expression/NeOperator.php new file mode 100644 index 000000000..7bc322571 --- /dev/null +++ b/src/Builder/Expression/NeOperator.php @@ -0,0 +1,48 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$ne'; + } +} diff --git a/src/Builder/Expression/NotOperator.php b/src/Builder/Expression/NotOperator.php new file mode 100644 index 000000000..170538e15 --- /dev/null +++ b/src/Builder/Expression/NotOperator.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$not'; + } +} diff --git a/src/Builder/Expression/NullFieldPath.php b/src/Builder/Expression/NullFieldPath.php new file mode 100644 index 000000000..63bf171dc --- /dev/null +++ b/src/Builder/Expression/NullFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/NumberFieldPath.php b/src/Builder/Expression/NumberFieldPath.php new file mode 100644 index 000000000..d8ab23a23 --- /dev/null +++ b/src/Builder/Expression/NumberFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/ObjectFieldPath.php b/src/Builder/Expression/ObjectFieldPath.php new file mode 100644 index 000000000..082989831 --- /dev/null +++ b/src/Builder/Expression/ObjectFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/ObjectIdFieldPath.php b/src/Builder/Expression/ObjectIdFieldPath.php new file mode 100644 index 000000000..4428c1a39 --- /dev/null +++ b/src/Builder/Expression/ObjectIdFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/ObjectToArrayOperator.php b/src/Builder/Expression/ObjectToArrayOperator.php new file mode 100644 index 000000000..1ce5c5c11 --- /dev/null +++ b/src/Builder/Expression/ObjectToArrayOperator.php @@ -0,0 +1,41 @@ +object = $object; + } + + public function getOperator(): string + { + return '$objectToArray'; + } +} diff --git a/src/Builder/Expression/OrOperator.php b/src/Builder/Expression/OrOperator.php new file mode 100644 index 000000000..a34896116 --- /dev/null +++ b/src/Builder/Expression/OrOperator.php @@ -0,0 +1,54 @@ + $expression */ + public readonly array $expression; + + /** + * @param ExpressionInterface|ResolvesToBool|Type|array|bool|float|int|null|stdClass|string ...$expression + * @no-named-arguments + */ + public function __construct( + Type|ResolvesToBool|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression, + ) { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$or'; + } +} diff --git a/src/Builder/Expression/PercentileOperator.php b/src/Builder/Expression/PercentileOperator.php new file mode 100644 index 000000000..981719772 --- /dev/null +++ b/src/Builder/Expression/PercentileOperator.php @@ -0,0 +1,79 @@ +input = $input; + if (is_array($p) && ! array_is_list($p)) { + throw new InvalidArgumentException('Expected $p argument to be a list, got an associative array.'); + } + + $this->p = $p; + $this->method = $method; + } + + public function getOperator(): string + { + return '$percentile'; + } +} diff --git a/src/Builder/Expression/PowOperator.php b/src/Builder/Expression/PowOperator.php new file mode 100644 index 000000000..1def31e7a --- /dev/null +++ b/src/Builder/Expression/PowOperator.php @@ -0,0 +1,47 @@ +number = $number; + $this->exponent = $exponent; + } + + public function getOperator(): string + { + return '$pow'; + } +} diff --git a/src/Builder/Expression/RadiansToDegreesOperator.php b/src/Builder/Expression/RadiansToDegreesOperator.php new file mode 100644 index 000000000..670c26d92 --- /dev/null +++ b/src/Builder/Expression/RadiansToDegreesOperator.php @@ -0,0 +1,40 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$radiansToDegrees'; + } +} diff --git a/src/Builder/Expression/RandOperator.php b/src/Builder/Expression/RandOperator.php new file mode 100644 index 000000000..dd52b877a --- /dev/null +++ b/src/Builder/Expression/RandOperator.php @@ -0,0 +1,31 @@ +start = $start; + $this->end = $end; + $this->step = $step; + } + + public function getOperator(): string + { + return '$range'; + } +} diff --git a/src/Builder/Expression/ReduceOperator.php b/src/Builder/Expression/ReduceOperator.php new file mode 100644 index 000000000..f57ef2dac --- /dev/null +++ b/src/Builder/Expression/ReduceOperator.php @@ -0,0 +1,78 @@ +input = $input; + $this->initialValue = $initialValue; + $this->in = $in; + } + + public function getOperator(): string + { + return '$reduce'; + } +} diff --git a/src/Builder/Expression/RegexFieldPath.php b/src/Builder/Expression/RegexFieldPath.php new file mode 100644 index 000000000..14950bee7 --- /dev/null +++ b/src/Builder/Expression/RegexFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/RegexFindAllOperator.php b/src/Builder/Expression/RegexFindAllOperator.php new file mode 100644 index 000000000..e4d2a8d0d --- /dev/null +++ b/src/Builder/Expression/RegexFindAllOperator.php @@ -0,0 +1,54 @@ +/. When using the regex //, you can also specify the regex options i and m (but not the s or x options) */ + public readonly Regex|ResolvesToString|string $regex; + + /** @var Optional|string $options */ + public readonly Optional|string $options; + + /** + * @param ResolvesToString|string $input The string on which you wish to apply the regex pattern. Can be a string or any valid expression that resolves to a string. + * @param Regex|ResolvesToString|string $regex The regex pattern to apply. Can be any valid expression that resolves to either a string or regex pattern //. When using the regex //, you can also specify the regex options i and m (but not the s or x options) + * @param Optional|string $options + */ + public function __construct( + ResolvesToString|string $input, + Regex|ResolvesToString|string $regex, + Optional|string $options = Optional::Undefined, + ) { + $this->input = $input; + $this->regex = $regex; + $this->options = $options; + } + + public function getOperator(): string + { + return '$regexFindAll'; + } +} diff --git a/src/Builder/Expression/RegexFindOperator.php b/src/Builder/Expression/RegexFindOperator.php new file mode 100644 index 000000000..0b5f30f08 --- /dev/null +++ b/src/Builder/Expression/RegexFindOperator.php @@ -0,0 +1,54 @@ +/. When using the regex //, you can also specify the regex options i and m (but not the s or x options) */ + public readonly Regex|ResolvesToString|string $regex; + + /** @var Optional|string $options */ + public readonly Optional|string $options; + + /** + * @param ResolvesToString|string $input The string on which you wish to apply the regex pattern. Can be a string or any valid expression that resolves to a string. + * @param Regex|ResolvesToString|string $regex The regex pattern to apply. Can be any valid expression that resolves to either a string or regex pattern //. When using the regex //, you can also specify the regex options i and m (but not the s or x options) + * @param Optional|string $options + */ + public function __construct( + ResolvesToString|string $input, + Regex|ResolvesToString|string $regex, + Optional|string $options = Optional::Undefined, + ) { + $this->input = $input; + $this->regex = $regex; + $this->options = $options; + } + + public function getOperator(): string + { + return '$regexFind'; + } +} diff --git a/src/Builder/Expression/RegexMatchOperator.php b/src/Builder/Expression/RegexMatchOperator.php new file mode 100644 index 000000000..ed9707b53 --- /dev/null +++ b/src/Builder/Expression/RegexMatchOperator.php @@ -0,0 +1,54 @@ +/. When using the regex //, you can also specify the regex options i and m (but not the s or x options) */ + public readonly Regex|ResolvesToString|string $regex; + + /** @var Optional|string $options */ + public readonly Optional|string $options; + + /** + * @param ResolvesToString|string $input The string on which you wish to apply the regex pattern. Can be a string or any valid expression that resolves to a string. + * @param Regex|ResolvesToString|string $regex The regex pattern to apply. Can be any valid expression that resolves to either a string or regex pattern //. When using the regex //, you can also specify the regex options i and m (but not the s or x options) + * @param Optional|string $options + */ + public function __construct( + ResolvesToString|string $input, + Regex|ResolvesToString|string $regex, + Optional|string $options = Optional::Undefined, + ) { + $this->input = $input; + $this->regex = $regex; + $this->options = $options; + } + + public function getOperator(): string + { + return '$regexMatch'; + } +} diff --git a/src/Builder/Expression/ReplaceAllOperator.php b/src/Builder/Expression/ReplaceAllOperator.php new file mode 100644 index 000000000..d2fa2a602 --- /dev/null +++ b/src/Builder/Expression/ReplaceAllOperator.php @@ -0,0 +1,53 @@ +input = $input; + $this->find = $find; + $this->replacement = $replacement; + } + + public function getOperator(): string + { + return '$replaceAll'; + } +} diff --git a/src/Builder/Expression/ReplaceOneOperator.php b/src/Builder/Expression/ReplaceOneOperator.php new file mode 100644 index 000000000..609a0060d --- /dev/null +++ b/src/Builder/Expression/ReplaceOneOperator.php @@ -0,0 +1,52 @@ +input = $input; + $this->find = $find; + $this->replacement = $replacement; + } + + public function getOperator(): string + { + return '$replaceOne'; + } +} diff --git a/src/Builder/Expression/ResolvesToAny.php b/src/Builder/Expression/ResolvesToAny.php new file mode 100644 index 000000000..665564887 --- /dev/null +++ b/src/Builder/Expression/ResolvesToAny.php @@ -0,0 +1,13 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$reverseArray'; + } +} diff --git a/src/Builder/Expression/RoundOperator.php b/src/Builder/Expression/RoundOperator.php new file mode 100644 index 000000000..7f3cad53e --- /dev/null +++ b/src/Builder/Expression/RoundOperator.php @@ -0,0 +1,52 @@ +number = $number; + $this->place = $place; + } + + public function getOperator(): string + { + return '$round'; + } +} diff --git a/src/Builder/Expression/RtrimOperator.php b/src/Builder/Expression/RtrimOperator.php new file mode 100644 index 000000000..f9a6bc145 --- /dev/null +++ b/src/Builder/Expression/RtrimOperator.php @@ -0,0 +1,52 @@ +input = $input; + $this->chars = $chars; + } + + public function getOperator(): string + { + return '$rtrim'; + } +} diff --git a/src/Builder/Expression/SecondOperator.php b/src/Builder/Expression/SecondOperator.php new file mode 100644 index 000000000..4c9da6db6 --- /dev/null +++ b/src/Builder/Expression/SecondOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$second'; + } +} diff --git a/src/Builder/Expression/SetDifferenceOperator.php b/src/Builder/Expression/SetDifferenceOperator.php new file mode 100644 index 000000000..ec7e4ff60 --- /dev/null +++ b/src/Builder/Expression/SetDifferenceOperator.php @@ -0,0 +1,59 @@ +expression1 = $expression1; + if (is_array($expression2) && ! array_is_list($expression2)) { + throw new InvalidArgumentException('Expected $expression2 argument to be a list, got an associative array.'); + } + + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$setDifference'; + } +} diff --git a/src/Builder/Expression/SetEqualsOperator.php b/src/Builder/Expression/SetEqualsOperator.php new file mode 100644 index 000000000..051da408d --- /dev/null +++ b/src/Builder/Expression/SetEqualsOperator.php @@ -0,0 +1,52 @@ + $expression */ + public readonly array $expression; + + /** + * @param BSONArray|PackedArray|ResolvesToArray|array ...$expression + * @no-named-arguments + */ + public function __construct(PackedArray|ResolvesToArray|BSONArray|array ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$setEquals'; + } +} diff --git a/src/Builder/Expression/SetFieldOperator.php b/src/Builder/Expression/SetFieldOperator.php new file mode 100644 index 000000000..f4449ded1 --- /dev/null +++ b/src/Builder/Expression/SetFieldOperator.php @@ -0,0 +1,61 @@ +field = $field; + $this->input = $input; + $this->value = $value; + } + + public function getOperator(): string + { + return '$setField'; + } +} diff --git a/src/Builder/Expression/SetIntersectionOperator.php b/src/Builder/Expression/SetIntersectionOperator.php new file mode 100644 index 000000000..42e3257d6 --- /dev/null +++ b/src/Builder/Expression/SetIntersectionOperator.php @@ -0,0 +1,52 @@ + $expression */ + public readonly array $expression; + + /** + * @param BSONArray|PackedArray|ResolvesToArray|array ...$expression + * @no-named-arguments + */ + public function __construct(PackedArray|ResolvesToArray|BSONArray|array ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$setIntersection'; + } +} diff --git a/src/Builder/Expression/SetIsSubsetOperator.php b/src/Builder/Expression/SetIsSubsetOperator.php new file mode 100644 index 000000000..cf2011a0e --- /dev/null +++ b/src/Builder/Expression/SetIsSubsetOperator.php @@ -0,0 +1,59 @@ +expression1 = $expression1; + if (is_array($expression2) && ! array_is_list($expression2)) { + throw new InvalidArgumentException('Expected $expression2 argument to be a list, got an associative array.'); + } + + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$setIsSubset'; + } +} diff --git a/src/Builder/Expression/SetUnionOperator.php b/src/Builder/Expression/SetUnionOperator.php new file mode 100644 index 000000000..80080fc5a --- /dev/null +++ b/src/Builder/Expression/SetUnionOperator.php @@ -0,0 +1,52 @@ + $expression */ + public readonly array $expression; + + /** + * @param BSONArray|PackedArray|ResolvesToArray|array ...$expression + * @no-named-arguments + */ + public function __construct(PackedArray|ResolvesToArray|BSONArray|array ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$setUnion'; + } +} diff --git a/src/Builder/Expression/SinOperator.php b/src/Builder/Expression/SinOperator.php new file mode 100644 index 000000000..1976065f1 --- /dev/null +++ b/src/Builder/Expression/SinOperator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$sin'; + } +} diff --git a/src/Builder/Expression/SinhOperator.php b/src/Builder/Expression/SinhOperator.php new file mode 100644 index 000000000..de97f2c8c --- /dev/null +++ b/src/Builder/Expression/SinhOperator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$sinh'; + } +} diff --git a/src/Builder/Expression/SizeOperator.php b/src/Builder/Expression/SizeOperator.php new file mode 100644 index 000000000..5ea541226 --- /dev/null +++ b/src/Builder/Expression/SizeOperator.php @@ -0,0 +1,48 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$size'; + } +} diff --git a/src/Builder/Expression/SliceOperator.php b/src/Builder/Expression/SliceOperator.php new file mode 100644 index 000000000..df9172133 --- /dev/null +++ b/src/Builder/Expression/SliceOperator.php @@ -0,0 +1,74 @@ + is specified. + */ + public readonly ResolvesToInt|int $n; + + /** + * @var Optional|ResolvesToInt|int $position Any valid expression as long as it resolves to an integer. + * If positive, $slice determines the starting position from the start of the array. If position is greater than the number of elements, the $slice returns an empty array. + * If negative, $slice determines the starting position from the end of the array. If the absolute value of the is greater than the number of elements, the starting position is the start of the array. + */ + public readonly Optional|ResolvesToInt|int $position; + + /** + * @param BSONArray|PackedArray|ResolvesToArray|array $expression Any valid expression as long as it resolves to an array. + * @param ResolvesToInt|int $n Any valid expression as long as it resolves to an integer. If position is specified, n must resolve to a positive integer. + * If positive, $slice returns up to the first n elements in the array. If the position is specified, $slice returns the first n elements starting from the position. + * If negative, $slice returns up to the last n elements in the array. n cannot resolve to a negative number if is specified. + * @param Optional|ResolvesToInt|int $position Any valid expression as long as it resolves to an integer. + * If positive, $slice determines the starting position from the start of the array. If position is greater than the number of elements, the $slice returns an empty array. + * If negative, $slice determines the starting position from the end of the array. If the absolute value of the is greater than the number of elements, the starting position is the start of the array. + */ + public function __construct( + PackedArray|ResolvesToArray|BSONArray|array $expression, + ResolvesToInt|int $n, + Optional|ResolvesToInt|int $position = Optional::Undefined, + ) { + if (is_array($expression) && ! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression argument to be a list, got an associative array.'); + } + + $this->expression = $expression; + $this->n = $n; + $this->position = $position; + } + + public function getOperator(): string + { + return '$slice'; + } +} diff --git a/src/Builder/Expression/SortArrayOperator.php b/src/Builder/Expression/SortArrayOperator.php new file mode 100644 index 000000000..983408432 --- /dev/null +++ b/src/Builder/Expression/SortArrayOperator.php @@ -0,0 +1,65 @@ +input = $input; + $this->sortBy = $sortBy; + } + + public function getOperator(): string + { + return '$sortArray'; + } +} diff --git a/src/Builder/Expression/SplitOperator.php b/src/Builder/Expression/SplitOperator.php new file mode 100644 index 000000000..6453cbfe5 --- /dev/null +++ b/src/Builder/Expression/SplitOperator.php @@ -0,0 +1,43 @@ +string = $string; + $this->delimiter = $delimiter; + } + + public function getOperator(): string + { + return '$split'; + } +} diff --git a/src/Builder/Expression/SqrtOperator.php b/src/Builder/Expression/SqrtOperator.php new file mode 100644 index 000000000..e396c986c --- /dev/null +++ b/src/Builder/Expression/SqrtOperator.php @@ -0,0 +1,40 @@ +number = $number; + } + + public function getOperator(): string + { + return '$sqrt'; + } +} diff --git a/src/Builder/Expression/StdDevPopOperator.php b/src/Builder/Expression/StdDevPopOperator.php new file mode 100644 index 000000000..3845f7a24 --- /dev/null +++ b/src/Builder/Expression/StdDevPopOperator.php @@ -0,0 +1,54 @@ + $expression */ + public readonly array $expression; + + /** + * @param Decimal128|Int64|ResolvesToNumber|float|int ...$expression + * @no-named-arguments + */ + public function __construct(Decimal128|Int64|ResolvesToNumber|float|int ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$stdDevPop'; + } +} diff --git a/src/Builder/Expression/StdDevSampOperator.php b/src/Builder/Expression/StdDevSampOperator.php new file mode 100644 index 000000000..3b6a3c0a7 --- /dev/null +++ b/src/Builder/Expression/StdDevSampOperator.php @@ -0,0 +1,53 @@ + $expression */ + public readonly array $expression; + + /** + * @param Decimal128|Int64|ResolvesToNumber|float|int ...$expression + * @no-named-arguments + */ + public function __construct(Decimal128|Int64|ResolvesToNumber|float|int ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$stdDevSamp'; + } +} diff --git a/src/Builder/Expression/StrLenBytesOperator.php b/src/Builder/Expression/StrLenBytesOperator.php new file mode 100644 index 000000000..1f859fce9 --- /dev/null +++ b/src/Builder/Expression/StrLenBytesOperator.php @@ -0,0 +1,38 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$strLenBytes'; + } +} diff --git a/src/Builder/Expression/StrLenCPOperator.php b/src/Builder/Expression/StrLenCPOperator.php new file mode 100644 index 000000000..7b63f98ff --- /dev/null +++ b/src/Builder/Expression/StrLenCPOperator.php @@ -0,0 +1,38 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$strLenCP'; + } +} diff --git a/src/Builder/Expression/StrcasecmpOperator.php b/src/Builder/Expression/StrcasecmpOperator.php new file mode 100644 index 000000000..ff6f54423 --- /dev/null +++ b/src/Builder/Expression/StrcasecmpOperator.php @@ -0,0 +1,43 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$strcasecmp'; + } +} diff --git a/src/Builder/Expression/StringFieldPath.php b/src/Builder/Expression/StringFieldPath.php new file mode 100644 index 000000000..487d34782 --- /dev/null +++ b/src/Builder/Expression/StringFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/SubstrBytesOperator.php b/src/Builder/Expression/SubstrBytesOperator.php new file mode 100644 index 000000000..3ff54354c --- /dev/null +++ b/src/Builder/Expression/SubstrBytesOperator.php @@ -0,0 +1,48 @@ +string = $string; + $this->start = $start; + $this->length = $length; + } + + public function getOperator(): string + { + return '$substrBytes'; + } +} diff --git a/src/Builder/Expression/SubstrCPOperator.php b/src/Builder/Expression/SubstrCPOperator.php new file mode 100644 index 000000000..85baea910 --- /dev/null +++ b/src/Builder/Expression/SubstrCPOperator.php @@ -0,0 +1,48 @@ +string = $string; + $this->start = $start; + $this->length = $length; + } + + public function getOperator(): string + { + return '$substrCP'; + } +} diff --git a/src/Builder/Expression/SubstrOperator.php b/src/Builder/Expression/SubstrOperator.php new file mode 100644 index 000000000..9ef174060 --- /dev/null +++ b/src/Builder/Expression/SubstrOperator.php @@ -0,0 +1,48 @@ +string = $string; + $this->start = $start; + $this->length = $length; + } + + public function getOperator(): string + { + return '$substr'; + } +} diff --git a/src/Builder/Expression/SubtractOperator.php b/src/Builder/Expression/SubtractOperator.php new file mode 100644 index 000000000..b8cf78387 --- /dev/null +++ b/src/Builder/Expression/SubtractOperator.php @@ -0,0 +1,48 @@ +expression1 = $expression1; + $this->expression2 = $expression2; + } + + public function getOperator(): string + { + return '$subtract'; + } +} diff --git a/src/Builder/Expression/SumOperator.php b/src/Builder/Expression/SumOperator.php new file mode 100644 index 000000000..dde3535ee --- /dev/null +++ b/src/Builder/Expression/SumOperator.php @@ -0,0 +1,56 @@ + $expression */ + public readonly array $expression; + + /** + * @param BSONArray|Decimal128|Int64|PackedArray|ResolvesToArray|ResolvesToNumber|array|float|int ...$expression + * @no-named-arguments + */ + public function __construct( + Decimal128|Int64|PackedArray|ResolvesToArray|ResolvesToNumber|BSONArray|array|float|int ...$expression, + ) { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + if (! array_is_list($expression)) { + throw new InvalidArgumentException('Expected $expression arguments to be a list (array), named arguments are not supported'); + } + + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$sum'; + } +} diff --git a/src/Builder/Expression/SwitchOperator.php b/src/Builder/Expression/SwitchOperator.php new file mode 100644 index 000000000..3545477ea --- /dev/null +++ b/src/Builder/Expression/SwitchOperator.php @@ -0,0 +1,71 @@ +branches = $branches; + $this->default = $default; + } + + public function getOperator(): string + { + return '$switch'; + } +} diff --git a/src/Builder/Expression/TanOperator.php b/src/Builder/Expression/TanOperator.php new file mode 100644 index 000000000..75bc2e00a --- /dev/null +++ b/src/Builder/Expression/TanOperator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$tan'; + } +} diff --git a/src/Builder/Expression/TanhOperator.php b/src/Builder/Expression/TanhOperator.php new file mode 100644 index 000000000..d78a3d42b --- /dev/null +++ b/src/Builder/Expression/TanhOperator.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$tanh'; + } +} diff --git a/src/Builder/Expression/TimestampFieldPath.php b/src/Builder/Expression/TimestampFieldPath.php new file mode 100644 index 000000000..5aac9492f --- /dev/null +++ b/src/Builder/Expression/TimestampFieldPath.php @@ -0,0 +1,29 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/ToBoolOperator.php b/src/Builder/Expression/ToBoolOperator.php new file mode 100644 index 000000000..2986fc8d3 --- /dev/null +++ b/src/Builder/Expression/ToBoolOperator.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$toBool'; + } +} diff --git a/src/Builder/Expression/ToDateOperator.php b/src/Builder/Expression/ToDateOperator.php new file mode 100644 index 000000000..d6ff4b2bd --- /dev/null +++ b/src/Builder/Expression/ToDateOperator.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$toDate'; + } +} diff --git a/src/Builder/Expression/ToDecimalOperator.php b/src/Builder/Expression/ToDecimalOperator.php new file mode 100644 index 000000000..d8dd60379 --- /dev/null +++ b/src/Builder/Expression/ToDecimalOperator.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$toDecimal'; + } +} diff --git a/src/Builder/Expression/ToDoubleOperator.php b/src/Builder/Expression/ToDoubleOperator.php new file mode 100644 index 000000000..a21fe9ad5 --- /dev/null +++ b/src/Builder/Expression/ToDoubleOperator.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$toDouble'; + } +} diff --git a/src/Builder/Expression/ToHashedIndexKeyOperator.php b/src/Builder/Expression/ToHashedIndexKeyOperator.php new file mode 100644 index 000000000..e3038832a --- /dev/null +++ b/src/Builder/Expression/ToHashedIndexKeyOperator.php @@ -0,0 +1,41 @@ +value = $value; + } + + public function getOperator(): string + { + return '$toHashedIndexKey'; + } +} diff --git a/src/Builder/Expression/ToIntOperator.php b/src/Builder/Expression/ToIntOperator.php new file mode 100644 index 000000000..d03980572 --- /dev/null +++ b/src/Builder/Expression/ToIntOperator.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$toInt'; + } +} diff --git a/src/Builder/Expression/ToLongOperator.php b/src/Builder/Expression/ToLongOperator.php new file mode 100644 index 000000000..301113251 --- /dev/null +++ b/src/Builder/Expression/ToLongOperator.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$toLong'; + } +} diff --git a/src/Builder/Expression/ToLowerOperator.php b/src/Builder/Expression/ToLowerOperator.php new file mode 100644 index 000000000..d680f3296 --- /dev/null +++ b/src/Builder/Expression/ToLowerOperator.php @@ -0,0 +1,38 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$toLower'; + } +} diff --git a/src/Builder/Expression/ToObjectIdOperator.php b/src/Builder/Expression/ToObjectIdOperator.php new file mode 100644 index 000000000..8e204f67a --- /dev/null +++ b/src/Builder/Expression/ToObjectIdOperator.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$toObjectId'; + } +} diff --git a/src/Builder/Expression/ToStringOperator.php b/src/Builder/Expression/ToStringOperator.php new file mode 100644 index 000000000..c549a6f22 --- /dev/null +++ b/src/Builder/Expression/ToStringOperator.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$toString'; + } +} diff --git a/src/Builder/Expression/ToUpperOperator.php b/src/Builder/Expression/ToUpperOperator.php new file mode 100644 index 000000000..f57c2cbbf --- /dev/null +++ b/src/Builder/Expression/ToUpperOperator.php @@ -0,0 +1,38 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$toUpper'; + } +} diff --git a/src/Builder/Expression/TrimOperator.php b/src/Builder/Expression/TrimOperator.php new file mode 100644 index 000000000..def9d3a5a --- /dev/null +++ b/src/Builder/Expression/TrimOperator.php @@ -0,0 +1,53 @@ +input = $input; + $this->chars = $chars; + } + + public function getOperator(): string + { + return '$trim'; + } +} diff --git a/src/Builder/Expression/TruncOperator.php b/src/Builder/Expression/TruncOperator.php new file mode 100644 index 000000000..926d2e11a --- /dev/null +++ b/src/Builder/Expression/TruncOperator.php @@ -0,0 +1,52 @@ +number = $number; + $this->place = $place; + } + + public function getOperator(): string + { + return '$trunc'; + } +} diff --git a/src/Builder/Expression/TsIncrementOperator.php b/src/Builder/Expression/TsIncrementOperator.php new file mode 100644 index 000000000..3bf5a933a --- /dev/null +++ b/src/Builder/Expression/TsIncrementOperator.php @@ -0,0 +1,40 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$tsIncrement'; + } +} diff --git a/src/Builder/Expression/TsSecondOperator.php b/src/Builder/Expression/TsSecondOperator.php new file mode 100644 index 000000000..c0dcbe631 --- /dev/null +++ b/src/Builder/Expression/TsSecondOperator.php @@ -0,0 +1,40 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$tsSecond'; + } +} diff --git a/src/Builder/Expression/TypeOperator.php b/src/Builder/Expression/TypeOperator.php new file mode 100644 index 000000000..a65d1dd2e --- /dev/null +++ b/src/Builder/Expression/TypeOperator.php @@ -0,0 +1,41 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$type'; + } +} diff --git a/src/Builder/Expression/UnsetFieldOperator.php b/src/Builder/Expression/UnsetFieldOperator.php new file mode 100644 index 000000000..45e9c41e2 --- /dev/null +++ b/src/Builder/Expression/UnsetFieldOperator.php @@ -0,0 +1,49 @@ +field = $field; + $this->input = $input; + } + + public function getOperator(): string + { + return '$unsetField'; + } +} diff --git a/src/Builder/Expression/Variable.php b/src/Builder/Expression/Variable.php new file mode 100644 index 000000000..99b0750be --- /dev/null +++ b/src/Builder/Expression/Variable.php @@ -0,0 +1,28 @@ +name = $name; + } +} diff --git a/src/Builder/Expression/WeekOperator.php b/src/Builder/Expression/WeekOperator.php new file mode 100644 index 000000000..a24ee935d --- /dev/null +++ b/src/Builder/Expression/WeekOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$week'; + } +} diff --git a/src/Builder/Expression/YearOperator.php b/src/Builder/Expression/YearOperator.php new file mode 100644 index 000000000..c2b869260 --- /dev/null +++ b/src/Builder/Expression/YearOperator.php @@ -0,0 +1,49 @@ +date = $date; + $this->timezone = $timezone; + } + + public function getOperator(): string + { + return '$year'; + } +} diff --git a/src/Builder/Expression/ZipOperator.php b/src/Builder/Expression/ZipOperator.php new file mode 100644 index 000000000..0c23bbd4f --- /dev/null +++ b/src/Builder/Expression/ZipOperator.php @@ -0,0 +1,82 @@ +inputs = $inputs; + $this->useLongestLength = $useLongestLength; + if (is_array($defaults) && ! array_is_list($defaults)) { + throw new InvalidArgumentException('Expected $defaults argument to be a list, got an associative array.'); + } + + $this->defaults = $defaults; + } + + public function getOperator(): string + { + return '$zip'; + } +} diff --git a/src/Builder/Pipeline.php b/src/Builder/Pipeline.php new file mode 100644 index 000000000..e481277a9 --- /dev/null +++ b/src/Builder/Pipeline.php @@ -0,0 +1,60 @@ +|stdClass + * @implements IteratorAggregate + */ +final class Pipeline implements IteratorAggregate +{ + private readonly array $stages; + + /** + * @psalm-param stage|list ...$stagesOrPipelines + * + * @no-named-arguments + */ + public function __construct(StageInterface|Pipeline|array|stdClass ...$stagesOrPipelines) + { + if (! array_is_list($stagesOrPipelines)) { + throw new InvalidArgumentException('Named arguments are not supported for pipelines'); + } + + $stages = []; + + foreach ($stagesOrPipelines as $stageOrPipeline) { + if (is_array($stageOrPipeline) && array_is_list($stageOrPipeline)) { + $stages = array_merge($stages, $stageOrPipeline); + } elseif ($stageOrPipeline instanceof Pipeline) { + $stages = array_merge($stages, $stageOrPipeline->stages); + } else { + $stages[] = $stageOrPipeline; + } + } + + $this->stages = $stages; + } + + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->stages); + } +} diff --git a/src/Builder/Projection.php b/src/Builder/Projection.php new file mode 100644 index 000000000..e8e90f7f6 --- /dev/null +++ b/src/Builder/Projection.php @@ -0,0 +1,22 @@ +query = $query; + } + + public function getOperator(): string + { + return '$elemMatch'; + } +} diff --git a/src/Builder/Projection/FactoryTrait.php b/src/Builder/Projection/FactoryTrait.php new file mode 100644 index 000000000..359926ded --- /dev/null +++ b/src/Builder/Projection/FactoryTrait.php @@ -0,0 +1,28 @@ + $value */ + public readonly array $value; + + /** + * @param FieldQueryInterface|Type|array|bool|float|int|null|stdClass|string ...$value + * @no-named-arguments + */ + public function __construct(Type|FieldQueryInterface|stdClass|array|bool|float|int|null|string ...$value) + { + if (\count($value) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $value, got %d.', 1, \count($value))); + } + + if (! array_is_list($value)) { + throw new InvalidArgumentException('Expected $value arguments to be a list (array), named arguments are not supported'); + } + + $this->value = $value; + } + + public function getOperator(): string + { + return '$all'; + } +} diff --git a/src/Builder/Query/AndOperator.php b/src/Builder/Query/AndOperator.php new file mode 100644 index 000000000..40337e870 --- /dev/null +++ b/src/Builder/Query/AndOperator.php @@ -0,0 +1,51 @@ + $queries */ + public readonly array $queries; + + /** + * @param QueryInterface|array ...$queries + * @no-named-arguments + */ + public function __construct(QueryInterface|array ...$queries) + { + if (\count($queries) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $queries, got %d.', 1, \count($queries))); + } + + if (! array_is_list($queries)) { + throw new InvalidArgumentException('Expected $queries arguments to be a list (array), named arguments are not supported'); + } + + $this->queries = $queries; + } + + public function getOperator(): string + { + return '$and'; + } +} diff --git a/src/Builder/Query/BitsAllClearOperator.php b/src/Builder/Query/BitsAllClearOperator.php new file mode 100644 index 000000000..6f056d30b --- /dev/null +++ b/src/Builder/Query/BitsAllClearOperator.php @@ -0,0 +1,50 @@ +bitmask = $bitmask; + } + + public function getOperator(): string + { + return '$bitsAllClear'; + } +} diff --git a/src/Builder/Query/BitsAllSetOperator.php b/src/Builder/Query/BitsAllSetOperator.php new file mode 100644 index 000000000..0278b81ee --- /dev/null +++ b/src/Builder/Query/BitsAllSetOperator.php @@ -0,0 +1,50 @@ +bitmask = $bitmask; + } + + public function getOperator(): string + { + return '$bitsAllSet'; + } +} diff --git a/src/Builder/Query/BitsAnyClearOperator.php b/src/Builder/Query/BitsAnyClearOperator.php new file mode 100644 index 000000000..c2b899ea7 --- /dev/null +++ b/src/Builder/Query/BitsAnyClearOperator.php @@ -0,0 +1,50 @@ +bitmask = $bitmask; + } + + public function getOperator(): string + { + return '$bitsAnyClear'; + } +} diff --git a/src/Builder/Query/BitsAnySetOperator.php b/src/Builder/Query/BitsAnySetOperator.php new file mode 100644 index 000000000..4dcf3c33e --- /dev/null +++ b/src/Builder/Query/BitsAnySetOperator.php @@ -0,0 +1,50 @@ +bitmask = $bitmask; + } + + public function getOperator(): string + { + return '$bitsAnySet'; + } +} diff --git a/src/Builder/Query/BoxOperator.php b/src/Builder/Query/BoxOperator.php new file mode 100644 index 000000000..30a55d1c8 --- /dev/null +++ b/src/Builder/Query/BoxOperator.php @@ -0,0 +1,49 @@ +value = $value; + } + + public function getOperator(): string + { + return '$box'; + } +} diff --git a/src/Builder/Query/CenterOperator.php b/src/Builder/Query/CenterOperator.php new file mode 100644 index 000000000..d5bc395f0 --- /dev/null +++ b/src/Builder/Query/CenterOperator.php @@ -0,0 +1,49 @@ +value = $value; + } + + public function getOperator(): string + { + return '$center'; + } +} diff --git a/src/Builder/Query/CenterSphereOperator.php b/src/Builder/Query/CenterSphereOperator.php new file mode 100644 index 000000000..0975417a9 --- /dev/null +++ b/src/Builder/Query/CenterSphereOperator.php @@ -0,0 +1,49 @@ +value = $value; + } + + public function getOperator(): string + { + return '$centerSphere'; + } +} diff --git a/src/Builder/Query/CommentOperator.php b/src/Builder/Query/CommentOperator.php new file mode 100644 index 000000000..121c0e3b6 --- /dev/null +++ b/src/Builder/Query/CommentOperator.php @@ -0,0 +1,39 @@ +comment = $comment; + } + + public function getOperator(): string + { + return '$comment'; + } +} diff --git a/src/Builder/Query/ElemMatchOperator.php b/src/Builder/Query/ElemMatchOperator.php new file mode 100644 index 000000000..073619874 --- /dev/null +++ b/src/Builder/Query/ElemMatchOperator.php @@ -0,0 +1,50 @@ +query = $query; + } + + public function getOperator(): string + { + return '$elemMatch'; + } +} diff --git a/src/Builder/Query/EqOperator.php b/src/Builder/Query/EqOperator.php new file mode 100644 index 000000000..2dcd605fd --- /dev/null +++ b/src/Builder/Query/EqOperator.php @@ -0,0 +1,41 @@ +value = $value; + } + + public function getOperator(): string + { + return '$eq'; + } +} diff --git a/src/Builder/Query/ExistsOperator.php b/src/Builder/Query/ExistsOperator.php new file mode 100644 index 000000000..92c04f066 --- /dev/null +++ b/src/Builder/Query/ExistsOperator.php @@ -0,0 +1,39 @@ +exists = $exists; + } + + public function getOperator(): string + { + return '$exists'; + } +} diff --git a/src/Builder/Query/ExprOperator.php b/src/Builder/Query/ExprOperator.php new file mode 100644 index 000000000..a6235cb67 --- /dev/null +++ b/src/Builder/Query/ExprOperator.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$expr'; + } +} diff --git a/src/Builder/Query/FactoryTrait.php b/src/Builder/Query/FactoryTrait.php new file mode 100644 index 000000000..9c11b0795 --- /dev/null +++ b/src/Builder/Query/FactoryTrait.php @@ -0,0 +1,522 @@ +geometry = $geometry; + } + + public function getOperator(): string + { + return '$geoIntersects'; + } +} diff --git a/src/Builder/Query/GeoWithinOperator.php b/src/Builder/Query/GeoWithinOperator.php new file mode 100644 index 000000000..939e13d4a --- /dev/null +++ b/src/Builder/Query/GeoWithinOperator.php @@ -0,0 +1,43 @@ +geometry = $geometry; + } + + public function getOperator(): string + { + return '$geoWithin'; + } +} diff --git a/src/Builder/Query/GeometryOperator.php b/src/Builder/Query/GeometryOperator.php new file mode 100644 index 000000000..efdd38a1f --- /dev/null +++ b/src/Builder/Query/GeometryOperator.php @@ -0,0 +1,66 @@ +type = $type; + if (is_array($coordinates) && ! array_is_list($coordinates)) { + throw new InvalidArgumentException('Expected $coordinates argument to be a list, got an associative array.'); + } + + $this->coordinates = $coordinates; + $this->crs = $crs; + } + + public function getOperator(): string + { + return '$geometry'; + } +} diff --git a/src/Builder/Query/GtOperator.php b/src/Builder/Query/GtOperator.php new file mode 100644 index 000000000..31c03e485 --- /dev/null +++ b/src/Builder/Query/GtOperator.php @@ -0,0 +1,41 @@ +value = $value; + } + + public function getOperator(): string + { + return '$gt'; + } +} diff --git a/src/Builder/Query/GteOperator.php b/src/Builder/Query/GteOperator.php new file mode 100644 index 000000000..2ef771f1d --- /dev/null +++ b/src/Builder/Query/GteOperator.php @@ -0,0 +1,41 @@ +value = $value; + } + + public function getOperator(): string + { + return '$gte'; + } +} diff --git a/src/Builder/Query/InOperator.php b/src/Builder/Query/InOperator.php new file mode 100644 index 000000000..605a90b92 --- /dev/null +++ b/src/Builder/Query/InOperator.php @@ -0,0 +1,49 @@ +value = $value; + } + + public function getOperator(): string + { + return '$in'; + } +} diff --git a/src/Builder/Query/JsonSchemaOperator.php b/src/Builder/Query/JsonSchemaOperator.php new file mode 100644 index 000000000..cce8438be --- /dev/null +++ b/src/Builder/Query/JsonSchemaOperator.php @@ -0,0 +1,42 @@ +schema = $schema; + } + + public function getOperator(): string + { + return '$jsonSchema'; + } +} diff --git a/src/Builder/Query/LtOperator.php b/src/Builder/Query/LtOperator.php new file mode 100644 index 000000000..f29c73b57 --- /dev/null +++ b/src/Builder/Query/LtOperator.php @@ -0,0 +1,41 @@ +value = $value; + } + + public function getOperator(): string + { + return '$lt'; + } +} diff --git a/src/Builder/Query/LteOperator.php b/src/Builder/Query/LteOperator.php new file mode 100644 index 000000000..18453cb58 --- /dev/null +++ b/src/Builder/Query/LteOperator.php @@ -0,0 +1,41 @@ +value = $value; + } + + public function getOperator(): string + { + return '$lte'; + } +} diff --git a/src/Builder/Query/MaxDistanceOperator.php b/src/Builder/Query/MaxDistanceOperator.php new file mode 100644 index 000000000..864f1b60f --- /dev/null +++ b/src/Builder/Query/MaxDistanceOperator.php @@ -0,0 +1,41 @@ +value = $value; + } + + public function getOperator(): string + { + return '$maxDistance'; + } +} diff --git a/src/Builder/Query/MinDistanceOperator.php b/src/Builder/Query/MinDistanceOperator.php new file mode 100644 index 000000000..ea57b8e3b --- /dev/null +++ b/src/Builder/Query/MinDistanceOperator.php @@ -0,0 +1,40 @@ +value = $value; + } + + public function getOperator(): string + { + return '$minDistance'; + } +} diff --git a/src/Builder/Query/ModOperator.php b/src/Builder/Query/ModOperator.php new file mode 100644 index 000000000..e608bf9c9 --- /dev/null +++ b/src/Builder/Query/ModOperator.php @@ -0,0 +1,46 @@ +divisor = $divisor; + $this->remainder = $remainder; + } + + public function getOperator(): string + { + return '$mod'; + } +} diff --git a/src/Builder/Query/NeOperator.php b/src/Builder/Query/NeOperator.php new file mode 100644 index 000000000..9c12c851c --- /dev/null +++ b/src/Builder/Query/NeOperator.php @@ -0,0 +1,41 @@ +value = $value; + } + + public function getOperator(): string + { + return '$ne'; + } +} diff --git a/src/Builder/Query/NearOperator.php b/src/Builder/Query/NearOperator.php new file mode 100644 index 000000000..68b99aa5e --- /dev/null +++ b/src/Builder/Query/NearOperator.php @@ -0,0 +1,59 @@ +geometry = $geometry; + $this->maxDistance = $maxDistance; + $this->minDistance = $minDistance; + } + + public function getOperator(): string + { + return '$near'; + } +} diff --git a/src/Builder/Query/NearSphereOperator.php b/src/Builder/Query/NearSphereOperator.php new file mode 100644 index 000000000..aab04903d --- /dev/null +++ b/src/Builder/Query/NearSphereOperator.php @@ -0,0 +1,59 @@ +geometry = $geometry; + $this->maxDistance = $maxDistance; + $this->minDistance = $minDistance; + } + + public function getOperator(): string + { + return '$nearSphere'; + } +} diff --git a/src/Builder/Query/NinOperator.php b/src/Builder/Query/NinOperator.php new file mode 100644 index 000000000..8b348b733 --- /dev/null +++ b/src/Builder/Query/NinOperator.php @@ -0,0 +1,49 @@ +value = $value; + } + + public function getOperator(): string + { + return '$nin'; + } +} diff --git a/src/Builder/Query/NorOperator.php b/src/Builder/Query/NorOperator.php new file mode 100644 index 000000000..4ff7f41a4 --- /dev/null +++ b/src/Builder/Query/NorOperator.php @@ -0,0 +1,51 @@ + $queries */ + public readonly array $queries; + + /** + * @param QueryInterface|array ...$queries + * @no-named-arguments + */ + public function __construct(QueryInterface|array ...$queries) + { + if (\count($queries) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $queries, got %d.', 1, \count($queries))); + } + + if (! array_is_list($queries)) { + throw new InvalidArgumentException('Expected $queries arguments to be a list (array), named arguments are not supported'); + } + + $this->queries = $queries; + } + + public function getOperator(): string + { + return '$nor'; + } +} diff --git a/src/Builder/Query/NotOperator.php b/src/Builder/Query/NotOperator.php new file mode 100644 index 000000000..c6d135445 --- /dev/null +++ b/src/Builder/Query/NotOperator.php @@ -0,0 +1,41 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$not'; + } +} diff --git a/src/Builder/Query/OrOperator.php b/src/Builder/Query/OrOperator.php new file mode 100644 index 000000000..f7ae64279 --- /dev/null +++ b/src/Builder/Query/OrOperator.php @@ -0,0 +1,51 @@ + $queries */ + public readonly array $queries; + + /** + * @param QueryInterface|array ...$queries + * @no-named-arguments + */ + public function __construct(QueryInterface|array ...$queries) + { + if (\count($queries) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $queries, got %d.', 1, \count($queries))); + } + + if (! array_is_list($queries)) { + throw new InvalidArgumentException('Expected $queries arguments to be a list (array), named arguments are not supported'); + } + + $this->queries = $queries; + } + + public function getOperator(): string + { + return '$or'; + } +} diff --git a/src/Builder/Query/PolygonOperator.php b/src/Builder/Query/PolygonOperator.php new file mode 100644 index 000000000..6c829e076 --- /dev/null +++ b/src/Builder/Query/PolygonOperator.php @@ -0,0 +1,49 @@ +points = $points; + } + + public function getOperator(): string + { + return '$polygon'; + } +} diff --git a/src/Builder/Query/RandOperator.php b/src/Builder/Query/RandOperator.php new file mode 100644 index 000000000..251f50ea1 --- /dev/null +++ b/src/Builder/Query/RandOperator.php @@ -0,0 +1,32 @@ +regex = $regex; + } + + public function getOperator(): string + { + return '$regex'; + } +} diff --git a/src/Builder/Query/SampleRateOperator.php b/src/Builder/Query/SampleRateOperator.php new file mode 100644 index 000000000..a21a1ba5a --- /dev/null +++ b/src/Builder/Query/SampleRateOperator.php @@ -0,0 +1,45 @@ +rate = $rate; + } + + public function getOperator(): string + { + return '$sampleRate'; + } +} diff --git a/src/Builder/Query/SizeOperator.php b/src/Builder/Query/SizeOperator.php new file mode 100644 index 000000000..db9176c82 --- /dev/null +++ b/src/Builder/Query/SizeOperator.php @@ -0,0 +1,39 @@ +value = $value; + } + + public function getOperator(): string + { + return '$size'; + } +} diff --git a/src/Builder/Query/TextOperator.php b/src/Builder/Query/TextOperator.php new file mode 100644 index 000000000..47b88ede1 --- /dev/null +++ b/src/Builder/Query/TextOperator.php @@ -0,0 +1,67 @@ +search = $search; + $this->language = $language; + $this->caseSensitive = $caseSensitive; + $this->diacriticSensitive = $diacriticSensitive; + } + + public function getOperator(): string + { + return '$text'; + } +} diff --git a/src/Builder/Query/TypeOperator.php b/src/Builder/Query/TypeOperator.php new file mode 100644 index 000000000..cffe79092 --- /dev/null +++ b/src/Builder/Query/TypeOperator.php @@ -0,0 +1,51 @@ + $type */ + public readonly array $type; + + /** + * @param int|string ...$type + * @no-named-arguments + */ + public function __construct(int|string ...$type) + { + if (\count($type) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $type, got %d.', 1, \count($type))); + } + + if (! array_is_list($type)) { + throw new InvalidArgumentException('Expected $type arguments to be a list (array), named arguments are not supported'); + } + + $this->type = $type; + } + + public function getOperator(): string + { + return '$type'; + } +} diff --git a/src/Builder/Query/WhereOperator.php b/src/Builder/Query/WhereOperator.php new file mode 100644 index 000000000..5a38eb8c6 --- /dev/null +++ b/src/Builder/Query/WhereOperator.php @@ -0,0 +1,46 @@ +function = $function; + } + + public function getOperator(): string + { + return '$where'; + } +} diff --git a/src/Builder/Stage.php b/src/Builder/Stage.php new file mode 100644 index 000000000..2232a0975 --- /dev/null +++ b/src/Builder/Stage.php @@ -0,0 +1,36 @@ +|bool|float|int|string|null ...$queries The query predicates to match + */ + public static function match(QueryInterface|FieldQueryInterface|Type|stdClass|array|bool|float|int|string|null ...$queries): MatchStage + { + // Override the generated method to allow variadic arguments + return self::generatedMatch($queries); + } + + private function __construct() + { + // This class cannot be instantiated + } +} diff --git a/src/Builder/Stage/AddFieldsStage.php b/src/Builder/Stage/AddFieldsStage.php new file mode 100644 index 000000000..15f509264 --- /dev/null +++ b/src/Builder/Stage/AddFieldsStage.php @@ -0,0 +1,56 @@ + $expression Specify the name of each field to add and set its value to an aggregation expression or an empty object. */ + public readonly stdClass $expression; + + /** + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$expression Specify the name of each field to add and set its value to an aggregation expression or an empty object. + */ + public function __construct(Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$expression) + { + if (\count($expression) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $expression, got %d.', 1, \count($expression))); + } + + foreach($expression as $key => $value) { + if (! is_string($key)) { + throw new InvalidArgumentException('Expected $expression arguments to be a map (object), named arguments (:) or array unpacking ...[\'\' => ] must be used'); + } + } + + $expression = (object) $expression; + $this->expression = $expression; + } + + public function getOperator(): string + { + return '$addFields'; + } +} diff --git a/src/Builder/Stage/BucketAutoStage.php b/src/Builder/Stage/BucketAutoStage.php new file mode 100644 index 000000000..341421f44 --- /dev/null +++ b/src/Builder/Stage/BucketAutoStage.php @@ -0,0 +1,72 @@ +groupBy = $groupBy; + $this->buckets = $buckets; + $this->output = $output; + $this->granularity = $granularity; + } + + public function getOperator(): string + { + return '$bucketAuto'; + } +} diff --git a/src/Builder/Stage/BucketStage.php b/src/Builder/Stage/BucketStage.php new file mode 100644 index 000000000..47a497153 --- /dev/null +++ b/src/Builder/Stage/BucketStage.php @@ -0,0 +1,96 @@ +groupBy = $groupBy; + if (is_array($boundaries) && ! array_is_list($boundaries)) { + throw new InvalidArgumentException('Expected $boundaries argument to be a list, got an associative array.'); + } + + $this->boundaries = $boundaries; + $this->default = $default; + $this->output = $output; + } + + public function getOperator(): string + { + return '$bucket'; + } +} diff --git a/src/Builder/Stage/ChangeStreamSplitLargeEventStage.php b/src/Builder/Stage/ChangeStreamSplitLargeEventStage.php new file mode 100644 index 000000000..eb0c8ce76 --- /dev/null +++ b/src/Builder/Stage/ChangeStreamSplitLargeEventStage.php @@ -0,0 +1,33 @@ +allChangesForCluster = $allChangesForCluster; + $this->fullDocument = $fullDocument; + $this->fullDocumentBeforeChange = $fullDocumentBeforeChange; + $this->resumeAfter = $resumeAfter; + $this->showExpandedEvents = $showExpandedEvents; + $this->startAfter = $startAfter; + $this->startAtOperationTime = $startAtOperationTime; + } + + public function getOperator(): string + { + return '$changeStream'; + } +} diff --git a/src/Builder/Stage/CollStatsStage.php b/src/Builder/Stage/CollStatsStage.php new file mode 100644 index 000000000..96f048f46 --- /dev/null +++ b/src/Builder/Stage/CollStatsStage.php @@ -0,0 +1,62 @@ +latencyStats = $latencyStats; + $this->storageStats = $storageStats; + $this->count = $count; + $this->queryExecStats = $queryExecStats; + } + + public function getOperator(): string + { + return '$collStats'; + } +} diff --git a/src/Builder/Stage/CountStage.php b/src/Builder/Stage/CountStage.php new file mode 100644 index 000000000..3864f00c5 --- /dev/null +++ b/src/Builder/Stage/CountStage.php @@ -0,0 +1,40 @@ +field = $field; + } + + public function getOperator(): string + { + return '$count'; + } +} diff --git a/src/Builder/Stage/CurrentOpStage.php b/src/Builder/Stage/CurrentOpStage.php new file mode 100644 index 000000000..3b5930d32 --- /dev/null +++ b/src/Builder/Stage/CurrentOpStage.php @@ -0,0 +1,65 @@ +allUsers = $allUsers; + $this->idleConnections = $idleConnections; + $this->idleCursors = $idleCursors; + $this->idleSessions = $idleSessions; + $this->localOps = $localOps; + } + + public function getOperator(): string + { + return '$currentOp'; + } +} diff --git a/src/Builder/Stage/DensifyStage.php b/src/Builder/Stage/DensifyStage.php new file mode 100644 index 000000000..3abfcb3fe --- /dev/null +++ b/src/Builder/Stage/DensifyStage.php @@ -0,0 +1,72 @@ + in an embedded document or in an array, use dot notation. + */ + public readonly string $field; + + /** @var Document|Serializable|array|stdClass $range Specification for range based densification. */ + public readonly Document|Serializable|stdClass|array $range; + + /** @var Optional|BSONArray|PackedArray|array $partitionByFields The field(s) that will be used as the partition keys. */ + public readonly Optional|PackedArray|BSONArray|array $partitionByFields; + + /** + * @param string $field The field to densify. The values of the specified field must either be all numeric values or all dates. + * Documents that do not contain the specified field continue through the pipeline unmodified. + * To specify a in an embedded document or in an array, use dot notation. + * @param Document|Serializable|array|stdClass $range Specification for range based densification. + * @param Optional|BSONArray|PackedArray|array $partitionByFields The field(s) that will be used as the partition keys. + */ + public function __construct( + string $field, + Document|Serializable|stdClass|array $range, + Optional|PackedArray|BSONArray|array $partitionByFields = Optional::Undefined, + ) { + $this->field = $field; + $this->range = $range; + if (is_array($partitionByFields) && ! array_is_list($partitionByFields)) { + throw new InvalidArgumentException('Expected $partitionByFields argument to be a list, got an associative array.'); + } + + $this->partitionByFields = $partitionByFields; + } + + public function getOperator(): string + { + return '$densify'; + } +} diff --git a/src/Builder/Stage/DocumentsStage.php b/src/Builder/Stage/DocumentsStage.php new file mode 100644 index 000000000..1ca0a3f9c --- /dev/null +++ b/src/Builder/Stage/DocumentsStage.php @@ -0,0 +1,60 @@ +documents = $documents; + } + + public function getOperator(): string + { + return '$documents'; + } +} diff --git a/src/Builder/Stage/FacetStage.php b/src/Builder/Stage/FacetStage.php new file mode 100644 index 000000000..e334167bf --- /dev/null +++ b/src/Builder/Stage/FacetStage.php @@ -0,0 +1,57 @@ + $facet */ + public readonly stdClass $facet; + + /** + * @param BSONArray|PackedArray|Pipeline|array ...$facet + */ + public function __construct(PackedArray|Pipeline|BSONArray|array ...$facet) + { + if (\count($facet) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $facet, got %d.', 1, \count($facet))); + } + + foreach($facet as $key => $value) { + if (! is_string($key)) { + throw new InvalidArgumentException('Expected $facet arguments to be a map (object), named arguments (:) or array unpacking ...[\'\' => ] must be used'); + } + } + + $facet = (object) $facet; + $this->facet = $facet; + } + + public function getOperator(): string + { + return '$facet'; + } +} diff --git a/src/Builder/Stage/FactoryTrait.php b/src/Builder/Stage/FactoryTrait.php new file mode 100644 index 000000000..199cf64ef --- /dev/null +++ b/src/Builder/Stage/FactoryTrait.php @@ -0,0 +1,681 @@ + in an embedded document or in an array, use dot notation. + * @param Document|Serializable|array|stdClass $range Specification for range based densification. + * @param Optional|BSONArray|PackedArray|array $partitionByFields The field(s) that will be used as the partition keys. + */ + public static function densify( + string $field, + Document|Serializable|stdClass|array $range, + Optional|PackedArray|BSONArray|array $partitionByFields = Optional::Undefined, + ): DensifyStage { + return new DensifyStage($field, $range, $partitionByFields); + } + + /** + * Returns literal documents from input values. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/documents/ + * @param BSONArray|PackedArray|ResolvesToArray|array $documents $documents accepts any valid expression that resolves to an array of objects. This includes: + * - system variables, such as $$NOW or $$SEARCH_META + * - $let expressions + * - variables in scope from $lookup expressions + * Expressions that do not resolve to a current document, like $myField or $$ROOT, will result in an error. + */ + public static function documents(PackedArray|ResolvesToArray|BSONArray|array $documents): DocumentsStage + { + return new DocumentsStage($documents); + } + + /** + * Processes multiple aggregation pipelines within a single stage on the same set of input documents. Enables the creation of multi-faceted aggregations capable of characterizing data across multiple dimensions, or facets, in a single stage. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/facet/ + * @param BSONArray|PackedArray|Pipeline|array ...$facet + */ + public static function facet(PackedArray|Pipeline|BSONArray|array ...$facet): FacetStage + { + return new FacetStage(...$facet); + } + + /** + * Populates null and missing field values within documents. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/fill/ + * @param Document|Serializable|array|stdClass $output Specifies an object containing each field for which to fill missing values. You can specify multiple fields in the output object. + * The object name is the name of the field to fill. The object value specifies how the field is filled. + * @param Optional|Document|Serializable|array|stdClass|string $partitionBy Specifies an expression to group the documents. In the $fill stage, a group of documents is known as a partition. + * If you omit partitionBy and partitionByFields, $fill uses one partition for the entire collection. + * partitionBy and partitionByFields are mutually exclusive. + * @param Optional|BSONArray|PackedArray|array $partitionByFields Specifies an array of fields as the compound key to group the documents. In the $fill stage, each group of documents is known as a partition. + * If you omit partitionBy and partitionByFields, $fill uses one partition for the entire collection. + * partitionBy and partitionByFields are mutually exclusive. + * @param Optional|Document|Serializable|array|stdClass $sortBy Specifies the field or fields to sort the documents within each partition. Uses the same syntax as the $sort stage. + */ + public static function fill( + Document|Serializable|stdClass|array $output, + Optional|Document|Serializable|stdClass|array|string $partitionBy = Optional::Undefined, + Optional|PackedArray|BSONArray|array $partitionByFields = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $sortBy = Optional::Undefined, + ): FillStage { + return new FillStage($output, $partitionBy, $partitionByFields, $sortBy); + } + + /** + * Returns an ordered stream of documents based on the proximity to a geospatial point. Incorporates the functionality of $match, $sort, and $limit for geospatial data. The output documents include an additional distance field and can include a location identifier field. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/geoNear/ + * @param string $distanceField The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. + * @param Document|ResolvesToObject|Serializable|array|stdClass $near The point for which to find the closest documents. + * @param Optional|Decimal128|Int64|float|int $distanceMultiplier The factor to multiply all distances returned by the query. For example, use the distanceMultiplier to convert radians, as returned by a spherical query, to kilometers by multiplying by the radius of the Earth. + * @param Optional|string $includeLocs This specifies the output field that identifies the location used to calculate the distance. This option is useful when a location field contains multiple locations. To specify a field within an embedded document, use dot notation. + * @param Optional|string $key Specify the geospatial indexed field to use when calculating the distance. + * @param Optional|Decimal128|Int64|float|int $maxDistance The maximum distance from the center point that the documents can be. MongoDB limits the results to those documents that fall within the specified distance from the center point. + * Specify the distance in meters if the specified point is GeoJSON and in radians if the specified point is legacy coordinate pairs. + * @param Optional|Decimal128|Int64|float|int $minDistance The minimum distance from the center point that the documents can be. MongoDB limits the results to those documents that fall outside the specified distance from the center point. + * Specify the distance in meters for GeoJSON data and in radians for legacy coordinate pairs. + * @param Optional|QueryInterface|array $query Limits the results to the documents that match the query. The query syntax is the usual MongoDB read operation query syntax. + * You cannot specify a $near predicate in the query field of the $geoNear stage. + * @param Optional|bool $spherical Determines how MongoDB calculates the distance between two points: + * - When true, MongoDB uses $nearSphere semantics and calculates distances using spherical geometry. + * - When false, MongoDB uses $near semantics: spherical geometry for 2dsphere indexes and planar geometry for 2d indexes. + * Default: false. + */ + public static function geoNear( + string $distanceField, + Document|Serializable|ResolvesToObject|stdClass|array $near, + Optional|Decimal128|Int64|float|int $distanceMultiplier = Optional::Undefined, + Optional|string $includeLocs = Optional::Undefined, + Optional|string $key = Optional::Undefined, + Optional|Decimal128|Int64|float|int $maxDistance = Optional::Undefined, + Optional|Decimal128|Int64|float|int $minDistance = Optional::Undefined, + Optional|QueryInterface|array $query = Optional::Undefined, + Optional|bool $spherical = Optional::Undefined, + ): GeoNearStage { + return new GeoNearStage($distanceField, $near, $distanceMultiplier, $includeLocs, $key, $maxDistance, $minDistance, $query, $spherical); + } + + /** + * Performs a recursive search on a collection. To each output document, adds a new array field that contains the traversal results of the recursive search for that document. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/graphLookup/ + * @param string $from Target collection for the $graphLookup operation to search, recursively matching the connectFromField to the connectToField. The from collection must be in the same database as any other collections used in the operation. + * Starting in MongoDB 5.1, the collection specified in the from parameter can be sharded. + * @param BSONArray|ExpressionInterface|PackedArray|Type|array|bool|float|int|null|stdClass|string $startWith Expression that specifies the value of the connectFromField with which to start the recursive search. Optionally, startWith may be array of values, each of which is individually followed through the traversal process. + * @param string $connectFromField Field name whose value $graphLookup uses to recursively match against the connectToField of other documents in the collection. If the value is an array, each element is individually followed through the traversal process. + * @param string $connectToField Field name in other documents against which to match the value of the field specified by the connectFromField parameter. + * @param string $as Name of the array field added to each output document. Contains the documents traversed in the $graphLookup stage to reach the document. + * @param Optional|int $maxDepth Non-negative integral number specifying the maximum recursion depth. + * @param Optional|string $depthField Name of the field to add to each traversed document in the search path. The value of this field is the recursion depth for the document, represented as a NumberLong. Recursion depth value starts at zero, so the first lookup corresponds to zero depth. + * @param Optional|QueryInterface|array $restrictSearchWithMatch A document specifying additional conditions for the recursive search. The syntax is identical to query filter syntax. + */ + public static function graphLookup( + string $from, + PackedArray|Type|ExpressionInterface|BSONArray|stdClass|array|bool|float|int|null|string $startWith, + string $connectFromField, + string $connectToField, + string $as, + Optional|int $maxDepth = Optional::Undefined, + Optional|string $depthField = Optional::Undefined, + Optional|QueryInterface|array $restrictSearchWithMatch = Optional::Undefined, + ): GraphLookupStage { + return new GraphLookupStage($from, $startWith, $connectFromField, $connectToField, $as, $maxDepth, $depthField, $restrictSearchWithMatch); + } + + /** + * Groups input documents by a specified identifier expression and applies the accumulator expression(s), if specified, to each group. Consumes all input documents and outputs one document per each distinct group. The output documents only contain the identifier field and, if specified, accumulated fields. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $_id The _id expression specifies the group key. If you specify an _id value of null, or any other constant value, the $group stage returns a single document that aggregates values across all of the input documents. + * @param AccumulatorInterface|Document|Serializable|array|stdClass ...$field Computed using the accumulator operators. + */ + public static function group( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $_id, + Document|Serializable|AccumulatorInterface|stdClass|array ...$field, + ): GroupStage { + return new GroupStage($_id, ...$field); + } + + /** + * Returns statistics regarding the use of each index for the collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexStats/ + */ + public static function indexStats(): IndexStatsStage + { + return new IndexStatsStage(); + } + + /** + * Passes the first n documents unmodified to the pipeline where n is the specified limit. For each input document, outputs either one document (for the first n documents) or zero documents (after the first n documents). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/limit/ + * @param int $limit + */ + public static function limit(int $limit): LimitStage + { + return new LimitStage($limit); + } + + /** + * Lists all active sessions recently in use on the currently connected mongos or mongod instance. These sessions may have not yet propagated to the system.sessions collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/listLocalSessions/ + * @param Optional|BSONArray|PackedArray|array $users Returns all sessions for the specified users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster to list sessions for other users. + * @param Optional|bool $allUsers Returns all sessions for all users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster. + */ + public static function listLocalSessions( + Optional|PackedArray|BSONArray|array $users = Optional::Undefined, + Optional|bool $allUsers = Optional::Undefined, + ): ListLocalSessionsStage { + return new ListLocalSessionsStage($users, $allUsers); + } + + /** + * Lists sampled queries for all collections or a specific collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSampledQueries/ + * @param Optional|string $namespace + */ + public static function listSampledQueries( + Optional|string $namespace = Optional::Undefined, + ): ListSampledQueriesStage { + return new ListSampledQueriesStage($namespace); + } + + /** + * Returns information about existing Atlas Search indexes on a specified collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSearchIndexes/ + * @param Optional|string $id The id of the index to return information about. + * @param Optional|string $name The name of the index to return information about. + */ + public static function listSearchIndexes( + Optional|string $id = Optional::Undefined, + Optional|string $name = Optional::Undefined, + ): ListSearchIndexesStage { + return new ListSearchIndexesStage($id, $name); + } + + /** + * Lists all sessions that have been active long enough to propagate to the system.sessions collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSessions/ + * @param Optional|BSONArray|PackedArray|array $users Returns all sessions for the specified users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster to list sessions for other users. + * @param Optional|bool $allUsers Returns all sessions for all users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster. + */ + public static function listSessions( + Optional|PackedArray|BSONArray|array $users = Optional::Undefined, + Optional|bool $allUsers = Optional::Undefined, + ): ListSessionsStage { + return new ListSessionsStage($users, $allUsers); + } + + /** + * Performs a left outer join to another collection in the same database to filter in documents from the "joined" collection for processing. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/ + * @param string $as Specifies the name of the new array field to add to the input documents. The new array field contains the matching documents from the from collection. If the specified name already exists in the input document, the existing field is overwritten. + * @param Optional|string $from Specifies the collection in the same database to perform the join with. + * from is optional, you can use a $documents stage in a $lookup stage instead. For an example, see Use a $documents Stage in a $lookup Stage. + * Starting in MongoDB 5.1, the collection specified in the from parameter can be sharded. + * @param Optional|string $localField Specifies the field from the documents input to the $lookup stage. $lookup performs an equality match on the localField to the foreignField from the documents of the from collection. If an input document does not contain the localField, the $lookup treats the field as having a value of null for matching purposes. + * @param Optional|string $foreignField Specifies the field from the documents in the from collection. $lookup performs an equality match on the foreignField to the localField from the input documents. If a document in the from collection does not contain the foreignField, the $lookup treats the value as null for matching purposes. + * @param Optional|Document|Serializable|array|stdClass $let Specifies variables to use in the pipeline stages. Use the variable expressions to access the fields from the joined collection's documents that are input to the pipeline. + * @param Optional|BSONArray|PackedArray|Pipeline|array $pipeline Specifies the pipeline to run on the joined collection. The pipeline determines the resulting documents from the joined collection. To return all documents, specify an empty pipeline []. + * The pipeline cannot include the $out stage or the $merge stage. Starting in v6.0, the pipeline can contain the Atlas Search $search stage as the first stage inside the pipeline. + * The pipeline cannot directly access the joined document fields. Instead, define variables for the joined document fields using the let option and then reference the variables in the pipeline stages. + */ + public static function lookup( + string $as, + Optional|string $from = Optional::Undefined, + Optional|string $localField = Optional::Undefined, + Optional|string $foreignField = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $let = Optional::Undefined, + Optional|PackedArray|Pipeline|BSONArray|array $pipeline = Optional::Undefined, + ): LookupStage { + return new LookupStage($as, $from, $localField, $foreignField, $let, $pipeline); + } + + /** + * Filters the document stream to allow only matching documents to pass unmodified into the next pipeline stage. $match uses standard MongoDB queries. For each input document, outputs either one document (a match) or zero documents (no match). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/ + * @param QueryInterface|array $query + */ + public static function match(QueryInterface|array $query): MatchStage + { + return new MatchStage($query); + } + + /** + * Writes the resulting documents of the aggregation pipeline to a collection. The stage can incorporate (insert new documents, merge documents, replace documents, keep existing documents, fail the operation, process documents with a custom update pipeline) the results into an output collection. To use the $merge stage, it must be the last stage in the pipeline. + * New in MongoDB 4.2. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/ + * @param Document|Serializable|array|stdClass|string $into The output collection. + * @param Optional|BSONArray|PackedArray|array|string $on Field or fields that act as a unique identifier for a document. The identifier determines if a results document matches an existing document in the output collection. + * @param Optional|Document|Serializable|array|stdClass $let Specifies variables for use in the whenMatched pipeline. + * @param Optional|BSONArray|PackedArray|Pipeline|array|string $whenMatched The behavior of $merge if a result document and an existing document in the collection have the same value for the specified on field(s). + * @param Optional|string $whenNotMatched The behavior of $merge if a result document does not match an existing document in the out collection. + */ + public static function merge( + Document|Serializable|stdClass|array|string $into, + Optional|PackedArray|BSONArray|array|string $on = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $let = Optional::Undefined, + Optional|PackedArray|Pipeline|BSONArray|array|string $whenMatched = Optional::Undefined, + Optional|string $whenNotMatched = Optional::Undefined, + ): MergeStage { + return new MergeStage($into, $on, $let, $whenMatched, $whenNotMatched); + } + + /** + * Writes the resulting documents of the aggregation pipeline to a collection. To use the $out stage, it must be the last stage in the pipeline. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/out/ + * @param Document|Serializable|array|stdClass|string $coll Target database name to write documents from $out to. + */ + public static function out(Document|Serializable|stdClass|array|string $coll): OutStage + { + return new OutStage($coll); + } + + /** + * Returns plan cache information for a collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/planCacheStats/ + */ + public static function planCacheStats(): PlanCacheStatsStage + { + return new PlanCacheStatsStage(); + } + + /** + * Reshapes each document in the stream, such as by adding new fields or removing existing fields. For each input document, outputs one document. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$specification + */ + public static function project( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$specification, + ): ProjectStage { + return new ProjectStage(...$specification); + } + + /** + * Reshapes each document in the stream by restricting the content for each document based on information stored in the documents themselves. Incorporates the functionality of $project and $match. Can be used to implement field level redaction. For each input document, outputs either one or zero documents. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/redact/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function redact( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): RedactStage { + return new RedactStage($expression); + } + + /** + * Replaces a document with the specified embedded document. The operation replaces all existing fields in the input document, including the _id field. Specify a document embedded in the input document to promote the embedded document to the top level. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceRoot/ + * @param Document|ResolvesToObject|Serializable|array|stdClass $newRoot + */ + public static function replaceRoot( + Document|Serializable|ResolvesToObject|stdClass|array $newRoot, + ): ReplaceRootStage { + return new ReplaceRootStage($newRoot); + } + + /** + * Replaces a document with the specified embedded document. The operation replaces all existing fields in the input document, including the _id field. Specify a document embedded in the input document to promote the embedded document to the top level. + * Alias for $replaceRoot. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceWith/ + * @param Document|ResolvesToObject|Serializable|array|stdClass $expression + */ + public static function replaceWith( + Document|Serializable|ResolvesToObject|stdClass|array $expression, + ): ReplaceWithStage { + return new ReplaceWithStage($expression); + } + + /** + * Randomly selects the specified number of documents from its input. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sample/ + * @param int $size The number of documents to randomly select. + */ + public static function sample(int $size): SampleStage + { + return new SampleStage($size); + } + + /** + * Performs a full-text search of the field or fields in an Atlas collection. + * NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/search/ + * @param Document|Serializable|array|stdClass $search + */ + public static function search(Document|Serializable|stdClass|array $search): SearchStage + { + return new SearchStage($search); + } + + /** + * Returns different types of metadata result documents for the Atlas Search query against an Atlas collection. + * NOTE: $searchMeta is only available for MongoDB Atlas clusters running MongoDB v4.4.9 or higher, and is not available for self-managed deployments. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/searchMeta/ + * @param Document|Serializable|array|stdClass $meta + */ + public static function searchMeta(Document|Serializable|stdClass|array $meta): SearchMetaStage + { + return new SearchMetaStage($meta); + } + + /** + * Adds new fields to documents. Outputs documents that contain all existing fields from the input documents and newly added fields. + * Alias for $addFields. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/set/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$field + */ + public static function set(Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$field): SetStage + { + return new SetStage(...$field); + } + + /** + * Groups documents into windows and applies one or more operators to the documents in each window. + * New in MongoDB 5.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/ + * @param Document|Serializable|array|stdClass $sortBy Specifies the field(s) to sort the documents by in the partition. Uses the same syntax as the $sort stage. Default is no sorting. + * @param Document|Serializable|array|stdClass $output Specifies the field(s) to append to the documents in the output returned by the $setWindowFields stage. Each field is set to the result returned by the window operator. + * A field can contain dots to specify embedded document fields and array fields. The semantics for the embedded document dotted notation in the $setWindowFields stage are the same as the $addFields and $set stages. + * @param Optional|ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $partitionBy Specifies an expression to group the documents. In the $setWindowFields stage, the group of documents is known as a partition. Default is one partition for the entire collection. + */ + public static function setWindowFields( + Document|Serializable|stdClass|array $sortBy, + Document|Serializable|stdClass|array $output, + Optional|Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $partitionBy = Optional::Undefined, + ): SetWindowFieldsStage { + return new SetWindowFieldsStage($sortBy, $output, $partitionBy); + } + + /** + * Provides data and size distribution information on sharded collections. + * New in MongoDB 6.0.3. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/shardedDataDistribution/ + */ + public static function shardedDataDistribution(): ShardedDataDistributionStage + { + return new ShardedDataDistributionStage(); + } + + /** + * Skips the first n documents where n is the specified skip number and passes the remaining documents unmodified to the pipeline. For each input document, outputs either zero documents (for the first n documents) or one document (if after the first n documents). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/skip/ + * @param int $skip + */ + public static function skip(int $skip): SkipStage + { + return new SkipStage($skip); + } + + /** + * Reorders the document stream by a specified sort key. Only the order changes; the documents remain unmodified. For each input document, outputs one document. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/ + * @param ExpressionInterface|Sort|Type|array|bool|float|int|null|stdClass|string ...$sort + */ + public static function sort( + Type|ExpressionInterface|Sort|stdClass|array|bool|float|int|null|string ...$sort, + ): SortStage { + return new SortStage(...$sort); + } + + /** + * Groups incoming documents based on the value of a specified expression, then computes the count of documents in each distinct group. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortByCount/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public static function sortByCount( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $expression, + ): SortByCountStage { + return new SortByCountStage($expression); + } + + /** + * Performs a union of two collections; i.e. combines pipeline results from two collections into a single result set. + * New in MongoDB 4.4. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/ + * @param string $coll The collection or view whose pipeline results you wish to include in the result set. + * @param Optional|BSONArray|PackedArray|Pipeline|array $pipeline An aggregation pipeline to apply to the specified coll. + * The pipeline cannot include the $out and $merge stages. Starting in v6.0, the pipeline can contain the Atlas Search $search stage as the first stage inside the pipeline. + */ + public static function unionWith( + string $coll, + Optional|PackedArray|Pipeline|BSONArray|array $pipeline = Optional::Undefined, + ): UnionWithStage { + return new UnionWithStage($coll, $pipeline); + } + + /** + * Removes or excludes fields from documents. + * Alias for $project stage that removes or excludes fields. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/unset/ + * @no-named-arguments + * @param FieldPath|string ...$field + */ + public static function unset(FieldPath|string ...$field): UnsetStage + { + return new UnsetStage(...$field); + } + + /** + * Deconstructs an array field from the input documents to output a document for each element. Each output document replaces the array with an element value. For each input document, outputs n documents where n is the number of array elements and can be zero for an empty array. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/ + * @param ArrayFieldPath|string $path Field path to an array field. + * @param Optional|string $includeArrayIndex The name of a new field to hold the array index of the element. The name cannot start with a dollar sign $. + * @param Optional|bool $preserveNullAndEmptyArrays If true, if the path is null, missing, or an empty array, $unwind outputs the document. + * If false, if path is null, missing, or an empty array, $unwind does not output a document. + * The default value is false. + */ + public static function unwind( + ArrayFieldPath|string $path, + Optional|string $includeArrayIndex = Optional::Undefined, + Optional|bool $preserveNullAndEmptyArrays = Optional::Undefined, + ): UnwindStage { + return new UnwindStage($path, $includeArrayIndex, $preserveNullAndEmptyArrays); + } +} diff --git a/src/Builder/Stage/FillStage.php b/src/Builder/Stage/FillStage.php new file mode 100644 index 000000000..634c05b8d --- /dev/null +++ b/src/Builder/Stage/FillStage.php @@ -0,0 +1,88 @@ +output = $output; + $this->partitionBy = $partitionBy; + if (is_array($partitionByFields) && ! array_is_list($partitionByFields)) { + throw new InvalidArgumentException('Expected $partitionByFields argument to be a list, got an associative array.'); + } + + $this->partitionByFields = $partitionByFields; + $this->sortBy = $sortBy; + } + + public function getOperator(): string + { + return '$fill'; + } +} diff --git a/src/Builder/Stage/FluentFactoryTrait.php b/src/Builder/Stage/FluentFactoryTrait.php new file mode 100644 index 000000000..6f468322d --- /dev/null +++ b/src/Builder/Stage/FluentFactoryTrait.php @@ -0,0 +1,770 @@ +|stdClass> */ + public array $pipeline = []; + + public function getPipeline(): Pipeline + { + return new Pipeline(...$this->pipeline); + } + + /** + * Filters the document stream to allow only matching documents to pass unmodified into the next pipeline stage. $match uses standard MongoDB queries. For each input document, outputs either one document (a match) or zero documents (no match). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/ + * + * @param QueryInterface|FieldQueryInterface|Type|stdClass|array|bool|float|int|string|null ...$queries The query predicates to match + */ + public function match( + QueryInterface|FieldQueryInterface|Type|stdClass|array|string|int|float|bool|null ...$queries, + ): static { + $this->pipeline[] = Stage::match(...$queries); + + return $this; + } + + /** + * Adds new fields to documents. Outputs documents that contain all existing fields from the input documents and newly added fields. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/addFields/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$expression Specify the name of each field to add and set its value to an aggregation expression or an empty object. + */ + public function addFields( + Type|ExpressionInterface|stdClass|array|string|int|float|bool|null ...$expression, + ): static { + $this->pipeline[] = Stage::addFields(...$expression); + + return $this; + } + + /** + * Categorizes incoming documents into groups, called buckets, based on a specified expression and bucket boundaries. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/bucket/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $groupBy An expression to group documents by. To specify a field path, prefix the field name with a dollar sign $ and enclose it in quotes. + * Unless $bucket includes a default specification, each input document must resolve the groupBy field path or expression to a value that falls within one of the ranges specified by the boundaries. + * @param BSONArray|PackedArray|array $boundaries An array of values based on the groupBy expression that specify the boundaries for each bucket. Each adjacent pair of values acts as the inclusive lower boundary and the exclusive upper boundary for the bucket. You must specify at least two boundaries. + * The specified values must be in ascending order and all of the same type. The exception is if the values are of mixed numeric types, such as: + * @param Optional|ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $default A literal that specifies the _id of an additional bucket that contains all documents whose groupBy expression result does not fall into a bucket specified by boundaries. + * If unspecified, each input document must resolve the groupBy expression to a value within one of the bucket ranges specified by boundaries or the operation throws an error. + * The default value must be less than the lowest boundaries value, or greater than or equal to the highest boundaries value. + * The default value can be of a different type than the entries in boundaries. + * @param Optional|Document|Serializable|array|stdClass $output A document that specifies the fields to include in the output documents in addition to the _id field. To specify the field to include, you must use accumulator expressions. + * If you do not specify an output document, the operation returns a count field containing the number of documents in each bucket. + * If you specify an output document, only the fields specified in the document are returned; i.e. the count field is not returned unless it is explicitly included in the output document. + */ + public function bucket( + Type|ExpressionInterface|stdClass|array|string|int|float|bool|null $groupBy, + PackedArray|BSONArray|array $boundaries, + Optional|Type|ExpressionInterface|stdClass|array|string|int|float|bool|null $default = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $output = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::bucket($groupBy, $boundaries, $default, $output); + + return $this; + } + + /** + * Categorizes incoming documents into a specific number of groups, called buckets, based on a specified expression. Bucket boundaries are automatically determined in an attempt to evenly distribute the documents into the specified number of buckets. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/bucketAuto/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $groupBy An expression to group documents by. To specify a field path, prefix the field name with a dollar sign $ and enclose it in quotes. + * @param int $buckets A positive 32-bit integer that specifies the number of buckets into which input documents are grouped. + * @param Optional|Document|Serializable|array|stdClass $output A document that specifies the fields to include in the output documents in addition to the _id field. To specify the field to include, you must use accumulator expressions. + * The default count field is not included in the output document when output is specified. Explicitly specify the count expression as part of the output document to include it. + * @param Optional|Document|Serializable|array|stdClass $granularity A string that specifies the preferred number series to use to ensure that the calculated boundary edges end on preferred round numbers or their powers of 10. + * Available only if the all groupBy values are numeric and none of them are NaN. + */ + public function bucketAuto( + Type|ExpressionInterface|stdClass|array|string|int|float|bool|null $groupBy, + int $buckets, + Optional|Document|Serializable|stdClass|array $output = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $granularity = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::bucketAuto($groupBy, $buckets, $output, $granularity); + + return $this; + } + + /** + * Returns a Change Stream cursor for the collection or database. This stage can only occur once in an aggregation pipeline and it must occur as the first stage. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/changeStream/ + * @param Optional|bool $allChangesForCluster A flag indicating whether the stream should report all changes that occur on the deployment, aside from those on internal databases or collections. + * @param Optional|string $fullDocument Specifies whether change notifications include a copy of the full document when modified by update operations. + * @param Optional|string $fullDocumentBeforeChange Valid values are "off", "whenAvailable", or "required". If set to "off", the "fullDocumentBeforeChange" field of the output document is always omitted. If set to "whenAvailable", the "fullDocumentBeforeChange" field will be populated with the pre-image of the document modified by the current change event if such a pre-image is available, and will be omitted otherwise. If set to "required", then the "fullDocumentBeforeChange" field is always populated and an exception is thrown if the pre-image is not available. + * @param Optional|int $resumeAfter Specifies a resume token as the logical starting point for the change stream. Cannot be used with startAfter or startAtOperationTime fields. + * @param Optional|bool $showExpandedEvents Specifies whether to include additional change events, such as such as DDL and index operations. + * New in MongoDB 6.0. + * @param Optional|Document|Serializable|array|stdClass $startAfter Specifies a resume token as the logical starting point for the change stream. Cannot be used with resumeAfter or startAtOperationTime fields. + * @param Optional|Timestamp|int $startAtOperationTime Specifies a time as the logical starting point for the change stream. Cannot be used with resumeAfter or startAfter fields. + */ + public function changeStream( + Optional|bool $allChangesForCluster = Optional::Undefined, + Optional|string $fullDocument = Optional::Undefined, + Optional|string $fullDocumentBeforeChange = Optional::Undefined, + Optional|int $resumeAfter = Optional::Undefined, + Optional|bool $showExpandedEvents = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $startAfter = Optional::Undefined, + Optional|Timestamp|int $startAtOperationTime = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::changeStream($allChangesForCluster, $fullDocument, $fullDocumentBeforeChange, $resumeAfter, $showExpandedEvents, $startAfter, $startAtOperationTime); + + return $this; + } + + /** + * Splits large change stream events that exceed 16 MB into smaller fragments returned in a change stream cursor. + * You can only use $changeStreamSplitLargeEvent in a $changeStream pipeline and it must be the final stage in the pipeline. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/changeStreamSplitLargeEvent/ + */ + public function changeStreamSplitLargeEvent(): static + { + $this->pipeline[] = Stage::changeStreamSplitLargeEvent(); + + return $this; + } + + /** + * Returns statistics regarding a collection or view. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/collStats/ + * @param Optional|Document|Serializable|array|stdClass $latencyStats + * @param Optional|Document|Serializable|array|stdClass $storageStats + * @param Optional|Document|Serializable|array|stdClass $count + * @param Optional|Document|Serializable|array|stdClass $queryExecStats + */ + public function collStats( + Optional|Document|Serializable|stdClass|array $latencyStats = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $storageStats = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $count = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $queryExecStats = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::collStats($latencyStats, $storageStats, $count, $queryExecStats); + + return $this; + } + + /** + * Returns a count of the number of documents at this stage of the aggregation pipeline. + * Distinct from the $count aggregation accumulator. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/count/ + * @param string $field Name of the output field which has the count as its value. It must be a non-empty string, must not start with $ and must not contain the . character. + */ + public function count(string $field): static + { + $this->pipeline[] = Stage::count($field); + + return $this; + } + + /** + * Returns information on active and/or dormant operations for the MongoDB deployment. To run, use the db.aggregate() method. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/currentOp/ + * @param Optional|bool $allUsers + * @param Optional|bool $idleConnections + * @param Optional|bool $idleCursors + * @param Optional|bool $idleSessions + * @param Optional|bool $localOps + */ + public function currentOp( + Optional|bool $allUsers = Optional::Undefined, + Optional|bool $idleConnections = Optional::Undefined, + Optional|bool $idleCursors = Optional::Undefined, + Optional|bool $idleSessions = Optional::Undefined, + Optional|bool $localOps = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::currentOp($allUsers, $idleConnections, $idleCursors, $idleSessions, $localOps); + + return $this; + } + + /** + * Creates new documents in a sequence of documents where certain values in a field are missing. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/densify/ + * @param string $field The field to densify. The values of the specified field must either be all numeric values or all dates. + * Documents that do not contain the specified field continue through the pipeline unmodified. + * To specify a in an embedded document or in an array, use dot notation. + * @param Document|Serializable|array|stdClass $range Specification for range based densification. + * @param Optional|BSONArray|PackedArray|array $partitionByFields The field(s) that will be used as the partition keys. + */ + public function densify( + string $field, + Document|Serializable|stdClass|array $range, + Optional|PackedArray|BSONArray|array $partitionByFields = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::densify($field, $range, $partitionByFields); + + return $this; + } + + /** + * Returns literal documents from input values. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/documents/ + * @param BSONArray|PackedArray|ResolvesToArray|array $documents $documents accepts any valid expression that resolves to an array of objects. This includes: + * - system variables, such as $$NOW or $$SEARCH_META + * - $let expressions + * - variables in scope from $lookup expressions + * Expressions that do not resolve to a current document, like $myField or $$ROOT, will result in an error. + */ + public function documents(PackedArray|ResolvesToArray|BSONArray|array $documents): static + { + $this->pipeline[] = Stage::documents($documents); + + return $this; + } + + /** + * Processes multiple aggregation pipelines within a single stage on the same set of input documents. Enables the creation of multi-faceted aggregations capable of characterizing data across multiple dimensions, or facets, in a single stage. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/facet/ + * @param BSONArray|PackedArray|Pipeline|array ...$facet + */ + public function facet(PackedArray|Pipeline|BSONArray|array ...$facet): static + { + $this->pipeline[] = Stage::facet(...$facet); + + return $this; + } + + /** + * Populates null and missing field values within documents. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/fill/ + * @param Document|Serializable|array|stdClass $output Specifies an object containing each field for which to fill missing values. You can specify multiple fields in the output object. + * The object name is the name of the field to fill. The object value specifies how the field is filled. + * @param Optional|Document|Serializable|array|stdClass|string $partitionBy Specifies an expression to group the documents. In the $fill stage, a group of documents is known as a partition. + * If you omit partitionBy and partitionByFields, $fill uses one partition for the entire collection. + * partitionBy and partitionByFields are mutually exclusive. + * @param Optional|BSONArray|PackedArray|array $partitionByFields Specifies an array of fields as the compound key to group the documents. In the $fill stage, each group of documents is known as a partition. + * If you omit partitionBy and partitionByFields, $fill uses one partition for the entire collection. + * partitionBy and partitionByFields are mutually exclusive. + * @param Optional|Document|Serializable|array|stdClass $sortBy Specifies the field or fields to sort the documents within each partition. Uses the same syntax as the $sort stage. + */ + public function fill( + Document|Serializable|stdClass|array $output, + Optional|Document|Serializable|stdClass|array|string $partitionBy = Optional::Undefined, + Optional|PackedArray|BSONArray|array $partitionByFields = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $sortBy = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::fill($output, $partitionBy, $partitionByFields, $sortBy); + + return $this; + } + + /** + * Returns an ordered stream of documents based on the proximity to a geospatial point. Incorporates the functionality of $match, $sort, and $limit for geospatial data. The output documents include an additional distance field and can include a location identifier field. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/geoNear/ + * @param string $distanceField The output field that contains the calculated distance. To specify a field within an embedded document, use dot notation. + * @param Document|ResolvesToObject|Serializable|array|stdClass $near The point for which to find the closest documents. + * @param Optional|Decimal128|Int64|float|int $distanceMultiplier The factor to multiply all distances returned by the query. For example, use the distanceMultiplier to convert radians, as returned by a spherical query, to kilometers by multiplying by the radius of the Earth. + * @param Optional|string $includeLocs This specifies the output field that identifies the location used to calculate the distance. This option is useful when a location field contains multiple locations. To specify a field within an embedded document, use dot notation. + * @param Optional|string $key Specify the geospatial indexed field to use when calculating the distance. + * @param Optional|Decimal128|Int64|float|int $maxDistance The maximum distance from the center point that the documents can be. MongoDB limits the results to those documents that fall within the specified distance from the center point. + * Specify the distance in meters if the specified point is GeoJSON and in radians if the specified point is legacy coordinate pairs. + * @param Optional|Decimal128|Int64|float|int $minDistance The minimum distance from the center point that the documents can be. MongoDB limits the results to those documents that fall outside the specified distance from the center point. + * Specify the distance in meters for GeoJSON data and in radians for legacy coordinate pairs. + * @param Optional|QueryInterface|array $query Limits the results to the documents that match the query. The query syntax is the usual MongoDB read operation query syntax. + * You cannot specify a $near predicate in the query field of the $geoNear stage. + * @param Optional|bool $spherical Determines how MongoDB calculates the distance between two points: + * - When true, MongoDB uses $nearSphere semantics and calculates distances using spherical geometry. + * - When false, MongoDB uses $near semantics: spherical geometry for 2dsphere indexes and planar geometry for 2d indexes. + * Default: false. + */ + public function geoNear( + string $distanceField, + Document|Serializable|ResolvesToObject|stdClass|array $near, + Optional|Decimal128|Int64|int|float $distanceMultiplier = Optional::Undefined, + Optional|string $includeLocs = Optional::Undefined, + Optional|string $key = Optional::Undefined, + Optional|Decimal128|Int64|int|float $maxDistance = Optional::Undefined, + Optional|Decimal128|Int64|int|float $minDistance = Optional::Undefined, + Optional|QueryInterface|array $query = Optional::Undefined, + Optional|bool $spherical = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::geoNear($distanceField, $near, $distanceMultiplier, $includeLocs, $key, $maxDistance, $minDistance, $query, $spherical); + + return $this; + } + + /** + * Performs a recursive search on a collection. To each output document, adds a new array field that contains the traversal results of the recursive search for that document. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/graphLookup/ + * @param string $from Target collection for the $graphLookup operation to search, recursively matching the connectFromField to the connectToField. The from collection must be in the same database as any other collections used in the operation. + * Starting in MongoDB 5.1, the collection specified in the from parameter can be sharded. + * @param BSONArray|ExpressionInterface|PackedArray|Type|array|bool|float|int|null|stdClass|string $startWith Expression that specifies the value of the connectFromField with which to start the recursive search. Optionally, startWith may be array of values, each of which is individually followed through the traversal process. + * @param string $connectFromField Field name whose value $graphLookup uses to recursively match against the connectToField of other documents in the collection. If the value is an array, each element is individually followed through the traversal process. + * @param string $connectToField Field name in other documents against which to match the value of the field specified by the connectFromField parameter. + * @param string $as Name of the array field added to each output document. Contains the documents traversed in the $graphLookup stage to reach the document. + * @param Optional|int $maxDepth Non-negative integral number specifying the maximum recursion depth. + * @param Optional|string $depthField Name of the field to add to each traversed document in the search path. The value of this field is the recursion depth for the document, represented as a NumberLong. Recursion depth value starts at zero, so the first lookup corresponds to zero depth. + * @param Optional|QueryInterface|array $restrictSearchWithMatch A document specifying additional conditions for the recursive search. The syntax is identical to query filter syntax. + */ + public function graphLookup( + string $from, + PackedArray|Type|ExpressionInterface|BSONArray|stdClass|array|string|int|float|bool|null $startWith, + string $connectFromField, + string $connectToField, + string $as, + Optional|int $maxDepth = Optional::Undefined, + Optional|string $depthField = Optional::Undefined, + Optional|QueryInterface|array $restrictSearchWithMatch = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::graphLookup($from, $startWith, $connectFromField, $connectToField, $as, $maxDepth, $depthField, $restrictSearchWithMatch); + + return $this; + } + + /** + * Groups input documents by a specified identifier expression and applies the accumulator expression(s), if specified, to each group. Consumes all input documents and outputs one document per each distinct group. The output documents only contain the identifier field and, if specified, accumulated fields. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/group/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $_id The _id expression specifies the group key. If you specify an _id value of null, or any other constant value, the $group stage returns a single document that aggregates values across all of the input documents. + * @param AccumulatorInterface|Document|Serializable|array|stdClass ...$field Computed using the accumulator operators. + */ + public function group( + Type|ExpressionInterface|stdClass|array|string|int|float|bool|null $_id, + Document|Serializable|AccumulatorInterface|stdClass|array ...$field, + ): static { + $this->pipeline[] = Stage::group($_id, ...$field); + + return $this; + } + + /** + * Returns statistics regarding the use of each index for the collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/indexStats/ + */ + public function indexStats(): static + { + $this->pipeline[] = Stage::indexStats(); + + return $this; + } + + /** + * Passes the first n documents unmodified to the pipeline where n is the specified limit. For each input document, outputs either one document (for the first n documents) or zero documents (after the first n documents). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/limit/ + * @param int $limit + */ + public function limit(int $limit): static + { + $this->pipeline[] = Stage::limit($limit); + + return $this; + } + + /** + * Lists all active sessions recently in use on the currently connected mongos or mongod instance. These sessions may have not yet propagated to the system.sessions collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/listLocalSessions/ + * @param Optional|BSONArray|PackedArray|array $users Returns all sessions for the specified users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster to list sessions for other users. + * @param Optional|bool $allUsers Returns all sessions for all users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster. + */ + public function listLocalSessions( + Optional|PackedArray|BSONArray|array $users = Optional::Undefined, + Optional|bool $allUsers = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::listLocalSessions($users, $allUsers); + + return $this; + } + + /** + * Lists sampled queries for all collections or a specific collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSampledQueries/ + * @param Optional|string $namespace + */ + public function listSampledQueries(Optional|string $namespace = Optional::Undefined): static + { + $this->pipeline[] = Stage::listSampledQueries($namespace); + + return $this; + } + + /** + * Returns information about existing Atlas Search indexes on a specified collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSearchIndexes/ + * @param Optional|string $id The id of the index to return information about. + * @param Optional|string $name The name of the index to return information about. + */ + public function listSearchIndexes( + Optional|string $id = Optional::Undefined, + Optional|string $name = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::listSearchIndexes($id, $name); + + return $this; + } + + /** + * Lists all sessions that have been active long enough to propagate to the system.sessions collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/listSessions/ + * @param Optional|BSONArray|PackedArray|array $users Returns all sessions for the specified users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster to list sessions for other users. + * @param Optional|bool $allUsers Returns all sessions for all users. If running with access control, the authenticated user must have privileges with listSessions action on the cluster. + */ + public function listSessions( + Optional|PackedArray|BSONArray|array $users = Optional::Undefined, + Optional|bool $allUsers = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::listSessions($users, $allUsers); + + return $this; + } + + /** + * Performs a left outer join to another collection in the same database to filter in documents from the "joined" collection for processing. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/ + * @param string $as Specifies the name of the new array field to add to the input documents. The new array field contains the matching documents from the from collection. If the specified name already exists in the input document, the existing field is overwritten. + * @param Optional|string $from Specifies the collection in the same database to perform the join with. + * from is optional, you can use a $documents stage in a $lookup stage instead. For an example, see Use a $documents Stage in a $lookup Stage. + * Starting in MongoDB 5.1, the collection specified in the from parameter can be sharded. + * @param Optional|string $localField Specifies the field from the documents input to the $lookup stage. $lookup performs an equality match on the localField to the foreignField from the documents of the from collection. If an input document does not contain the localField, the $lookup treats the field as having a value of null for matching purposes. + * @param Optional|string $foreignField Specifies the field from the documents in the from collection. $lookup performs an equality match on the foreignField to the localField from the input documents. If a document in the from collection does not contain the foreignField, the $lookup treats the value as null for matching purposes. + * @param Optional|Document|Serializable|array|stdClass $let Specifies variables to use in the pipeline stages. Use the variable expressions to access the fields from the joined collection's documents that are input to the pipeline. + * @param Optional|BSONArray|PackedArray|Pipeline|array $pipeline Specifies the pipeline to run on the joined collection. The pipeline determines the resulting documents from the joined collection. To return all documents, specify an empty pipeline []. + * The pipeline cannot include the $out stage or the $merge stage. Starting in v6.0, the pipeline can contain the Atlas Search $search stage as the first stage inside the pipeline. + * The pipeline cannot directly access the joined document fields. Instead, define variables for the joined document fields using the let option and then reference the variables in the pipeline stages. + */ + public function lookup( + string $as, + Optional|string $from = Optional::Undefined, + Optional|string $localField = Optional::Undefined, + Optional|string $foreignField = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $let = Optional::Undefined, + Optional|PackedArray|Pipeline|BSONArray|array $pipeline = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::lookup($as, $from, $localField, $foreignField, $let, $pipeline); + + return $this; + } + + /** + * Writes the resulting documents of the aggregation pipeline to a collection. The stage can incorporate (insert new documents, merge documents, replace documents, keep existing documents, fail the operation, process documents with a custom update pipeline) the results into an output collection. To use the $merge stage, it must be the last stage in the pipeline. + * New in MongoDB 4.2. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/merge/ + * @param Document|Serializable|array|stdClass|string $into The output collection. + * @param Optional|BSONArray|PackedArray|array|string $on Field or fields that act as a unique identifier for a document. The identifier determines if a results document matches an existing document in the output collection. + * @param Optional|Document|Serializable|array|stdClass $let Specifies variables for use in the whenMatched pipeline. + * @param Optional|BSONArray|PackedArray|Pipeline|array|string $whenMatched The behavior of $merge if a result document and an existing document in the collection have the same value for the specified on field(s). + * @param Optional|string $whenNotMatched The behavior of $merge if a result document does not match an existing document in the out collection. + */ + public function merge( + Document|Serializable|stdClass|array|string $into, + Optional|PackedArray|BSONArray|array|string $on = Optional::Undefined, + Optional|Document|Serializable|stdClass|array $let = Optional::Undefined, + Optional|PackedArray|Pipeline|BSONArray|array|string $whenMatched = Optional::Undefined, + Optional|string $whenNotMatched = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::merge($into, $on, $let, $whenMatched, $whenNotMatched); + + return $this; + } + + /** + * Writes the resulting documents of the aggregation pipeline to a collection. To use the $out stage, it must be the last stage in the pipeline. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/out/ + * @param Document|Serializable|array|stdClass|string $coll Target database name to write documents from $out to. + */ + public function out(Document|Serializable|stdClass|array|string $coll): static + { + $this->pipeline[] = Stage::out($coll); + + return $this; + } + + /** + * Returns plan cache information for a collection. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/planCacheStats/ + */ + public function planCacheStats(): static + { + $this->pipeline[] = Stage::planCacheStats(); + + return $this; + } + + /** + * Reshapes each document in the stream, such as by adding new fields or removing existing fields. For each input document, outputs one document. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$specification + */ + public function project( + Type|ExpressionInterface|stdClass|array|string|int|float|bool|null ...$specification, + ): static { + $this->pipeline[] = Stage::project(...$specification); + + return $this; + } + + /** + * Reshapes each document in the stream by restricting the content for each document based on information stored in the documents themselves. Incorporates the functionality of $project and $match. Can be used to implement field level redaction. For each input document, outputs either one or zero documents. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/redact/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public function redact(Type|ExpressionInterface|stdClass|array|string|int|float|bool|null $expression): static + { + $this->pipeline[] = Stage::redact($expression); + + return $this; + } + + /** + * Replaces a document with the specified embedded document. The operation replaces all existing fields in the input document, including the _id field. Specify a document embedded in the input document to promote the embedded document to the top level. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceRoot/ + * @param Document|ResolvesToObject|Serializable|array|stdClass $newRoot + */ + public function replaceRoot(Document|Serializable|ResolvesToObject|stdClass|array $newRoot): static + { + $this->pipeline[] = Stage::replaceRoot($newRoot); + + return $this; + } + + /** + * Replaces a document with the specified embedded document. The operation replaces all existing fields in the input document, including the _id field. Specify a document embedded in the input document to promote the embedded document to the top level. + * Alias for $replaceRoot. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/replaceWith/ + * @param Document|ResolvesToObject|Serializable|array|stdClass $expression + */ + public function replaceWith(Document|Serializable|ResolvesToObject|stdClass|array $expression): static + { + $this->pipeline[] = Stage::replaceWith($expression); + + return $this; + } + + /** + * Randomly selects the specified number of documents from its input. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sample/ + * @param int $size The number of documents to randomly select. + */ + public function sample(int $size): static + { + $this->pipeline[] = Stage::sample($size); + + return $this; + } + + /** + * Performs a full-text search of the field or fields in an Atlas collection. + * NOTE: $search is only available for MongoDB Atlas clusters, and is not available for self-managed deployments. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/search/ + * @param Document|Serializable|array|stdClass $search + */ + public function search(Document|Serializable|stdClass|array $search): static + { + $this->pipeline[] = Stage::search($search); + + return $this; + } + + /** + * Returns different types of metadata result documents for the Atlas Search query against an Atlas collection. + * NOTE: $searchMeta is only available for MongoDB Atlas clusters running MongoDB v4.4.9 or higher, and is not available for self-managed deployments. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/searchMeta/ + * @param Document|Serializable|array|stdClass $meta + */ + public function searchMeta(Document|Serializable|stdClass|array $meta): static + { + $this->pipeline[] = Stage::searchMeta($meta); + + return $this; + } + + /** + * Adds new fields to documents. Outputs documents that contain all existing fields from the input documents and newly added fields. + * Alias for $addFields. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/set/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$field + */ + public function set(Type|ExpressionInterface|stdClass|array|string|int|float|bool|null ...$field): static + { + $this->pipeline[] = Stage::set(...$field); + + return $this; + } + + /** + * Groups documents into windows and applies one or more operators to the documents in each window. + * New in MongoDB 5.0. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/ + * @param Document|Serializable|array|stdClass $sortBy Specifies the field(s) to sort the documents by in the partition. Uses the same syntax as the $sort stage. Default is no sorting. + * @param Document|Serializable|array|stdClass $output Specifies the field(s) to append to the documents in the output returned by the $setWindowFields stage. Each field is set to the result returned by the window operator. + * A field can contain dots to specify embedded document fields and array fields. The semantics for the embedded document dotted notation in the $setWindowFields stage are the same as the $addFields and $set stages. + * @param Optional|ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $partitionBy Specifies an expression to group the documents. In the $setWindowFields stage, the group of documents is known as a partition. Default is one partition for the entire collection. + */ + public function setWindowFields( + Document|Serializable|stdClass|array $sortBy, + Document|Serializable|stdClass|array $output, + Optional|Type|ExpressionInterface|stdClass|array|string|int|float|bool|null $partitionBy = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::setWindowFields($sortBy, $output, $partitionBy); + + return $this; + } + + /** + * Provides data and size distribution information on sharded collections. + * New in MongoDB 6.0.3. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/shardedDataDistribution/ + */ + public function shardedDataDistribution(): static + { + $this->pipeline[] = Stage::shardedDataDistribution(); + + return $this; + } + + /** + * Skips the first n documents where n is the specified skip number and passes the remaining documents unmodified to the pipeline. For each input document, outputs either zero documents (for the first n documents) or one document (if after the first n documents). + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/skip/ + * @param int $skip + */ + public function skip(int $skip): static + { + $this->pipeline[] = Stage::skip($skip); + + return $this; + } + + /** + * Reorders the document stream by a specified sort key. Only the order changes; the documents remain unmodified. For each input document, outputs one document. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/ + * @param ExpressionInterface|Sort|Type|array|bool|float|int|null|stdClass|string ...$sort + */ + public function sort(Type|ExpressionInterface|Sort|stdClass|array|string|int|float|bool|null ...$sort): static + { + $this->pipeline[] = Stage::sort(...$sort); + + return $this; + } + + /** + * Groups incoming documents based on the value of a specified expression, then computes the count of documents in each distinct group. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortByCount/ + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $expression + */ + public function sortByCount( + Type|ExpressionInterface|stdClass|array|string|int|float|bool|null $expression, + ): static { + $this->pipeline[] = Stage::sortByCount($expression); + + return $this; + } + + /** + * Performs a union of two collections; i.e. combines pipeline results from two collections into a single result set. + * New in MongoDB 4.4. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/unionWith/ + * @param string $coll The collection or view whose pipeline results you wish to include in the result set. + * @param Optional|BSONArray|PackedArray|Pipeline|array $pipeline An aggregation pipeline to apply to the specified coll. + * The pipeline cannot include the $out and $merge stages. Starting in v6.0, the pipeline can contain the Atlas Search $search stage as the first stage inside the pipeline. + */ + public function unionWith( + string $coll, + Optional|PackedArray|Pipeline|BSONArray|array $pipeline = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::unionWith($coll, $pipeline); + + return $this; + } + + /** + * Removes or excludes fields from documents. + * Alias for $project stage that removes or excludes fields. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/unset/ + * @no-named-arguments + * @param FieldPath|string ...$field + */ + public function unset(FieldPath|string ...$field): static + { + $this->pipeline[] = Stage::unset(...$field); + + return $this; + } + + /** + * Deconstructs an array field from the input documents to output a document for each element. Each output document replaces the array with an element value. For each input document, outputs n documents where n is the number of array elements and can be zero for an empty array. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/unwind/ + * @param ArrayFieldPath|string $path Field path to an array field. + * @param Optional|string $includeArrayIndex The name of a new field to hold the array index of the element. The name cannot start with a dollar sign $. + * @param Optional|bool $preserveNullAndEmptyArrays If true, if the path is null, missing, or an empty array, $unwind outputs the document. + * If false, if path is null, missing, or an empty array, $unwind does not output a document. + * The default value is false. + */ + public function unwind( + ArrayFieldPath|string $path, + Optional|string $includeArrayIndex = Optional::Undefined, + Optional|bool $preserveNullAndEmptyArrays = Optional::Undefined, + ): static { + $this->pipeline[] = Stage::unwind($path, $includeArrayIndex, $preserveNullAndEmptyArrays); + + return $this; + } +} diff --git a/src/Builder/Stage/GeoNearStage.php b/src/Builder/Stage/GeoNearStage.php new file mode 100644 index 000000000..d39766dc5 --- /dev/null +++ b/src/Builder/Stage/GeoNearStage.php @@ -0,0 +1,123 @@ +distanceField = $distanceField; + $this->near = $near; + $this->distanceMultiplier = $distanceMultiplier; + $this->includeLocs = $includeLocs; + $this->key = $key; + $this->maxDistance = $maxDistance; + $this->minDistance = $minDistance; + if (is_array($query)) { + $query = QueryObject::create($query); + } + + $this->query = $query; + $this->spherical = $spherical; + } + + public function getOperator(): string + { + return '$geoNear'; + } +} diff --git a/src/Builder/Stage/GraphLookupStage.php b/src/Builder/Stage/GraphLookupStage.php new file mode 100644 index 000000000..ceaa7a4b1 --- /dev/null +++ b/src/Builder/Stage/GraphLookupStage.php @@ -0,0 +1,106 @@ +from = $from; + if (is_array($startWith) && ! array_is_list($startWith)) { + throw new InvalidArgumentException('Expected $startWith argument to be a list, got an associative array.'); + } + + $this->startWith = $startWith; + $this->connectFromField = $connectFromField; + $this->connectToField = $connectToField; + $this->as = $as; + $this->maxDepth = $maxDepth; + $this->depthField = $depthField; + if (is_array($restrictSearchWithMatch)) { + $restrictSearchWithMatch = QueryObject::create($restrictSearchWithMatch); + } + + $this->restrictSearchWithMatch = $restrictSearchWithMatch; + } + + public function getOperator(): string + { + return '$graphLookup'; + } +} diff --git a/src/Builder/Stage/GroupStage.php b/src/Builder/Stage/GroupStage.php new file mode 100644 index 000000000..a16038007 --- /dev/null +++ b/src/Builder/Stage/GroupStage.php @@ -0,0 +1,62 @@ + $field Computed using the accumulator operators. */ + public readonly stdClass $field; + + /** + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string $_id The _id expression specifies the group key. If you specify an _id value of null, or any other constant value, the $group stage returns a single document that aggregates values across all of the input documents. + * @param AccumulatorInterface|Document|Serializable|array|stdClass ...$field Computed using the accumulator operators. + */ + public function __construct( + Type|ExpressionInterface|stdClass|array|bool|float|int|null|string $_id, + Document|Serializable|AccumulatorInterface|stdClass|array ...$field, + ) { + $this->_id = $_id; + foreach($field as $key => $value) { + if (! is_string($key)) { + throw new InvalidArgumentException('Expected $field arguments to be a map (object), named arguments (:) or array unpacking ...[\'\' => ] must be used'); + } + } + + $field = (object) $field; + $this->field = $field; + } + + public function getOperator(): string + { + return '$group'; + } +} diff --git a/src/Builder/Stage/IndexStatsStage.php b/src/Builder/Stage/IndexStatsStage.php new file mode 100644 index 000000000..4fca140bb --- /dev/null +++ b/src/Builder/Stage/IndexStatsStage.php @@ -0,0 +1,32 @@ +limit = $limit; + } + + public function getOperator(): string + { + return '$limit'; + } +} diff --git a/src/Builder/Stage/ListLocalSessionsStage.php b/src/Builder/Stage/ListLocalSessionsStage.php new file mode 100644 index 000000000..de60e49f4 --- /dev/null +++ b/src/Builder/Stage/ListLocalSessionsStage.php @@ -0,0 +1,57 @@ +users = $users; + $this->allUsers = $allUsers; + } + + public function getOperator(): string + { + return '$listLocalSessions'; + } +} diff --git a/src/Builder/Stage/ListSampledQueriesStage.php b/src/Builder/Stage/ListSampledQueriesStage.php new file mode 100644 index 000000000..8669c0fe8 --- /dev/null +++ b/src/Builder/Stage/ListSampledQueriesStage.php @@ -0,0 +1,40 @@ +namespace = $namespace; + } + + public function getOperator(): string + { + return '$listSampledQueries'; + } +} diff --git a/src/Builder/Stage/ListSearchIndexesStage.php b/src/Builder/Stage/ListSearchIndexesStage.php new file mode 100644 index 000000000..a2850eab2 --- /dev/null +++ b/src/Builder/Stage/ListSearchIndexesStage.php @@ -0,0 +1,47 @@ +id = $id; + $this->name = $name; + } + + public function getOperator(): string + { + return '$listSearchIndexes'; + } +} diff --git a/src/Builder/Stage/ListSessionsStage.php b/src/Builder/Stage/ListSessionsStage.php new file mode 100644 index 000000000..f299874db --- /dev/null +++ b/src/Builder/Stage/ListSessionsStage.php @@ -0,0 +1,57 @@ +users = $users; + $this->allUsers = $allUsers; + } + + public function getOperator(): string + { + return '$listSessions'; + } +} diff --git a/src/Builder/Stage/LookupStage.php b/src/Builder/Stage/LookupStage.php new file mode 100644 index 000000000..34f0fb66f --- /dev/null +++ b/src/Builder/Stage/LookupStage.php @@ -0,0 +1,97 @@ +as = $as; + $this->from = $from; + $this->localField = $localField; + $this->foreignField = $foreignField; + $this->let = $let; + if (is_array($pipeline) && ! array_is_list($pipeline)) { + throw new InvalidArgumentException('Expected $pipeline argument to be a list, got an associative array.'); + } + + $this->pipeline = $pipeline; + } + + public function getOperator(): string + { + return '$lookup'; + } +} diff --git a/src/Builder/Stage/MatchStage.php b/src/Builder/Stage/MatchStage.php new file mode 100644 index 000000000..9590763ae --- /dev/null +++ b/src/Builder/Stage/MatchStage.php @@ -0,0 +1,47 @@ +query = $query; + } + + public function getOperator(): string + { + return '$match'; + } +} diff --git a/src/Builder/Stage/MergeStage.php b/src/Builder/Stage/MergeStage.php new file mode 100644 index 000000000..271c012ac --- /dev/null +++ b/src/Builder/Stage/MergeStage.php @@ -0,0 +1,84 @@ +into = $into; + if (is_array($on) && ! array_is_list($on)) { + throw new InvalidArgumentException('Expected $on argument to be a list, got an associative array.'); + } + + $this->on = $on; + $this->let = $let; + if (is_array($whenMatched) && ! array_is_list($whenMatched)) { + throw new InvalidArgumentException('Expected $whenMatched argument to be a list, got an associative array.'); + } + + $this->whenMatched = $whenMatched; + $this->whenNotMatched = $whenNotMatched; + } + + public function getOperator(): string + { + return '$merge'; + } +} diff --git a/src/Builder/Stage/OutStage.php b/src/Builder/Stage/OutStage.php new file mode 100644 index 000000000..5e99d578a --- /dev/null +++ b/src/Builder/Stage/OutStage.php @@ -0,0 +1,42 @@ +coll = $coll; + } + + public function getOperator(): string + { + return '$out'; + } +} diff --git a/src/Builder/Stage/PlanCacheStatsStage.php b/src/Builder/Stage/PlanCacheStatsStage.php new file mode 100644 index 000000000..98755095f --- /dev/null +++ b/src/Builder/Stage/PlanCacheStatsStage.php @@ -0,0 +1,32 @@ + $specification */ + public readonly stdClass $specification; + + /** + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$specification + */ + public function __construct(Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$specification) + { + if (\count($specification) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $specification, got %d.', 1, \count($specification))); + } + + foreach($specification as $key => $value) { + if (! is_string($key)) { + throw new InvalidArgumentException('Expected $specification arguments to be a map (object), named arguments (:) or array unpacking ...[\'\' => ] must be used'); + } + } + + $specification = (object) $specification; + $this->specification = $specification; + } + + public function getOperator(): string + { + return '$project'; + } +} diff --git a/src/Builder/Stage/RedactStage.php b/src/Builder/Stage/RedactStage.php new file mode 100644 index 000000000..96dcab949 --- /dev/null +++ b/src/Builder/Stage/RedactStage.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$redact'; + } +} diff --git a/src/Builder/Stage/ReplaceRootStage.php b/src/Builder/Stage/ReplaceRootStage.php new file mode 100644 index 000000000..5c97b1f15 --- /dev/null +++ b/src/Builder/Stage/ReplaceRootStage.php @@ -0,0 +1,43 @@ +newRoot = $newRoot; + } + + public function getOperator(): string + { + return '$replaceRoot'; + } +} diff --git a/src/Builder/Stage/ReplaceWithStage.php b/src/Builder/Stage/ReplaceWithStage.php new file mode 100644 index 000000000..6741a38fc --- /dev/null +++ b/src/Builder/Stage/ReplaceWithStage.php @@ -0,0 +1,44 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$replaceWith'; + } +} diff --git a/src/Builder/Stage/SampleStage.php b/src/Builder/Stage/SampleStage.php new file mode 100644 index 000000000..9cf433eb5 --- /dev/null +++ b/src/Builder/Stage/SampleStage.php @@ -0,0 +1,39 @@ +size = $size; + } + + public function getOperator(): string + { + return '$sample'; + } +} diff --git a/src/Builder/Stage/SearchMetaStage.php b/src/Builder/Stage/SearchMetaStage.php new file mode 100644 index 000000000..c27b230eb --- /dev/null +++ b/src/Builder/Stage/SearchMetaStage.php @@ -0,0 +1,43 @@ +meta = $meta; + } + + public function getOperator(): string + { + return '$searchMeta'; + } +} diff --git a/src/Builder/Stage/SearchStage.php b/src/Builder/Stage/SearchStage.php new file mode 100644 index 000000000..3eed9c17d --- /dev/null +++ b/src/Builder/Stage/SearchStage.php @@ -0,0 +1,43 @@ +search = $search; + } + + public function getOperator(): string + { + return '$search'; + } +} diff --git a/src/Builder/Stage/SetStage.php b/src/Builder/Stage/SetStage.php new file mode 100644 index 000000000..ca929a2b8 --- /dev/null +++ b/src/Builder/Stage/SetStage.php @@ -0,0 +1,57 @@ + $field */ + public readonly stdClass $field; + + /** + * @param ExpressionInterface|Type|array|bool|float|int|null|stdClass|string ...$field + */ + public function __construct(Type|ExpressionInterface|stdClass|array|bool|float|int|null|string ...$field) + { + if (\count($field) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $field, got %d.', 1, \count($field))); + } + + foreach($field as $key => $value) { + if (! is_string($key)) { + throw new InvalidArgumentException('Expected $field arguments to be a map (object), named arguments (:) or array unpacking ...[\'\' => ] must be used'); + } + } + + $field = (object) $field; + $this->field = $field; + } + + public function getOperator(): string + { + return '$set'; + } +} diff --git a/src/Builder/Stage/SetWindowFieldsStage.php b/src/Builder/Stage/SetWindowFieldsStage.php new file mode 100644 index 000000000..cb1354e66 --- /dev/null +++ b/src/Builder/Stage/SetWindowFieldsStage.php @@ -0,0 +1,63 @@ +sortBy = $sortBy; + $this->output = $output; + $this->partitionBy = $partitionBy; + } + + public function getOperator(): string + { + return '$setWindowFields'; + } +} diff --git a/src/Builder/Stage/ShardedDataDistributionStage.php b/src/Builder/Stage/ShardedDataDistributionStage.php new file mode 100644 index 000000000..de3c3f3a0 --- /dev/null +++ b/src/Builder/Stage/ShardedDataDistributionStage.php @@ -0,0 +1,33 @@ +skip = $skip; + } + + public function getOperator(): string + { + return '$skip'; + } +} diff --git a/src/Builder/Stage/SortByCountStage.php b/src/Builder/Stage/SortByCountStage.php new file mode 100644 index 000000000..9c7160b8c --- /dev/null +++ b/src/Builder/Stage/SortByCountStage.php @@ -0,0 +1,42 @@ +expression = $expression; + } + + public function getOperator(): string + { + return '$sortByCount'; + } +} diff --git a/src/Builder/Stage/SortStage.php b/src/Builder/Stage/SortStage.php new file mode 100644 index 000000000..5f4de9e19 --- /dev/null +++ b/src/Builder/Stage/SortStage.php @@ -0,0 +1,57 @@ + $sort */ + public readonly stdClass $sort; + + /** + * @param ExpressionInterface|Sort|Type|array|bool|float|int|null|stdClass|string ...$sort + */ + public function __construct(Type|ExpressionInterface|Sort|stdClass|array|bool|float|int|null|string ...$sort) + { + if (\count($sort) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $sort, got %d.', 1, \count($sort))); + } + + foreach($sort as $key => $value) { + if (! is_string($key)) { + throw new InvalidArgumentException('Expected $sort arguments to be a map (object), named arguments (:) or array unpacking ...[\'\' => ] must be used'); + } + } + + $sort = (object) $sort; + $this->sort = $sort; + } + + public function getOperator(): string + { + return '$sort'; + } +} diff --git a/src/Builder/Stage/UnionWithStage.php b/src/Builder/Stage/UnionWithStage.php new file mode 100644 index 000000000..0882f74cb --- /dev/null +++ b/src/Builder/Stage/UnionWithStage.php @@ -0,0 +1,63 @@ +coll = $coll; + if (is_array($pipeline) && ! array_is_list($pipeline)) { + throw new InvalidArgumentException('Expected $pipeline argument to be a list, got an associative array.'); + } + + $this->pipeline = $pipeline; + } + + public function getOperator(): string + { + return '$unionWith'; + } +} diff --git a/src/Builder/Stage/UnsetStage.php b/src/Builder/Stage/UnsetStage.php new file mode 100644 index 000000000..f75546ed0 --- /dev/null +++ b/src/Builder/Stage/UnsetStage.php @@ -0,0 +1,53 @@ + $field */ + public readonly array $field; + + /** + * @param FieldPath|string ...$field + * @no-named-arguments + */ + public function __construct(FieldPath|string ...$field) + { + if (\count($field) < 1) { + throw new InvalidArgumentException(\sprintf('Expected at least %d values for $field, got %d.', 1, \count($field))); + } + + if (! array_is_list($field)) { + throw new InvalidArgumentException('Expected $field arguments to be a list (array), named arguments are not supported'); + } + + $this->field = $field; + } + + public function getOperator(): string + { + return '$unset'; + } +} diff --git a/src/Builder/Stage/UnwindStage.php b/src/Builder/Stage/UnwindStage.php new file mode 100644 index 000000000..09c9f1e82 --- /dev/null +++ b/src/Builder/Stage/UnwindStage.php @@ -0,0 +1,60 @@ +path = $path; + $this->includeArrayIndex = $includeArrayIndex; + $this->preserveNullAndEmptyArrays = $preserveNullAndEmptyArrays; + } + + public function getOperator(): string + { + return '$unwind'; + } +} diff --git a/src/Builder/Type/AccumulatorInterface.php b/src/Builder/Type/AccumulatorInterface.php new file mode 100644 index 000000000..c3aaad2af --- /dev/null +++ b/src/Builder/Type/AccumulatorInterface.php @@ -0,0 +1,14 @@ + $fieldQueries */ + public readonly array $fieldQueries; + + /** @param list $fieldQueries */ + public function __construct(array $fieldQueries) + { + if (! array_is_list($fieldQueries)) { + throw new InvalidArgumentException('Expected filters to be a list, invalid array given.'); + } + + // Flatten nested CombinedFieldQuery + $this->fieldQueries = array_reduce( + $fieldQueries, + /** + * @param list $fieldQueries + * + * @return list + */ + static function (array $fieldQueries, QueryInterface|FieldQueryInterface|Type|stdClass|array|bool|float|int|string|null $fieldQuery): array { + if ($fieldQuery instanceof CombinedFieldQuery) { + return array_merge($fieldQueries, $fieldQuery->fieldQueries); + } + + $fieldQueries[] = $fieldQuery; + + return $fieldQueries; + }, + [], + ); + + // Validate FieldQuery types and non-duplicate operators + $seenOperators = []; + foreach ($this->fieldQueries as $fieldQuery) { + if ($fieldQuery instanceof stdClass) { + $fieldQuery = get_object_vars($fieldQuery); + } + + if ($fieldQuery instanceof FieldQueryInterface && $fieldQuery instanceof OperatorInterface) { + $operator = $fieldQuery->getOperator(); + } elseif (is_array($fieldQuery)) { + if (count($fieldQuery) !== 1) { + throw new InvalidArgumentException(sprintf('Operator must contain exactly one key, %d given', count($fieldQuery))); + } + + $operator = array_key_first($fieldQuery); + if (! is_string($operator) || ! str_starts_with($operator, '$')) { + throw new InvalidArgumentException(sprintf('Operator must contain exactly one key starting with $, "%s" given', $operator)); + } + } else { + throw new InvalidArgumentException(sprintf('Expected filters to be a list of field query operators, array or stdClass, %s given', get_debug_type($fieldQuery))); + } + + if (array_key_exists($operator, $seenOperators)) { + throw new InvalidArgumentException(sprintf('Duplicate operator "%s" detected', $operator)); + } + + $seenOperators[$operator] = true; + } + } +} diff --git a/src/Builder/Type/DictionaryInterface.php b/src/Builder/Type/DictionaryInterface.php new file mode 100644 index 000000000..5d88abffa --- /dev/null +++ b/src/Builder/Type/DictionaryInterface.php @@ -0,0 +1,10 @@ +|stdClass $operator Window operator to use in the $setWindowFields stage. + * @param Optional|array{string|int,string|int} $documents A window where the lower and upper boundaries are specified relative to the position of the current document read from the collection. + * @param Optional|array{string|numeric,string|numeric} $range Arguments passed to the init function. + * @param Optional|non-empty-string $unit Specifies the units for time range window boundaries. If omitted, default numeric range window boundaries are used. + */ + public function __construct( + Document|Serializable|WindowInterface|stdClass|array $operator, + Optional|array $documents = Optional::Undefined, + Optional|array $range = Optional::Undefined, + Optional|TimeUnit|string $unit = Optional::Undefined, + ) { + $this->operator = $operator; + + $window = null; + if ($documents !== Optional::Undefined) { + if (! array_is_list($documents) || count($documents) !== 2) { + throw new InvalidArgumentException('Expected $documents argument to be a list of 2 string or int'); + } + + if (! is_string($documents[0]) && ! is_int($documents[0]) || ! is_string($documents[1]) && ! is_int($documents[1])) { + throw new InvalidArgumentException(sprintf('Expected $documents argument to be a list of 2 string or int. Got [%s, %s]', get_debug_type($documents[0]), get_debug_type($documents[1]))); + } + + $window = new stdClass(); + $window->documents = $documents; + } + + if ($range !== Optional::Undefined) { + if (! array_is_list($range) || count($range) !== 2) { + throw new InvalidArgumentException('Expected $range argument to be a list of 2 string or numeric'); + } + + if (! is_string($range[0]) && ! is_numeric($range[0]) || ! is_string($range[1]) && ! is_numeric($range[1])) { + throw new InvalidArgumentException(sprintf('Expected $range argument to be a list of 2 string or numeric. Got [%s, %s]', get_debug_type($range[0]), get_debug_type($range[1]))); + } + + $window ??= new stdClass(); + $window->range = $range; + } + + if ($unit !== Optional::Undefined) { + $window ??= new stdClass(); + $window->unit = $unit; + } + + $this->window = $window ?? Optional::Undefined; + } +} diff --git a/src/Builder/Type/ProjectionInterface.php b/src/Builder/Type/ProjectionInterface.php new file mode 100644 index 000000000..e411dde60 --- /dev/null +++ b/src/Builder/Type/ProjectionInterface.php @@ -0,0 +1,14 @@ + $queries */ + public static function create(array $queries): QueryInterface + { + // We don't wrap a single query in a QueryObject + if (count($queries) === 1 && isset($queries[0]) && $queries[0] instanceof QueryInterface) { + return $queries[0]; + } + + return new self($queries); + } + + /** @param array $queriesOrArrayOfQueries */ + private function __construct(array $queriesOrArrayOfQueries) + { + // If the first element is an array and not an operator, we assume variadic arguments were not used + if ( + count($queriesOrArrayOfQueries) === 1 && + isset($queriesOrArrayOfQueries[0]) && + is_array($queriesOrArrayOfQueries[0]) && + count($queriesOrArrayOfQueries[0]) > 0 && + ! str_starts_with((string) array_key_first($queriesOrArrayOfQueries[0]), '$') + ) { + $queriesOrArrayOfQueries = $queriesOrArrayOfQueries[0]; + } + + $seenQueryOperators = []; + $queries = []; + + foreach ($queriesOrArrayOfQueries as $fieldPath => $query) { + if ($query instanceof QueryInterface) { + if ($query instanceof OperatorInterface) { + if (isset($seenQueryOperators[$query->getOperator()])) { + throw new InvalidArgumentException(sprintf('Query operator "%s" cannot be used multiple times in the same query.', $query->getOperator())); + } + + $seenQueryOperators[$query->getOperator()] = true; + } + + $queries[] = $query; + continue; + } + + // Convert list of filters into CombinedFieldQuery + if (self::isListOfFilters($query)) { + if (count($query) === 1) { + $query = $query[0]; + } else { + $query = new CombinedFieldQuery($query); + } + } + + $queries[$fieldPath] = $query; + } + + $this->queries = $queries; + } + + /** @psalm-assert-if-true list $values */ + private static function isListOfFilters(mixed $values): bool + { + if (! is_array($values) || ! array_is_list($values)) { + return false; + } + + /** @var mixed $value */ + foreach ($values as $value) { + if ($value instanceof FieldQueryInterface) { + return true; + } + } + + return false; + } +} diff --git a/src/Builder/Type/Sort.php b/src/Builder/Type/Sort.php new file mode 100644 index 000000000..5d1196311 --- /dev/null +++ b/src/Builder/Type/Sort.php @@ -0,0 +1,26 @@ + 1, + self::Desc => -1, + self::TextScore => ['$meta' => 'textScore'], + }; + } +} diff --git a/src/Builder/Type/StageInterface.php b/src/Builder/Type/StageInterface.php new file mode 100644 index 000000000..6d4fcf2e6 --- /dev/null +++ b/src/Builder/Type/StageInterface.php @@ -0,0 +1,14 @@ +value; + } +} diff --git a/src/Builder/Type/WindowInterface.php b/src/Builder/Type/WindowInterface.php new file mode 100644 index 000000000..d3fc8c378 --- /dev/null +++ b/src/Builder/Type/WindowInterface.php @@ -0,0 +1,14 @@ + is equivalent to $$CURRENT., rebinding + * CURRENT changes the meaning of $ accesses. + */ + public static function current(string $fieldPath = ''): ResolvesToAny + { + return new Expression\Variable('CURRENT' . ($fieldPath ? '.' . $fieldPath : '')); + } + + /** + * One of the allowed results of a $redact expression. + * + * $redact returns the fields at the current document level, excluding embedded documents. To include embedded + * documents and embedded documents within arrays, apply the $cond expression to the embedded documents to determine + * access for these embedded documents. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/redact/#mongodb-pipeline-pipe.-redact + */ + public static function descend(): ExpressionInterface + { + return new Expression\Variable('DESCEND'); + } + + /** + * One of the allowed results of a $redact expression. + * + * $redact returns or keeps all fields at this current document/embedded document level, without further inspection + * of the fields at this level. This applies even if the included field contains embedded documents that may have + * different access levels. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/redact/#mongodb-pipeline-pipe.-redact + */ + public static function keep(): ExpressionInterface + { + return new Expression\Variable('KEEP'); + } + + /** + * A variable that returns the current datetime value. + * NOW returns the same value for all members of the deployment and remains the same throughout all stages of the + * aggregation pipeline. + * + * New in MongoDB 4.2. + */ + public static function now(): ResolvesToDate + { + return new Expression\Variable('NOW'); + } + + /** + * One of the allowed results of a $redact expression. + * + * $redact excludes all fields at this current document/embedded document level, without further inspection of any + * of the excluded fields. This applies even if the excluded field contains embedded documents that may have + * different access levels. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/redact/#mongodb-pipeline-pipe.-redact + */ + public static function prune(): ExpressionInterface + { + return new Expression\Variable('PRUNE'); + } + + /** + * A variable which evaluates to the missing value. Allows for the conditional exclusion of fields. In a $project, + * a field set to the variable REMOVE is excluded from the output. + * Can be used with $cond operator for conditionally exclude fields. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/project/#std-label-remove-example + */ + public static function remove(): ResolvesToAny + { + return new Expression\Variable('REMOVE'); + } + + /** + * References the root document, i.e. the top-level document, currently being processed in the aggregation pipeline + * stage. + */ + public static function root(): ResolvesToObject + { + return new Expression\Variable('ROOT'); + } + + /** + * A variable that stores the metadata results of an Atlas Search query. In all supported aggregation pipeline + * stages, a field set to the variable $$SEARCH_META returns the metadata results for the query. + * For an example of its usage, see Atlas Search facet and count. + * + * @see https://www.mongodb.com/docs/atlas/atlas-search/query-syntax/#metadata-result-types + */ + public static function searchMeta(): ResolvesToObject + { + return new Expression\Variable('SEARCH_META'); + } + + /** + * Returns the roles assigned to the current user. + * For use cases that include USER_ROLES, see the find, aggregation, view, updateOne, updateMany, and findAndModify + * examples. + * + * New in MongoDB 7.0. + */ + public static function userRoles(): ResolvesToArray + { + return new Expression\Variable('USER_ROLES'); + } + + /** + * User-defined variable that can be used to store any BSON type. + * + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/let/ + */ + public static function variable(string $name): Expression\Variable + { + return new Expression\Variable($name); + } + + private function __construct() + { + // This class cannot be instantiated + } +} diff --git a/src/functions.php b/src/functions.php index bf3a736ec..ca30fdbc0 100644 --- a/src/functions.php +++ b/src/functions.php @@ -34,6 +34,7 @@ use Psr\Log\LoggerInterface; use ReflectionClass; use ReflectionException; +use stdClass; use function array_is_list; use function array_key_first; @@ -68,6 +69,22 @@ function remove_logger(LoggerInterface $logger): void PsrLogAdapter::removeLogger($logger); } +/** + * Create a new stdClass instance with the provided properties. + * Use named arguments to specify the property names. + * object( property1: value1, property2: value2 ) + * + * If property names contain a dot or a dollar characters, use array unpacking syntax. + * object( ...[ 'author.name' => 1, 'array.$' => 1 ] ) + * + * @psalm-suppress MoreSpecificReturnType + * @psalm-suppress LessSpecificReturnStatement + */ +function object(mixed ...$values): stdClass +{ + return (object) $values; +} + /** * Check whether all servers support executing a write stage on a secondary. * diff --git a/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php b/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php new file mode 100644 index 000000000..8371b7ff2 --- /dev/null +++ b/tests/Builder/Accumulator/AccumulatorAccumulatorTest.php @@ -0,0 +1,102 @@ +assertSamePipeline(Pipelines::AccumulatorUseAccumulatorToImplementTheAvgOperator, $pipeline); + } + + public function testUseInitArgsToVaryTheInitialStateByGroup(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: object(city: Expression::fieldPath('city')), + restaurants: Accumulator::accumulator( + init: <<<'JS' + function(city, userProfileCity) { + return { max: city === userProfileCity ? 3 : 1, restaurants: [] } + } + JS, + accumulate: <<<'JS' + function(state, restaurantName) { + if (state.restaurants.length < state.max) { + state.restaurants.push(restaurantName); + } + return state; + } + JS, + accumulateArgs: [Expression::fieldPath('name')], + merge: <<<'JS' + function(state1, state2) { + return { + max: state1.max, + restaurants: state1.restaurants.concat(state2.restaurants).slice(0, state1.max) + } + } + JS, + lang: 'js', + initArgs: [ + Expression::fieldPath('city'), + 'Bettles', + ], + finalize: <<<'JS' + function(state) { + return state.restaurants + } + JS, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::AccumulatorUseInitArgsToVaryTheInitialStateByGroup, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/AddToSetAccumulatorTest.php b/tests/Builder/Accumulator/AddToSetAccumulatorTest.php new file mode 100644 index 000000000..bd3b20912 --- /dev/null +++ b/tests/Builder/Accumulator/AddToSetAccumulatorTest.php @@ -0,0 +1,58 @@ +assertSamePipeline(Pipelines::AddToSetUseInGroupStage, $pipeline); + } + + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: Sort::Asc, + ), + output: object( + cakeTypesForState: Accumulator::outputWindow( + Accumulator::addToSet(Expression::fieldPath('type')), + documents: [ + 'unbounded', + 'current', + ], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::AddToSetUseInSetWindowFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/AvgAccumulatorTest.php b/tests/Builder/Accumulator/AvgAccumulatorTest.php new file mode 100644 index 000000000..78e37a905 --- /dev/null +++ b/tests/Builder/Accumulator/AvgAccumulatorTest.php @@ -0,0 +1,62 @@ +assertSamePipeline(Pipelines::AvgUseInGroupStage, $pipeline); + } + + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: Sort::Asc, + ), + output: object( + averageQuantityForState: Accumulator::outputWindow( + Accumulator::avg( + Expression::intFieldPath('quantity'), + ), + documents: ['unbounded', 'current'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::AvgUseInSetWindowFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/BottomAccumulatorTest.php b/tests/Builder/Accumulator/BottomAccumulatorTest.php new file mode 100644 index 000000000..674c60ddb --- /dev/null +++ b/tests/Builder/Accumulator/BottomAccumulatorTest.php @@ -0,0 +1,63 @@ +assertSamePipeline(Pipelines::BottomFindTheBottomScore, $pipeline); + } + + public function testFindingTheBottomScoreAcrossMultipleGames(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('gameId'), + playerId: Accumulator::bottom( + output: [ + Expression::fieldPath('playerId'), + Expression::fieldPath('score'), + ], + sortBy: object( + score: Sort::Desc, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::BottomFindingTheBottomScoreAcrossMultipleGames, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/BottomNAccumulatorTest.php b/tests/Builder/Accumulator/BottomNAccumulatorTest.php new file mode 100644 index 000000000..8ea89f669 --- /dev/null +++ b/tests/Builder/Accumulator/BottomNAccumulatorTest.php @@ -0,0 +1,92 @@ +assertSamePipeline(Pipelines::BottomNComputingNBasedOnTheGroupKeyForGroup, $pipeline); + } + + public function testFindTheThreeLowestScores(): void + { + $pipeline = new Pipeline( + Stage::match( + gameId: 'G1', + ), + Stage::group( + _id: Expression::fieldPath('gameId'), + playerId: Accumulator::bottomN( + output: [ + Expression::fieldPath('playerId'), + Expression::fieldPath('score'), + ], + sortBy: object( + score: Sort::Desc, + ), + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::BottomNFindTheThreeLowestScores, $pipeline); + } + + public function testFindingTheThreeLowestScoreDocumentsAcrossMultipleGames(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('gameId'), + playerId: Accumulator::bottomN( + output: [ + Expression::fieldPath('playerId'), + Expression::fieldPath('score'), + ], + sortBy: object( + score: Sort::Desc, + ), + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::BottomNFindingTheThreeLowestScoreDocumentsAcrossMultipleGames, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/CountAccumulatorTest.php b/tests/Builder/Accumulator/CountAccumulatorTest.php new file mode 100644 index 000000000..6c33d7293 --- /dev/null +++ b/tests/Builder/Accumulator/CountAccumulatorTest.php @@ -0,0 +1,52 @@ +assertSamePipeline(Pipelines::CountUseInGroupStage, $pipeline); + } + + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: Sort::Asc, + ), + output: object( + countNumberOfDocumentsForState: Accumulator::outputWindow( + Accumulator::count(), + documents: ['unbounded', 'current'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::CountUseInSetWindowFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/CovariancePopAccumulatorTest.php b/tests/Builder/Accumulator/CovariancePopAccumulatorTest.php new file mode 100644 index 000000000..516ecca95 --- /dev/null +++ b/tests/Builder/Accumulator/CovariancePopAccumulatorTest.php @@ -0,0 +1,45 @@ +assertSamePipeline(Pipelines::CovariancePopExample, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/CovarianceSampAccumulatorTest.php b/tests/Builder/Accumulator/CovarianceSampAccumulatorTest.php new file mode 100644 index 000000000..d2f176006 --- /dev/null +++ b/tests/Builder/Accumulator/CovarianceSampAccumulatorTest.php @@ -0,0 +1,45 @@ +assertSamePipeline(Pipelines::CovarianceSampExample, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/DenseRankAccumulatorTest.php b/tests/Builder/Accumulator/DenseRankAccumulatorTest.php new file mode 100644 index 000000000..7ba7b8a54 --- /dev/null +++ b/tests/Builder/Accumulator/DenseRankAccumulatorTest.php @@ -0,0 +1,57 @@ +assertSamePipeline(Pipelines::DenseRankDenseRankPartitionsByADateField, $pipeline); + } + + public function testDenseRankPartitionsByAnIntegerField(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::stringFieldPath('state'), + sortBy: object( + quantity: Sort::Desc, + ), + output: object( + // The outputWindow is optional when no window property is set. + denseRankQuantityForState: Accumulator::denseRank(), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::DenseRankDenseRankPartitionsByAnIntegerField, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/DerivativeAccumulatorTest.php b/tests/Builder/Accumulator/DerivativeAccumulatorTest.php new file mode 100644 index 000000000..7941c4b6a --- /dev/null +++ b/tests/Builder/Accumulator/DerivativeAccumulatorTest.php @@ -0,0 +1,49 @@ +assertSamePipeline(Pipelines::DerivativeExample, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/DocumentNumberAccumulatorTest.php b/tests/Builder/Accumulator/DocumentNumberAccumulatorTest.php new file mode 100644 index 000000000..fbc13c4cb --- /dev/null +++ b/tests/Builder/Accumulator/DocumentNumberAccumulatorTest.php @@ -0,0 +1,37 @@ +assertSamePipeline(Pipelines::DocumentNumberDocumentNumberForEachState, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/ExpMovingAvgAccumulatorTest.php b/tests/Builder/Accumulator/ExpMovingAvgAccumulatorTest.php new file mode 100644 index 000000000..59a6156d4 --- /dev/null +++ b/tests/Builder/Accumulator/ExpMovingAvgAccumulatorTest.php @@ -0,0 +1,60 @@ +assertSamePipeline(Pipelines::ExpMovingAvgExponentialMovingAverageUsingAlpha, $pipeline); + } + + public function testExponentialMovingAverageUsingN(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::stringFieldPath('stock'), + sortBy: object( + date: Sort::Asc, + ), + output: object( + expMovingAvgForStock: Accumulator::expMovingAvg( + input: Expression::numberFieldPath('price'), + N: 2, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ExpMovingAvgExponentialMovingAverageUsingN, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/FirstAccumulatorTest.php b/tests/Builder/Accumulator/FirstAccumulatorTest.php new file mode 100644 index 000000000..e78f9c53d --- /dev/null +++ b/tests/Builder/Accumulator/FirstAccumulatorTest.php @@ -0,0 +1,56 @@ +assertSamePipeline(Pipelines::FirstUseInGroupStage, $pipeline); + } + + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: Sort::Asc, + ), + output: object( + firstOrderTypeForState: Accumulator::outputWindow( + Accumulator::first(Expression::stringFieldPath('type')), + documents: ['unbounded', 'current'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FirstUseInSetWindowFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/FirstNAccumulatorTest.php b/tests/Builder/Accumulator/FirstNAccumulatorTest.php new file mode 100644 index 000000000..fe4f36384 --- /dev/null +++ b/tests/Builder/Accumulator/FirstNAccumulatorTest.php @@ -0,0 +1,126 @@ +assertSamePipeline(Pipelines::FirstNComputingNBasedOnTheGroupKeyForGroup, $pipeline); + } + + public function testFindTheFirstThreePlayerScoresForASingleGame(): void + { + $pipeline = new Pipeline( + Stage::match( + gameId: 'G1', + ), + Stage::group( + _id: Expression::fieldPath('gameId'), + firstThreeScores: Accumulator::firstN( + input: [ + Expression::fieldPath('playerId'), + Expression::fieldPath('score'), + ], + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FirstNFindTheFirstThreePlayerScoresForASingleGame, $pipeline); + } + + public function testFindingTheFirstThreePlayerScoresAcrossMultipleGames(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('gameId'), + playerId: Accumulator::firstN( + input: [ + Expression::fieldPath('playerId'), + Expression::fieldPath('score'), + ], + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FirstNFindingTheFirstThreePlayerScoresAcrossMultipleGames, $pipeline); + } + + public function testNullAndMissingValues(): void + { + $pipeline = new Pipeline( + Stage::documents([ + object(playerId: 'PlayerA', gameId: 'G1', score: 1), + object(playerId: 'PlayerB', gameId: 'G1', score: 2), + object(playerId: 'PlayerC', gameId: 'G1', score: 3), + object(playerId: 'PlayerD', gameId: 'G1'), + object(playerId: 'PlayerE', gameId: 'G1', score: null), + ]), + Stage::group( + _id: Expression::stringFieldPath('gameId'), + firstFiveScores: Accumulator::firstN( + input: Expression::fieldPath('score'), + n: 5, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FirstNNullAndMissingValues, $pipeline); + } + + public function testUsingSortWithFirstN(): void + { + $pipeline = new Pipeline( + Stage::sort( + score: Sort::Desc, + ), + Stage::group( + _id: Expression::fieldPath('gameId'), + playerId: Accumulator::firstN( + input: [ + Expression::fieldPath('playerId'), + Expression::fieldPath('score'), + ], + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FirstNUsingSortWithFirstN, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/IntegralAccumulatorTest.php b/tests/Builder/Accumulator/IntegralAccumulatorTest.php new file mode 100644 index 000000000..636456518 --- /dev/null +++ b/tests/Builder/Accumulator/IntegralAccumulatorTest.php @@ -0,0 +1,45 @@ +assertSamePipeline(Pipelines::IntegralExample, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/LastAccumulatorTest.php b/tests/Builder/Accumulator/LastAccumulatorTest.php new file mode 100644 index 000000000..e0439b662 --- /dev/null +++ b/tests/Builder/Accumulator/LastAccumulatorTest.php @@ -0,0 +1,56 @@ +assertSamePipeline(Pipelines::LastUseInGroupStage, $pipeline); + } + + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: Sort::Asc, + ), + output: object( + lastOrderTypeForState: Accumulator::outputWindow( + Accumulator::last(Expression::stringFieldPath('type')), + documents: ['current', 'unbounded'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::LastUseInSetWindowFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/LastNAccumulatorTest.php b/tests/Builder/Accumulator/LastNAccumulatorTest.php new file mode 100644 index 000000000..caf957fde --- /dev/null +++ b/tests/Builder/Accumulator/LastNAccumulatorTest.php @@ -0,0 +1,100 @@ + Expression::arrayFieldPath('gameId')], + gamescores: Accumulator::lastN( + input: Expression::arrayFieldPath('score'), + n: Expression::cond( + if: Expression::eq( + Expression::arrayFieldPath('gameId'), + 'G2', + ), + then: 1, + else: 3, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::LastNComputingNBasedOnTheGroupKeyForGroup, $pipeline); + } + + public function testFindTheLastThreePlayerScoresForASingleGame(): void + { + $pipeline = new Pipeline( + Stage::match( + gameId: 'G1', + ), + Stage::group( + _id: Expression::fieldPath('gameId'), + lastThreeScores: Accumulator::lastN( + input: [ + Expression::arrayFieldPath('playerId'), + Expression::arrayFieldPath('score'), + ], + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::LastNFindTheLastThreePlayerScoresForASingleGame, $pipeline); + } + + public function testFindingTheLastThreePlayerScoresAcrossMultipleGames(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('gameId'), + playerId: Accumulator::lastN( + input: [ + Expression::arrayFieldPath('playerId'), + Expression::arrayFieldPath('score'), + ], + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::LastNFindingTheLastThreePlayerScoresAcrossMultipleGames, $pipeline); + } + + public function testUsingSortWithLastN(): void + { + $pipeline = new Pipeline( + Stage::sort( + score: Sort::Desc, + ), + Stage::group( + _id: Expression::fieldPath('gameId'), + playerId: Accumulator::lastN( + input: [ + Expression::arrayFieldPath('playerId'), + Expression::arrayFieldPath('score'), + ], + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::LastNUsingSortWithLastN, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/LinearFillAccumulatorTest.php b/tests/Builder/Accumulator/LinearFillAccumulatorTest.php new file mode 100644 index 000000000..0142d0d68 --- /dev/null +++ b/tests/Builder/Accumulator/LinearFillAccumulatorTest.php @@ -0,0 +1,59 @@ +assertSamePipeline(Pipelines::LinearFillFillMissingValuesWithLinearInterpolation, $pipeline); + } + + public function testUseMultipleFillMethodsInASingleStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + sortBy: object( + time: Sort::Asc, + ), + output: object( + linearFillPrice: Accumulator::linearFill( + Expression::numberFieldPath('price'), + ), + locfPrice: Accumulator::locf( + Expression::numberFieldPath('price'), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::LinearFillUseMultipleFillMethodsInASingleStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/LocfAccumulatorTest.php b/tests/Builder/Accumulator/LocfAccumulatorTest.php new file mode 100644 index 000000000..259a92aff --- /dev/null +++ b/tests/Builder/Accumulator/LocfAccumulatorTest.php @@ -0,0 +1,38 @@ +assertSamePipeline(Pipelines::LocfFillMissingValuesWithTheLastObservedValue, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/MaxAccumulatorTest.php b/tests/Builder/Accumulator/MaxAccumulatorTest.php new file mode 100644 index 000000000..6a75d85c9 --- /dev/null +++ b/tests/Builder/Accumulator/MaxAccumulatorTest.php @@ -0,0 +1,62 @@ +assertSamePipeline(Pipelines::MaxUseInGroupStage, $pipeline); + } + + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: Sort::Asc, + ), + output: object( + maximumQuantityForState: Accumulator::outputWindow( + Accumulator::max( + Expression::intFieldPath('quantity'), + ), + documents: ['unbounded', 'current'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::MaxUseInSetWindowFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/MaxNAccumulatorTest.php b/tests/Builder/Accumulator/MaxNAccumulatorTest.php new file mode 100644 index 000000000..19617834c --- /dev/null +++ b/tests/Builder/Accumulator/MaxNAccumulatorTest.php @@ -0,0 +1,85 @@ +assertSamePipeline(Pipelines::MaxNComputingNBasedOnTheGroupKeyForGroup, $pipeline); + } + + public function testFindTheMaximumThreeScoresForASingleGame(): void + { + $pipeline = new Pipeline( + Stage::match( + gameId: 'G1', + ), + Stage::group( + _id: Expression::fieldPath('gameId'), + maxThreeScores: Accumulator::maxN( + input: [ + Expression::fieldPath('score'), + Expression::fieldPath('playerId'), + ], + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::MaxNFindTheMaximumThreeScoresForASingleGame, $pipeline); + } + + public function testFindingTheMaximumThreeScoresAcrossMultipleGames(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('gameId'), + maxScores: Accumulator::maxN( + input: [ + Expression::fieldPath('score'), + Expression::fieldPath('playerId'), + ], + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::MaxNFindingTheMaximumThreeScoresAcrossMultipleGames, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/MedianAccumulatorTest.php b/tests/Builder/Accumulator/MedianAccumulatorTest.php new file mode 100644 index 000000000..c1f453347 --- /dev/null +++ b/tests/Builder/Accumulator/MedianAccumulatorTest.php @@ -0,0 +1,62 @@ +assertSamePipeline(Pipelines::MedianUseMedianAsAnAccumulator, $pipeline); + } + + public function testUseMedianInASetWindowFieldStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + sortBy: object( + test01: Sort::Asc, + ), + output: object( + test01_median: Accumulator::outputWindow( + Accumulator::median( + input: Expression::intFieldPath('test01'), + method: 'approximate', + ), + range: [-3, 3], + ), + ), + ), + Stage::project( + _id: 0, + studentId: 1, + test01_median: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::MedianUseMedianInASetWindowFieldStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/MergeObjectsAccumulatorTest.php b/tests/Builder/Accumulator/MergeObjectsAccumulatorTest.php new file mode 100644 index 000000000..62fabc4f1 --- /dev/null +++ b/tests/Builder/Accumulator/MergeObjectsAccumulatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::MergeObjectsMergeObjectsAsAnAccumulator, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/MinAccumulatorTest.php b/tests/Builder/Accumulator/MinAccumulatorTest.php new file mode 100644 index 000000000..442a643a3 --- /dev/null +++ b/tests/Builder/Accumulator/MinAccumulatorTest.php @@ -0,0 +1,56 @@ +assertSamePipeline(Pipelines::MinUseInGroupStage, $pipeline); + } + + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: Sort::Asc, + ), + output: object( + minimumQuantityForState: Accumulator::outputWindow( + Accumulator::min( + Expression::intFieldPath('quantity'), + ), + documents: ['unbounded', 'current'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::MinUseInSetWindowFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/MinNAccumulatorTest.php b/tests/Builder/Accumulator/MinNAccumulatorTest.php new file mode 100644 index 000000000..c44c1f6d2 --- /dev/null +++ b/tests/Builder/Accumulator/MinNAccumulatorTest.php @@ -0,0 +1,85 @@ +assertSamePipeline(Pipelines::MinNComputingNBasedOnTheGroupKeyForGroup, $pipeline); + } + + public function testFindTheMinimumThreeScoresForASingleGame(): void + { + $pipeline = new Pipeline( + Stage::match( + gameId: 'G1', + ), + Stage::group( + _id: Expression::fieldPath('gameId'), + minScores: Accumulator::minN( + input: [ + Expression::fieldPath('score'), + Expression::fieldPath('playerId'), + ], + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::MinNFindTheMinimumThreeScoresForASingleGame, $pipeline); + } + + public function testFindingTheMinimumThreeDocumentsAcrossMultipleGames(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('gameId'), + minScores: Accumulator::minN( + input: [ + Expression::fieldPath('score'), + Expression::fieldPath('playerId'), + ], + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::MinNFindingTheMinimumThreeDocumentsAcrossMultipleGames, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/PercentileAccumulatorTest.php b/tests/Builder/Accumulator/PercentileAccumulatorTest.php new file mode 100644 index 000000000..a3fc1708d --- /dev/null +++ b/tests/Builder/Accumulator/PercentileAccumulatorTest.php @@ -0,0 +1,95 @@ +assertSamePipeline(Pipelines::PercentileCalculateASingleValueAsAnAccumulator, $pipeline); + } + + public function testCalculateMultipleValuesAsAnAccumulator(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: null, + test01_percentiles: Accumulator::percentile( + input: Expression::numberFieldPath('test01'), + p: [0.5, 0.75, 0.9, 0.95], + method: 'approximate', + ), + test02_percentiles: Accumulator::percentile( + input: Expression::numberFieldPath('test02'), + p: [0.5, 0.75, 0.9, 0.95], + method: 'approximate', + ), + test03_percentiles: Accumulator::percentile( + input: Expression::numberFieldPath('test03'), + p: [0.5, 0.75, 0.9, 0.95], + method: 'approximate', + ), + test03_percent_alt: Accumulator::percentile( + input: Expression::numberFieldPath('test03'), + p: [0.9, 0.5, 0.75, 0.95], + method: 'approximate', + ), + ), + ); + + $this->assertSamePipeline(Pipelines::PercentileCalculateMultipleValuesAsAnAccumulator, $pipeline); + } + + public function testUsePercentileInASetWindowFieldStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + sortBy: object( + test01: Sort::Asc, + ), + output: object( + test01_95percentile: Accumulator::outputWindow( + Accumulator::percentile( + input: Expression::numberFieldPath('test01'), + p: [0.95], + method: 'approximate', + ), + range: [-3, 3], + ), + ), + ), + Stage::project( + _id: 0, + studentId: 1, + test01_95percentile: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::PercentileUsePercentileInASetWindowFieldStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/Pipelines.php b/tests/Builder/Accumulator/Pipelines.php new file mode 100644 index 000000000..fedc91ab4 --- /dev/null +++ b/tests/Builder/Accumulator/Pipelines.php @@ -0,0 +1,2323 @@ +assertSamePipeline(Pipelines::PushUseInGroupStage, $pipeline); + } + + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: Sort::Asc, + ), + output: object( + quantitiesForState: Accumulator::outputWindow( + Accumulator::push( + Expression::numberFieldPath('quantity'), + ), + documents: ['unbounded', 'current'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::PushUseInSetWindowFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/RankAccumulatorTest.php b/tests/Builder/Accumulator/RankAccumulatorTest.php new file mode 100644 index 000000000..3be7e63aa --- /dev/null +++ b/tests/Builder/Accumulator/RankAccumulatorTest.php @@ -0,0 +1,54 @@ +assertSamePipeline(Pipelines::RankRankPartitionsByADateField, $pipeline); + } + + public function testRankPartitionsByAnIntegerField(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::stringFieldPath('state'), + sortBy: object( + quantity: Sort::Desc, + ), + output: object( + rankQuantityForState: Accumulator::rank(), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::RankRankPartitionsByAnIntegerField, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/ShiftAccumulatorTest.php b/tests/Builder/Accumulator/ShiftAccumulatorTest.php new file mode 100644 index 000000000..7d8c58856 --- /dev/null +++ b/tests/Builder/Accumulator/ShiftAccumulatorTest.php @@ -0,0 +1,62 @@ +assertSamePipeline(Pipelines::ShiftShiftUsingANegativeInteger, $pipeline); + } + + public function testShiftUsingAPositiveInteger(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::stringFieldPath('state'), + sortBy: object( + quantity: Sort::Desc, + ), + output: object( + shiftQuantityForState: Accumulator::shift( + output: Expression::fieldPath('quantity'), + by: 1, + default: 'Not available', + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ShiftShiftUsingAPositiveInteger, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/StdDevPopAccumulatorTest.php b/tests/Builder/Accumulator/StdDevPopAccumulatorTest.php new file mode 100644 index 000000000..7d38829e4 --- /dev/null +++ b/tests/Builder/Accumulator/StdDevPopAccumulatorTest.php @@ -0,0 +1,56 @@ +assertSamePipeline(Pipelines::StdDevPopUseInGroupStage, $pipeline); + } + + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: Sort::Asc, + ), + output: object( + stdDevPopQuantityForState: Accumulator::outputWindow( + Accumulator::stdDevPop( + Expression::numberFieldPath('quantity'), + ), + documents: ['unbounded', 'current'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::StdDevPopUseInSetWindowFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/StdDevSampAccumulatorTest.php b/tests/Builder/Accumulator/StdDevSampAccumulatorTest.php new file mode 100644 index 000000000..c1f49e00a --- /dev/null +++ b/tests/Builder/Accumulator/StdDevSampAccumulatorTest.php @@ -0,0 +1,57 @@ +assertSamePipeline(Pipelines::StdDevSampUseInGroupStage, $pipeline); + } + + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: Sort::Asc, + ), + output: object( + stdDevSampQuantityForState: Accumulator::outputWindow( + Accumulator::stdDevSamp( + Expression::numberFieldPath('quantity'), + ), + documents: ['unbounded', 'current'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::StdDevSampUseInSetWindowFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/SumAccumulatorTest.php b/tests/Builder/Accumulator/SumAccumulatorTest.php new file mode 100644 index 000000000..21be6da72 --- /dev/null +++ b/tests/Builder/Accumulator/SumAccumulatorTest.php @@ -0,0 +1,67 @@ +assertSamePipeline(Pipelines::SumUseInGroupStage, $pipeline); + } + + public function testUseInSetWindowFieldsStage(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::fieldPath('state'), + sortBy: object( + orderDate: Sort::Asc, + ), + output: object( + sumQuantityForState: Accumulator::outputWindow( + Accumulator::sum( + Expression::intFieldPath('quantity'), + ), + documents: ['unbounded', 'current'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SumUseInSetWindowFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/TopAccumulatorTest.php b/tests/Builder/Accumulator/TopAccumulatorTest.php new file mode 100644 index 000000000..c6d32edb5 --- /dev/null +++ b/tests/Builder/Accumulator/TopAccumulatorTest.php @@ -0,0 +1,63 @@ +assertSamePipeline(Pipelines::TopFindTheTopScore, $pipeline); + } + + public function testFindTheTopScoreAcrossMultipleGames(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('gameId'), + playerId: Accumulator::top( + output: [ + Expression::fieldPath('playerId'), + Expression::fieldPath('score'), + ], + sortBy: object( + score: Sort::Desc, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::TopFindTheTopScoreAcrossMultipleGames, $pipeline); + } +} diff --git a/tests/Builder/Accumulator/TopNAccumulatorTest.php b/tests/Builder/Accumulator/TopNAccumulatorTest.php new file mode 100644 index 000000000..6d14a9c6f --- /dev/null +++ b/tests/Builder/Accumulator/TopNAccumulatorTest.php @@ -0,0 +1,92 @@ +assertSamePipeline(Pipelines::TopNComputingNBasedOnTheGroupKeyForGroup, $pipeline); + } + + public function testFindTheThreeHighestScores(): void + { + $pipeline = new Pipeline( + Stage::match( + gameId: 'G1', + ), + Stage::group( + _id: Expression::fieldPath('gameId'), + playerId: Accumulator::topN( + output: [ + Expression::fieldPath('playerId'), + Expression::fieldPath('score'), + ], + sortBy: object( + score: Sort::Desc, + ), + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::TopNFindTheThreeHighestScores, $pipeline); + } + + public function testFindingTheThreeHighestScoreDocumentsAcrossMultipleGames(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('gameId'), + playerId: Accumulator::topN( + output: [ + Expression::fieldPath('playerId'), + Expression::fieldPath('score'), + ], + sortBy: object( + score: Sort::Desc, + ), + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::TopNFindingTheThreeHighestScoreDocumentsAcrossMultipleGames, $pipeline); + } +} diff --git a/tests/Builder/BuilderEncoderTest.php b/tests/Builder/BuilderEncoderTest.php new file mode 100644 index 000000000..9d3292bb2 --- /dev/null +++ b/tests/Builder/BuilderEncoderTest.php @@ -0,0 +1,353 @@ + ['author' => 'dave']], + ['$limit' => 1], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + public function testMatchNumericFieldName(): void + { + $pipeline = new Pipeline( + Stage::match(['1' => Query::eq('dave')]), + Stage::match(['1' => Query::not(Query::eq('dave'))]), + Stage::match( + Query::and( + Query::query(['2' => Query::gt(3)]), + Query::query(['2' => Query::lt(4)]), + ), + ), + Stage::match( + Query::or( + Query::query(['2' => Query::gt(3)]), + Query::query(['2' => Query::lt(4)]), + ), + ), + Stage::match( + Query::nor( + Query::query(['2' => Query::gt(3)]), + Query::query(['2' => Query::lt(4)]), + ), + ), + ); + + $expected = [ + ['$match' => ['1' => ['$eq' => 'dave']]], + ['$match' => ['1' => ['$not' => ['$eq' => 'dave']]]], + [ + '$match' => [ + '$and' => [ + ['2' => ['$gt' => 3]], + ['2' => ['$lt' => 4]], + ], + ], + ], + [ + '$match' => [ + '$or' => [ + ['2' => ['$gt' => 3]], + ['2' => ['$lt' => 4]], + ], + ], + ], + [ + '$match' => [ + '$nor' => [ + ['2' => ['$gt' => 3]], + ['2' => ['$lt' => 4]], + ], + ], + ], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/sort/#ascending-descending-sort */ + public function testSort(): void + { + $pipeline = new Pipeline( + Stage::sort( + age: Sort::Desc, + posts: Sort::Asc, + ), + ); + + $expected = [ + ['$sort' => ['age' => -1, 'posts' => 1]], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/match/#perform-a-count */ + public function testPerformCount(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::or( + Query::query(score: [Query::gt(70), Query::lt(90)]), + Query::query(views: Query::gte(1000)), + ), + ), + Stage::group( + _id: null, + count: Accumulator::sum(1), + ), + ); + + $expected = [ + [ + '$match' => [ + '$or' => [ + ['score' => ['$gt' => 70, '$lt' => 90]], + ['views' => ['$gte' => 1000]], + ], + ], + ], + [ + '$group' => [ + '_id' => null, + 'count' => ['$sum' => 1], + ], + ], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + /** + * @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/filter/#examples + * + * @param list $limit + * @param array $expectedLimit + * + * @dataProvider provideExpressionFilterLimit + */ + public function testExpressionFilter(array $limit, array $expectedLimit): void + { + $pipeline = new Pipeline( + Stage::project( + items: Expression::filter( + ...$limit, + input: Expression::arrayFieldPath('items'), + cond: Expression::gte(Expression::variable('item.price'), 100), + as:'item', + ), + ), + ); + + $expected = [ + [ + '$project' => [ + 'items' => [ + '$filter' => array_merge([ + 'input' => '$items', + 'as' => 'item', + 'cond' => ['$gte' => ['$$item.price', 100]], + ], $expectedLimit), + ], + ], + ], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + public static function provideExpressionFilterLimit(): Generator + { + yield 'unspecified limit' => [ + [], + [], + ]; + + yield 'int limit' => [ + ['limit' => 1], + ['limit' => 1], + ]; + } + + /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/slice/#example */ + public function testSlice(): void + { + $pipeline = new Pipeline( + Stage::project( + name: 1, + threeFavorites: Expression::slice( + Expression::arrayFieldPath('items'), + n: 3, + ), + ), + ); + + $expected = [ + [ + '$project' => [ + 'name' => 1, + 'threeFavorites' => [ + '$slice' => ['$items', 3], + ], + ], + ], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/setWindowFields/#use-documents-window-to-obtain-cumulative-and-maximum-quantity-for-each-year */ + public function testSetWindowFields(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::year(Expression::dateFieldPath('orderDate')), + sortBy: object(orderDate: Sort::Asc), + output: object( + cumulativeQuantityForYear: Accumulator::outputWindow( + Accumulator::sum(Expression::intFieldPath('quantity')), + documents: ['unbounded', 'current'], + ), + maximumQuantityForYear: Accumulator::outputWindow( + Accumulator::max(Expression::intFieldPath('quantity')), + documents: ['unbounded', 'unbounded'], + ), + ), + ), + ); + + $expected = [ + [ + '$setWindowFields' => [ + // "date" key is optional for $year, but we always add it for consistency + 'partitionBy' => ['$year' => ['date' => '$orderDate']], + 'sortBy' => ['orderDate' => 1], + 'output' => [ + 'cumulativeQuantityForYear' => [ + '$sum' => '$quantity', + 'window' => ['documents' => ['unbounded', 'current']], + ], + 'maximumQuantityForYear' => [ + '$max' => '$quantity', + 'window' => ['documents' => ['unbounded', 'unbounded']], + ], + ], + ], + ], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + public function testUnionWith(): void + { + $pipeline = new Pipeline( + Stage::unionWith( + coll: 'orders', + pipeline: new Pipeline( + Stage::match(status: 'A'), + Stage::project( + item: 1, + status: 1, + ), + ), + ), + ); + + $expected = [ + [ + '$unionWith' => [ + 'coll' => 'orders', + 'pipeline' => [ + [ + '$match' => ['status' => 'A'], + ], + [ + '$project' => [ + 'item' => 1, + 'status' => 1, + ], + ], + ], + ], + ], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + /** @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/redact/ */ + public function testRedactStage(): void + { + $pipeline = new Pipeline( + Stage::match(status: 'A'), + Stage::redact( + Expression::cond( + if: Expression::eq(Expression::fieldPath('level'), 5), + then: Variable::prune(), + else: Variable::descend(), + ), + ), + ); + $expected = [ + [ + '$match' => ['status' => 'A'], + ], + [ + '$redact' => [ + '$cond' => [ + 'if' => ['$eq' => ['$level', 5]], + 'then' => '$$PRUNE', + 'else' => '$$DESCEND', + ], + ], + ], + ]; + + $this->assertSamePipeline($expected, $pipeline); + } + + /** @param list> $expected */ + private static function assertSamePipeline(array $expected, Pipeline $pipeline): void + { + $codec = new BuilderEncoder(); + $actual = $codec->encode($pipeline); + + // Normalize with BSON round-trip + // BSON Documents doesn't support top-level arrays. + $actual = Document::fromPHP(['root' => $actual])->toCanonicalExtendedJSON(); + $expected = Document::fromPHP(['root' => $expected])->toCanonicalExtendedJSON(); + + self::assertJsonStringEqualsJsonString($expected, $actual, var_export($actual, true)); + } +} diff --git a/tests/Builder/Expression/AbsOperatorTest.php b/tests/Builder/Expression/AbsOperatorTest.php new file mode 100644 index 000000000..829c9c551 --- /dev/null +++ b/tests/Builder/Expression/AbsOperatorTest.php @@ -0,0 +1,32 @@ +assertSamePipeline(Pipelines::AbsExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/AcosOperatorTest.php b/tests/Builder/Expression/AcosOperatorTest.php new file mode 100644 index 000000000..21b4f4ff0 --- /dev/null +++ b/tests/Builder/Expression/AcosOperatorTest.php @@ -0,0 +1,34 @@ +assertSamePipeline(Pipelines::AcosExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/AcoshOperatorTest.php b/tests/Builder/Expression/AcoshOperatorTest.php new file mode 100644 index 000000000..4ba41cf59 --- /dev/null +++ b/tests/Builder/Expression/AcoshOperatorTest.php @@ -0,0 +1,33 @@ + Expression::radiansToDegrees( + Expression::acosh( + Expression::numberFieldPath('x-coordinate'), + ), + ), + ], + ), + ); + + $this->assertSamePipeline(Pipelines::AcoshExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/AddOperatorTest.php b/tests/Builder/Expression/AddOperatorTest.php new file mode 100644 index 000000000..b6fd43935 --- /dev/null +++ b/tests/Builder/Expression/AddOperatorTest.php @@ -0,0 +1,46 @@ +assertSamePipeline(Pipelines::AddAddNumbers, $pipeline); + } + + public function testPerformAdditionOnADate(): void + { + $pipeline = new Pipeline( + Stage::project( + item: 1, + billing_date: Expression::add( + Expression::fieldPath('date'), + 3 * 24 * 60 * 60000, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::AddPerformAdditionOnADate, $pipeline); + } +} diff --git a/tests/Builder/Expression/AllElementsTrueOperatorTest.php b/tests/Builder/Expression/AllElementsTrueOperatorTest.php new file mode 100644 index 000000000..b3591dc3f --- /dev/null +++ b/tests/Builder/Expression/AllElementsTrueOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::AllElementsTrueExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/AndOperatorTest.php b/tests/Builder/Expression/AndOperatorTest.php new file mode 100644 index 000000000..3956d29e0 --- /dev/null +++ b/tests/Builder/Expression/AndOperatorTest.php @@ -0,0 +1,38 @@ +assertSamePipeline(Pipelines::AndExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/AnyElementTrueOperatorTest.php b/tests/Builder/Expression/AnyElementTrueOperatorTest.php new file mode 100644 index 000000000..a99fd1654 --- /dev/null +++ b/tests/Builder/Expression/AnyElementTrueOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::AnyElementTrueExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ArrayElemAtOperatorTest.php b/tests/Builder/Expression/ArrayElemAtOperatorTest.php new file mode 100644 index 000000000..0dbe768ec --- /dev/null +++ b/tests/Builder/Expression/ArrayElemAtOperatorTest.php @@ -0,0 +1,35 @@ +assertSamePipeline(Pipelines::ArrayElemAtExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ArrayToObjectOperatorTest.php b/tests/Builder/Expression/ArrayToObjectOperatorTest.php new file mode 100644 index 000000000..c18d7bdcb --- /dev/null +++ b/tests/Builder/Expression/ArrayToObjectOperatorTest.php @@ -0,0 +1,60 @@ +assertSamePipeline(Pipelines::ArrayToObjectArrayToObjectExample, $pipeline); + } + + public function testObjectToArrayAndArrayToObjectExample(): void + { + $pipeline = new Pipeline( + Stage::addFields( + instock: Expression::objectToArray( + Expression::objectFieldPath('instock'), + ), + ), + Stage::addFields( + instock: Expression::concatArrays( + Expression::arrayFieldPath('instock'), + [ + object(k: 'total', v: Expression::sum( + Expression::fieldPath('instock.v'), + )), + ], + ), + ), + Stage::addFields( + instock: Expression::arrayToObject( + Expression::arrayFieldPath('instock'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ArrayToObjectObjectToArrayAndArrayToObjectExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/AsinOperatorTest.php b/tests/Builder/Expression/AsinOperatorTest.php new file mode 100644 index 000000000..c74dc75c7 --- /dev/null +++ b/tests/Builder/Expression/AsinOperatorTest.php @@ -0,0 +1,34 @@ +assertSamePipeline(Pipelines::AsinExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/AsinhOperatorTest.php b/tests/Builder/Expression/AsinhOperatorTest.php new file mode 100644 index 000000000..138030e54 --- /dev/null +++ b/tests/Builder/Expression/AsinhOperatorTest.php @@ -0,0 +1,33 @@ + Expression::radiansToDegrees( + Expression::asinh( + Expression::numberFieldPath('x-coordinate'), + ), + ), + ], + ), + ); + + $this->assertSamePipeline(Pipelines::AsinhExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/Atan2OperatorTest.php b/tests/Builder/Expression/Atan2OperatorTest.php new file mode 100644 index 000000000..442e0150f --- /dev/null +++ b/tests/Builder/Expression/Atan2OperatorTest.php @@ -0,0 +1,32 @@ +assertSamePipeline(Pipelines::Atan2Example, $pipeline); + } +} diff --git a/tests/Builder/Expression/AtanOperatorTest.php b/tests/Builder/Expression/AtanOperatorTest.php new file mode 100644 index 000000000..3040823b1 --- /dev/null +++ b/tests/Builder/Expression/AtanOperatorTest.php @@ -0,0 +1,34 @@ +assertSamePipeline(Pipelines::AtanExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/AtanhOperatorTest.php b/tests/Builder/Expression/AtanhOperatorTest.php new file mode 100644 index 000000000..3194a8e94 --- /dev/null +++ b/tests/Builder/Expression/AtanhOperatorTest.php @@ -0,0 +1,33 @@ + Expression::radiansToDegrees( + Expression::atanh( + Expression::numberFieldPath('x-coordinate'), + ), + ), + ], + ), + ); + + $this->assertSamePipeline(Pipelines::AtanhExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/AvgOperatorTest.php b/tests/Builder/Expression/AvgOperatorTest.php new file mode 100644 index 000000000..595c49f1b --- /dev/null +++ b/tests/Builder/Expression/AvgOperatorTest.php @@ -0,0 +1,36 @@ +assertSamePipeline(Pipelines::AvgUseInProjectStage, $pipeline); + } +} diff --git a/tests/Builder/Expression/BinarySizeOperatorTest.php b/tests/Builder/Expression/BinarySizeOperatorTest.php new file mode 100644 index 000000000..8b0653315 --- /dev/null +++ b/tests/Builder/Expression/BinarySizeOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::BinarySizeExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/BitAndOperatorTest.php b/tests/Builder/Expression/BitAndOperatorTest.php new file mode 100644 index 000000000..e46fbb55a --- /dev/null +++ b/tests/Builder/Expression/BitAndOperatorTest.php @@ -0,0 +1,45 @@ +assertSamePipeline(Pipelines::BitAndBitwiseANDWithALongAndInteger, $pipeline); + } + + public function testBitwiseANDWithTwoIntegers(): void + { + $pipeline = new Pipeline( + Stage::project( + result: Expression::bitAnd( + Expression::intFieldPath('a'), + Expression::intFieldPath('b'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::BitAndBitwiseANDWithTwoIntegers, $pipeline); + } +} diff --git a/tests/Builder/Expression/BitNotOperatorTest.php b/tests/Builder/Expression/BitNotOperatorTest.php new file mode 100644 index 000000000..9407a2861 --- /dev/null +++ b/tests/Builder/Expression/BitNotOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::BitNotExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/BitOrOperatorTest.php b/tests/Builder/Expression/BitOrOperatorTest.php new file mode 100644 index 000000000..2558432e9 --- /dev/null +++ b/tests/Builder/Expression/BitOrOperatorTest.php @@ -0,0 +1,45 @@ +assertSamePipeline(Pipelines::BitOrBitwiseORWithALongAndInteger, $pipeline); + } + + public function testBitwiseORWithTwoIntegers(): void + { + $pipeline = new Pipeline( + Stage::project( + result: Expression::bitOr( + Expression::intFieldPath('a'), + Expression::intFieldPath('b'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::BitOrBitwiseORWithTwoIntegers, $pipeline); + } +} diff --git a/tests/Builder/Expression/BitXorOperatorTest.php b/tests/Builder/Expression/BitXorOperatorTest.php new file mode 100644 index 000000000..1fffe2c18 --- /dev/null +++ b/tests/Builder/Expression/BitXorOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::BitXorExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/BsonSizeOperatorTest.php b/tests/Builder/Expression/BsonSizeOperatorTest.php new file mode 100644 index 000000000..49707b120 --- /dev/null +++ b/tests/Builder/Expression/BsonSizeOperatorTest.php @@ -0,0 +1,66 @@ +assertSamePipeline(Pipelines::BsonSizeReturnCombinedSizeOfAllDocumentsInACollection, $pipeline); + } + + public function testReturnDocumentWithLargestSpecifiedField(): void + { + $pipeline = new Pipeline( + Stage::project( + name: Expression::stringFieldPath('name'), + task_object_size: Expression::bsonSize( + Expression::objectFieldPath('current_task'), + ), + ), + Stage::sort( + task_object_size: Sort::Desc, + ), + Stage::limit(1), + ); + + $this->assertSamePipeline(Pipelines::BsonSizeReturnDocumentWithLargestSpecifiedField, $pipeline); + } + + public function testReturnSizesOfDocuments(): void + { + $pipeline = new Pipeline( + Stage::project( + name: 1, + object_size: Expression::bsonSize( + Expression::variable('ROOT'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::BsonSizeReturnSizesOfDocuments, $pipeline); + } +} diff --git a/tests/Builder/Expression/CeilOperatorTest.php b/tests/Builder/Expression/CeilOperatorTest.php new file mode 100644 index 000000000..6d78af6a3 --- /dev/null +++ b/tests/Builder/Expression/CeilOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::CeilExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/CmpOperatorTest.php b/tests/Builder/Expression/CmpOperatorTest.php new file mode 100644 index 000000000..e7bb5bc09 --- /dev/null +++ b/tests/Builder/Expression/CmpOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::CmpExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ConcatArraysOperatorTest.php b/tests/Builder/Expression/ConcatArraysOperatorTest.php new file mode 100644 index 000000000..ba7dffb4c --- /dev/null +++ b/tests/Builder/Expression/ConcatArraysOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::ConcatArraysExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ConcatOperatorTest.php b/tests/Builder/Expression/ConcatOperatorTest.php new file mode 100644 index 000000000..d96f17685 --- /dev/null +++ b/tests/Builder/Expression/ConcatOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::ConcatExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/CondOperatorTest.php b/tests/Builder/Expression/CondOperatorTest.php new file mode 100644 index 000000000..725ab1c50 --- /dev/null +++ b/tests/Builder/Expression/CondOperatorTest.php @@ -0,0 +1,35 @@ +assertSamePipeline(Pipelines::CondExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ConvertOperatorTest.php b/tests/Builder/Expression/ConvertOperatorTest.php new file mode 100644 index 000000000..ab559c38a --- /dev/null +++ b/tests/Builder/Expression/ConvertOperatorTest.php @@ -0,0 +1,73 @@ +assertSamePipeline(Pipelines::ConvertExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/CosOperatorTest.php b/tests/Builder/Expression/CosOperatorTest.php new file mode 100644 index 000000000..0124b88c1 --- /dev/null +++ b/tests/Builder/Expression/CosOperatorTest.php @@ -0,0 +1,34 @@ +assertSamePipeline(Pipelines::CosExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/CoshOperatorTest.php b/tests/Builder/Expression/CoshOperatorTest.php new file mode 100644 index 000000000..37ec83724 --- /dev/null +++ b/tests/Builder/Expression/CoshOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::CoshExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/DateAddOperatorTest.php b/tests/Builder/Expression/DateAddOperatorTest.php new file mode 100644 index 000000000..32ecac36b --- /dev/null +++ b/tests/Builder/Expression/DateAddOperatorTest.php @@ -0,0 +1,125 @@ +assertSamePipeline(Pipelines::DateAddAddAFutureDate, $pipeline); + } + + public function testAdjustForDaylightSavingsTime(): void + { + $pipeline = new Pipeline( + Stage::project( + _id: 0, + location: 1, + start: Expression::dateToString( + format: '%Y-%m-%d %H:%M', + date: Expression::dateFieldPath('login'), + ), + days: Expression::dateToString( + format: '%Y-%m-%d %H:%M', + date: Expression::dateAdd( + startDate: Expression::dateFieldPath('login'), + unit: TimeUnit::Day, + amount: 1, + timezone: Expression::stringFieldPath('location'), + ), + ), + hours: Expression::dateToString( + format: '%Y-%m-%d %H:%M', + date: Expression::dateAdd( + startDate: Expression::dateFieldPath('login'), + unit: TimeUnit::Hour, + amount: 24, + timezone: Expression::stringFieldPath('location'), + ), + ), + startTZInfo: Expression::dateToString( + format: '%Y-%m-%d %H:%M', + date: Expression::dateFieldPath('login'), + timezone: Expression::stringFieldPath('location'), + ), + daysTZInfo: Expression::dateToString( + format: '%Y-%m-%d %H:%M', + date: Expression::dateAdd( + startDate: Expression::dateFieldPath('login'), + unit: TimeUnit::Day, + amount: 1, + timezone: Expression::stringFieldPath('location'), + ), + timezone: Expression::stringFieldPath('location'), + ), + hoursTZInfo: Expression::dateToString( + format: '%Y-%m-%d %H:%M', + date: Expression::dateAdd( + startDate: Expression::dateFieldPath('login'), + unit: TimeUnit::Hour, + amount: 24, + timezone: Expression::stringFieldPath('location'), + ), + timezone: Expression::stringFieldPath('location'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::DateAddAdjustForDaylightSavingsTime, $pipeline); + } + + public function testFilterOnADateRange(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::expr( + Expression::gt( + Expression::dateFieldPath('deliveryDate'), + Expression::dateAdd( + startDate: Expression::dateFieldPath('purchaseDate'), + unit: TimeUnit::Day, + amount: 5, + ), + ), + ), + ), + Stage::project( + _id: 0, + custId: 1, + purchased: Expression::dateToString( + format: '%Y-%m-%d', + date: Expression::dateFieldPath('purchaseDate'), + ), + delivery: Expression::dateToString( + format: '%Y-%m-%d', + date: Expression::dateFieldPath('deliveryDate'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::DateAddFilterOnADateRange, $pipeline); + } +} diff --git a/tests/Builder/Expression/DateDiffOperatorTest.php b/tests/Builder/Expression/DateDiffOperatorTest.php new file mode 100644 index 000000000..571e0473c --- /dev/null +++ b/tests/Builder/Expression/DateDiffOperatorTest.php @@ -0,0 +1,99 @@ +assertSamePipeline(Pipelines::DateDiffElapsedTime, $pipeline); + } + + public function testResultPrecision(): void + { + $pipeline = new Pipeline( + Stage::project( + Start: Expression::dateFieldPath('start'), + End: Expression::dateFieldPath('end'), + years: Expression::dateDiff( + startDate: Expression::dateFieldPath('start'), + endDate: Expression::dateFieldPath('end'), + unit: TimeUnit::Year, + ), + months: Expression::dateDiff( + startDate: Expression::dateFieldPath('start'), + endDate: Expression::dateFieldPath('end'), + unit: TimeUnit::Month, + ), + days: Expression::dateDiff( + startDate: Expression::dateFieldPath('start'), + endDate: Expression::dateFieldPath('end'), + unit: TimeUnit::Day, + ), + _id: 0, + ), + ); + + $this->assertSamePipeline(Pipelines::DateDiffResultPrecision, $pipeline); + } + + public function testWeeksPerMonth(): void + { + $pipeline = new Pipeline( + Stage::project( + wks_default: Expression::dateDiff( + startDate: Expression::dateFieldPath('start'), + endDate: Expression::dateFieldPath('end'), + unit: TimeUnit::Week, + ), + wks_monday: Expression::dateDiff( + startDate: Expression::dateFieldPath('start'), + endDate: Expression::dateFieldPath('end'), + unit: TimeUnit::Week, + startOfWeek: 'Monday', + ), + wks_friday: Expression::dateDiff( + startDate: Expression::dateFieldPath('start'), + endDate: Expression::dateFieldPath('end'), + unit: TimeUnit::Week, + startOfWeek: 'fri', + ), + _id: 0, + ), + ); + + $this->assertSamePipeline(Pipelines::DateDiffWeeksPerMonth, $pipeline); + } +} diff --git a/tests/Builder/Expression/DateFromPartsOperatorTest.php b/tests/Builder/Expression/DateFromPartsOperatorTest.php new file mode 100644 index 000000000..aa8a471e1 --- /dev/null +++ b/tests/Builder/Expression/DateFromPartsOperatorTest.php @@ -0,0 +1,47 @@ +assertSamePipeline(Pipelines::DateFromPartsExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/DateFromStringOperatorTest.php b/tests/Builder/Expression/DateFromStringOperatorTest.php new file mode 100644 index 000000000..ce5e53a21 --- /dev/null +++ b/tests/Builder/Expression/DateFromStringOperatorTest.php @@ -0,0 +1,61 @@ +assertSamePipeline(Pipelines::DateFromStringConvertingDates, $pipeline); + } + + public function testOnError(): void + { + $pipeline = new Pipeline( + Stage::project( + date: Expression::dateFromString( + dateString: Expression::stringFieldPath('date'), + timezone: Expression::stringFieldPath('timezone'), + onError: Expression::stringFieldPath('date'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::DateFromStringOnError, $pipeline); + } + + public function testOnNull(): void + { + $pipeline = new Pipeline( + Stage::project( + date: Expression::dateFromString( + dateString: Expression::stringFieldPath('date'), + timezone: Expression::stringFieldPath('timezone'), + onNull: new UTCDateTime(0), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::DateFromStringOnNull, $pipeline); + } +} diff --git a/tests/Builder/Expression/DateSubtractOperatorTest.php b/tests/Builder/Expression/DateSubtractOperatorTest.php new file mode 100644 index 000000000..013a3db74 --- /dev/null +++ b/tests/Builder/Expression/DateSubtractOperatorTest.php @@ -0,0 +1,131 @@ +assertSamePipeline(Pipelines::DateSubtractAdjustForDaylightSavingsTime, $pipeline); + } + + public function testFilterByRelativeDates(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::expr( + Expression::gt( + Expression::dateFieldPath('logoutTime'), + Expression::dateSubtract( + startDate: Expression::variable('NOW'), + unit: TimeUnit::Week, + amount: 1, + ), + ), + ), + ), + Stage::project( + _id: 0, + custId: 1, + loggedOut: Expression::dateToString( + format: '%Y-%m-%d', + date: Expression::dateFieldPath('logoutTime'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::DateSubtractFilterByRelativeDates, $pipeline); + } + + public function testSubtractAFixedAmount(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::expr( + Expression::eq( + Expression::month( + Expression::dateFieldPath('logout'), + ), + 1, + ), + ), + ), + Stage::project( + logoutTime: Expression::dateSubtract( + startDate: Expression::dateFieldPath('logout'), + unit: TimeUnit::Hour, + amount: 3, + ), + ), + Stage::merge('connectionTime'), + ); + + $this->assertSamePipeline(Pipelines::DateSubtractSubtractAFixedAmount, $pipeline); + } +} diff --git a/tests/Builder/Expression/DateToPartsOperatorTest.php b/tests/Builder/Expression/DateToPartsOperatorTest.php new file mode 100644 index 000000000..3df3ae827 --- /dev/null +++ b/tests/Builder/Expression/DateToPartsOperatorTest.php @@ -0,0 +1,37 @@ +assertSamePipeline(Pipelines::DateToPartsExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/DateToStringOperatorTest.php b/tests/Builder/Expression/DateToStringOperatorTest.php new file mode 100644 index 000000000..a8a24a148 --- /dev/null +++ b/tests/Builder/Expression/DateToStringOperatorTest.php @@ -0,0 +1,60 @@ +assertSamePipeline(Pipelines::DateToStringExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/DateTruncOperatorTest.php b/tests/Builder/Expression/DateTruncOperatorTest.php new file mode 100644 index 000000000..37bf8592a --- /dev/null +++ b/tests/Builder/Expression/DateTruncOperatorTest.php @@ -0,0 +1,59 @@ +assertSamePipeline(Pipelines::DateTruncTruncateOrderDatesAndObtainQuantitySumInAGroupPipelineStage, $pipeline); + } + + public function testTruncateOrderDatesInAProjectPipelineStage(): void + { + $pipeline = new Pipeline( + Stage::project( + _id: 1, + orderDate: 1, + truncatedOrderDate: Expression::dateTrunc( + date: Expression::dateFieldPath('orderDate'), + unit: TimeUnit::Week, + binSize: 2, + timezone: 'America/Los_Angeles', + startOfWeek: 'Monday', + ), + ), + ); + + $this->assertSamePipeline(Pipelines::DateTruncTruncateOrderDatesInAProjectPipelineStage, $pipeline); + } +} diff --git a/tests/Builder/Expression/DayOfMonthOperatorTest.php b/tests/Builder/Expression/DayOfMonthOperatorTest.php new file mode 100644 index 000000000..07b22ae70 --- /dev/null +++ b/tests/Builder/Expression/DayOfMonthOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::DayOfMonthExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/DayOfWeekOperatorTest.php b/tests/Builder/Expression/DayOfWeekOperatorTest.php new file mode 100644 index 000000000..54c5d5402 --- /dev/null +++ b/tests/Builder/Expression/DayOfWeekOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::DayOfWeekExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/DayOfYearOperatorTest.php b/tests/Builder/Expression/DayOfYearOperatorTest.php new file mode 100644 index 000000000..494ec4365 --- /dev/null +++ b/tests/Builder/Expression/DayOfYearOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::DayOfYearExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/DegreesToRadiansOperatorTest.php b/tests/Builder/Expression/DegreesToRadiansOperatorTest.php new file mode 100644 index 000000000..82e9797a2 --- /dev/null +++ b/tests/Builder/Expression/DegreesToRadiansOperatorTest.php @@ -0,0 +1,35 @@ +assertSamePipeline(Pipelines::DegreesToRadiansExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/DivideOperatorTest.php b/tests/Builder/Expression/DivideOperatorTest.php new file mode 100644 index 000000000..dfccd98eb --- /dev/null +++ b/tests/Builder/Expression/DivideOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::DivideExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/EqOperatorTest.php b/tests/Builder/Expression/EqOperatorTest.php new file mode 100644 index 000000000..0c9b72bef --- /dev/null +++ b/tests/Builder/Expression/EqOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::EqExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ExpOperatorTest.php b/tests/Builder/Expression/ExpOperatorTest.php new file mode 100644 index 000000000..daa582518 --- /dev/null +++ b/tests/Builder/Expression/ExpOperatorTest.php @@ -0,0 +1,32 @@ +assertSamePipeline(Pipelines::ExpExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/FilterOperatorTest.php b/tests/Builder/Expression/FilterOperatorTest.php new file mode 100644 index 000000000..1cb3af584 --- /dev/null +++ b/tests/Builder/Expression/FilterOperatorTest.php @@ -0,0 +1,91 @@ +assertSamePipeline(Pipelines::FilterExample, $pipeline); + } + + public function testLimitAsANumericExpression(): void + { + $pipeline = new Pipeline( + Stage::project( + items: Expression::filter( + input: Expression::arrayFieldPath('items'), + cond: Expression::lte( + Expression::variable('item.price'), + 150, + ), + as: 'item', + limit: 2, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FilterLimitAsANumericExpression, $pipeline); + } + + public function testLimitGreaterThanPossibleMatches(): void + { + $pipeline = new Pipeline( + Stage::project( + items: Expression::filter( + input: Expression::arrayFieldPath('items'), + cond: Expression::gte( + Expression::variable('item.price'), + 100, + ), + as: 'item', + limit: 5, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FilterLimitGreaterThanPossibleMatches, $pipeline); + } + + public function testUsingTheLimitField(): void + { + $pipeline = new Pipeline( + Stage::project( + items: Expression::filter( + input: Expression::arrayFieldPath('items'), + cond: Expression::gte( + Expression::variable('item.price'), + 100, + ), + as: 'item', + limit: 1, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FilterUsingTheLimitField, $pipeline); + } +} diff --git a/tests/Builder/Expression/FirstNOperatorTest.php b/tests/Builder/Expression/FirstNOperatorTest.php new file mode 100644 index 000000000..a09c356d7 --- /dev/null +++ b/tests/Builder/Expression/FirstNOperatorTest.php @@ -0,0 +1,48 @@ +assertSamePipeline(Pipelines::FirstNExample, $pipeline); + } + + public function testUsingFirstNAsAnAggregationExpression(): void + { + $pipeline = new Pipeline( + Stage::documents([ + object( + array: [10, 20, 30, 40], + ), + ]), + Stage::project( + firstThreeElements: Expression::firstN( + input: Expression::arrayFieldPath('array'), + n: 3, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FirstNUsingFirstNAsAnAggregationExpression, $pipeline); + } +} diff --git a/tests/Builder/Expression/FirstOperatorTest.php b/tests/Builder/Expression/FirstOperatorTest.php new file mode 100644 index 000000000..10fab6cdc --- /dev/null +++ b/tests/Builder/Expression/FirstOperatorTest.php @@ -0,0 +1,27 @@ +assertSamePipeline(Pipelines::FirstUseInAddFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Expression/FloorOperatorTest.php b/tests/Builder/Expression/FloorOperatorTest.php new file mode 100644 index 000000000..8aa3c8393 --- /dev/null +++ b/tests/Builder/Expression/FloorOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::FloorExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/FunctionOperatorTest.php b/tests/Builder/Expression/FunctionOperatorTest.php new file mode 100644 index 000000000..97ee7c3c7 --- /dev/null +++ b/tests/Builder/Expression/FunctionOperatorTest.php @@ -0,0 +1,71 @@ +assertSamePipeline(Pipelines::FunctionAlternativeToWhere, $pipeline); + } + + public function testUsageExample(): void + { + $pipeline = new Pipeline( + Stage::addFields( + isFound: Expression::function( + body: <<<'JS' + function(name) { + return hex_md5(name) == "15b0a220baa16331e8d80e15367677ad" + } + JS, + args: [ + Expression::stringFieldPath('name'), + ], + ), + message: Expression::function( + body: <<<'JS' + function(name, scores) { + let total = Array.sum(scores); + return `Hello ${name}. Your total score is ${total}.` + } + JS, + args: [ + Expression::stringFieldPath('name'), + Expression::stringFieldPath('scores'), + ], + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FunctionUsageExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/GetFieldOperatorTest.php b/tests/Builder/Expression/GetFieldOperatorTest.php new file mode 100644 index 000000000..117149ddf --- /dev/null +++ b/tests/Builder/Expression/GetFieldOperatorTest.php @@ -0,0 +1,70 @@ +assertSamePipeline(Pipelines::GetFieldQueryAFieldInASubdocument, $pipeline); + } + + public function testQueryFieldsThatContainPeriods(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::expr( + Expression::gt( + Expression::getField('price.usd'), + 200, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::GetFieldQueryFieldsThatContainPeriods, $pipeline); + } + + public function testQueryFieldsThatStartWithADollarSign(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::expr( + Expression::gt( + Expression::getField( + Expression::literal('$price'), + ), + 200, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::GetFieldQueryFieldsThatStartWithADollarSign, $pipeline); + } +} diff --git a/tests/Builder/Expression/GtOperatorTest.php b/tests/Builder/Expression/GtOperatorTest.php new file mode 100644 index 000000000..a8cecfd84 --- /dev/null +++ b/tests/Builder/Expression/GtOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::GtExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/GteOperatorTest.php b/tests/Builder/Expression/GteOperatorTest.php new file mode 100644 index 000000000..abc89743f --- /dev/null +++ b/tests/Builder/Expression/GteOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::GteExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/HourOperatorTest.php b/tests/Builder/Expression/HourOperatorTest.php new file mode 100644 index 000000000..32234ea91 --- /dev/null +++ b/tests/Builder/Expression/HourOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::HourExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/IfNullOperatorTest.php b/tests/Builder/Expression/IfNullOperatorTest.php new file mode 100644 index 000000000..2bd046e8c --- /dev/null +++ b/tests/Builder/Expression/IfNullOperatorTest.php @@ -0,0 +1,47 @@ +assertSamePipeline(Pipelines::IfNullMultipleInputExpressions, $pipeline); + } + + public function testSingleInputExpression(): void + { + $pipeline = new Pipeline( + Stage::project( + item: 1, + description: Expression::ifNull( + Expression::fieldPath('description'), + 'Unspecified', + ), + ), + ); + + $this->assertSamePipeline(Pipelines::IfNullSingleInputExpression, $pipeline); + } +} diff --git a/tests/Builder/Expression/InOperatorTest.php b/tests/Builder/Expression/InOperatorTest.php new file mode 100644 index 000000000..27722f856 --- /dev/null +++ b/tests/Builder/Expression/InOperatorTest.php @@ -0,0 +1,33 @@ + Expression::fieldPath('location'), + 'has bananas' => Expression::in( + 'bananas', + Expression::arrayFieldPath('in_stock'), + ), + ], + ), + ); + + $this->assertSamePipeline(Pipelines::InExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/IndexOfArrayOperatorTest.php b/tests/Builder/Expression/IndexOfArrayOperatorTest.php new file mode 100644 index 000000000..a18343ea2 --- /dev/null +++ b/tests/Builder/Expression/IndexOfArrayOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::IndexOfArrayExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/IndexOfBytesOperatorTest.php b/tests/Builder/Expression/IndexOfBytesOperatorTest.php new file mode 100644 index 000000000..51c7720e9 --- /dev/null +++ b/tests/Builder/Expression/IndexOfBytesOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::IndexOfBytesExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/IndexOfCPOperatorTest.php b/tests/Builder/Expression/IndexOfCPOperatorTest.php new file mode 100644 index 000000000..4d5e3b6da --- /dev/null +++ b/tests/Builder/Expression/IndexOfCPOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::IndexOfCPExamples, $pipeline); + } +} diff --git a/tests/Builder/Expression/IsArrayOperatorTest.php b/tests/Builder/Expression/IsArrayOperatorTest.php new file mode 100644 index 000000000..983f4490d --- /dev/null +++ b/tests/Builder/Expression/IsArrayOperatorTest.php @@ -0,0 +1,37 @@ +assertSamePipeline(Pipelines::IsArrayExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/IsNumberOperatorTest.php b/tests/Builder/Expression/IsNumberOperatorTest.php new file mode 100644 index 000000000..0d7d2d4bc --- /dev/null +++ b/tests/Builder/Expression/IsNumberOperatorTest.php @@ -0,0 +1,94 @@ +assertSamePipeline(Pipelines::IsNumberConditionallyModifyFieldsUsingIsNumber, $pipeline); + } + + public function testUseIsNumberToCheckIfAFieldIsNumeric(): void + { + $pipeline = new Pipeline( + Stage::addFields( + isNumber: Expression::isNumber( + Expression::fieldPath('reading'), + ), + hasType: Expression::type( + Expression::fieldPath('reading'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::IsNumberUseIsNumberToCheckIfAFieldIsNumeric, $pipeline); + } +} diff --git a/tests/Builder/Expression/IsoDayOfWeekOperatorTest.php b/tests/Builder/Expression/IsoDayOfWeekOperatorTest.php new file mode 100644 index 000000000..0169b97dc --- /dev/null +++ b/tests/Builder/Expression/IsoDayOfWeekOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::IsoDayOfWeekExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/IsoWeekOperatorTest.php b/tests/Builder/Expression/IsoWeekOperatorTest.php new file mode 100644 index 000000000..adc00f640 --- /dev/null +++ b/tests/Builder/Expression/IsoWeekOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::IsoWeekExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/IsoWeekYearOperatorTest.php b/tests/Builder/Expression/IsoWeekYearOperatorTest.php new file mode 100644 index 000000000..613f32090 --- /dev/null +++ b/tests/Builder/Expression/IsoWeekYearOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::IsoWeekYearExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/LastNOperatorTest.php b/tests/Builder/Expression/LastNOperatorTest.php new file mode 100644 index 000000000..485721135 --- /dev/null +++ b/tests/Builder/Expression/LastNOperatorTest.php @@ -0,0 +1,43 @@ +assertSamePipeline(Pipelines::LastNExample, $pipeline); + } + + public function testUsingLastNAsAnAggregationExpression(): void + { + $pipeline = new Pipeline( + Stage::documents([ + [ + 'array' => [10, 20, 30, 40], + ], + ]), + Stage::project( + lastThreeElements: Expression::lastN(input: Expression::arrayFieldPath('array'), n: 3), + ), + ); + + $this->assertSamePipeline(Pipelines::LastNUsingLastNAsAnAggregationExpression, $pipeline); + } +} diff --git a/tests/Builder/Expression/LastOperatorTest.php b/tests/Builder/Expression/LastOperatorTest.php new file mode 100644 index 000000000..7383819d6 --- /dev/null +++ b/tests/Builder/Expression/LastOperatorTest.php @@ -0,0 +1,27 @@ +assertSamePipeline(Pipelines::LastUseInAddFieldsStage, $pipeline); + } +} diff --git a/tests/Builder/Expression/LetOperatorTest.php b/tests/Builder/Expression/LetOperatorTest.php new file mode 100644 index 000000000..602a69adf --- /dev/null +++ b/tests/Builder/Expression/LetOperatorTest.php @@ -0,0 +1,45 @@ +assertSamePipeline(Pipelines::LetExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/LnOperatorTest.php b/tests/Builder/Expression/LnOperatorTest.php new file mode 100644 index 000000000..cbbb85ce2 --- /dev/null +++ b/tests/Builder/Expression/LnOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::LnExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/Log10OperatorTest.php b/tests/Builder/Expression/Log10OperatorTest.php new file mode 100644 index 000000000..eb7e6b693 --- /dev/null +++ b/tests/Builder/Expression/Log10OperatorTest.php @@ -0,0 +1,32 @@ +assertSamePipeline(Pipelines::Log10Example, $pipeline); + } +} diff --git a/tests/Builder/Expression/LogOperatorTest.php b/tests/Builder/Expression/LogOperatorTest.php new file mode 100644 index 000000000..f1215d53e --- /dev/null +++ b/tests/Builder/Expression/LogOperatorTest.php @@ -0,0 +1,35 @@ +assertSamePipeline(Pipelines::LogExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/LtOperatorTest.php b/tests/Builder/Expression/LtOperatorTest.php new file mode 100644 index 000000000..86a7815aa --- /dev/null +++ b/tests/Builder/Expression/LtOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::LtExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/LteOperatorTest.php b/tests/Builder/Expression/LteOperatorTest.php new file mode 100644 index 000000000..4a65cd593 --- /dev/null +++ b/tests/Builder/Expression/LteOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::LteExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/LtrimOperatorTest.php b/tests/Builder/Expression/LtrimOperatorTest.php new file mode 100644 index 000000000..96045dd32 --- /dev/null +++ b/tests/Builder/Expression/LtrimOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::LtrimExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/MapOperatorTest.php b/tests/Builder/Expression/MapOperatorTest.php new file mode 100644 index 000000000..347801c3a --- /dev/null +++ b/tests/Builder/Expression/MapOperatorTest.php @@ -0,0 +1,75 @@ + [ + '$$grade', + 2, + ], + ], + ), + ), + ); + + $this->assertSamePipeline(Pipelines::MapAddToEachElementOfAnArray, $pipeline); + } + + public function testConvertCelsiusTemperaturesToFahrenheit(): void + { + $pipeline = new Pipeline( + Stage::addFields( + tempsF: Expression::map( + input: Expression::arrayFieldPath('tempsC'), + as: 'tempInCelsius', + in: Expression::add( + Expression::multiply( + Expression::variable('tempInCelsius'), + 1.8, + ), + 32, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::MapConvertCelsiusTemperaturesToFahrenheit, $pipeline); + } + + public function testTruncateEachArrayElement(): void + { + $pipeline = new Pipeline( + Stage::project( + city: Expression::stringFieldPath('city'), + integerValues: Expression::map( + input: Expression::arrayFieldPath('distances'), + as: 'decimalValue', + in: Expression::trunc( + Expression::variable('decimalValue'), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::MapTruncateEachArrayElement, $pipeline); + } +} diff --git a/tests/Builder/Expression/MaxNOperatorTest.php b/tests/Builder/Expression/MaxNOperatorTest.php new file mode 100644 index 000000000..1f94cd31f --- /dev/null +++ b/tests/Builder/Expression/MaxNOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::MaxNExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/MaxOperatorTest.php b/tests/Builder/Expression/MaxOperatorTest.php new file mode 100644 index 000000000..eb88daa85 --- /dev/null +++ b/tests/Builder/Expression/MaxOperatorTest.php @@ -0,0 +1,36 @@ +assertSamePipeline(Pipelines::MaxUseInProjectStage, $pipeline); + } +} diff --git a/tests/Builder/Expression/MedianOperatorTest.php b/tests/Builder/Expression/MedianOperatorTest.php new file mode 100644 index 000000000..f231344f3 --- /dev/null +++ b/tests/Builder/Expression/MedianOperatorTest.php @@ -0,0 +1,36 @@ +assertSamePipeline(Pipelines::MedianUseMedianInAProjectStage, $pipeline); + } +} diff --git a/tests/Builder/Expression/MergeObjectsOperatorTest.php b/tests/Builder/Expression/MergeObjectsOperatorTest.php new file mode 100644 index 000000000..0ca4f313f --- /dev/null +++ b/tests/Builder/Expression/MergeObjectsOperatorTest.php @@ -0,0 +1,39 @@ +assertSamePipeline(Pipelines::MergeObjectsMergeObjects, $pipeline); + } +} diff --git a/tests/Builder/Expression/MetaOperatorTest.php b/tests/Builder/Expression/MetaOperatorTest.php new file mode 100644 index 000000000..b8bc3d933 --- /dev/null +++ b/tests/Builder/Expression/MetaOperatorTest.php @@ -0,0 +1,49 @@ +assertSamePipeline(Pipelines::MetaIndexKey, $pipeline); + } + + public function testTextScore(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::text( + search: 'cake', + ), + ), + Stage::group( + _id: Expression::meta('textScore'), + count: Accumulator::sum(1), + ), + ); + + $this->assertSamePipeline(Pipelines::MetaTextScore, $pipeline); + } +} diff --git a/tests/Builder/Expression/MillisecondOperatorTest.php b/tests/Builder/Expression/MillisecondOperatorTest.php new file mode 100644 index 000000000..4d6bd1f57 --- /dev/null +++ b/tests/Builder/Expression/MillisecondOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::MillisecondExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/MinNOperatorTest.php b/tests/Builder/Expression/MinNOperatorTest.php new file mode 100644 index 000000000..f6fc34eff --- /dev/null +++ b/tests/Builder/Expression/MinNOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::MinNExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/MinOperatorTest.php b/tests/Builder/Expression/MinOperatorTest.php new file mode 100644 index 000000000..11e1416a5 --- /dev/null +++ b/tests/Builder/Expression/MinOperatorTest.php @@ -0,0 +1,36 @@ +assertSamePipeline(Pipelines::MinUseInProjectStage, $pipeline); + } +} diff --git a/tests/Builder/Expression/MinuteOperatorTest.php b/tests/Builder/Expression/MinuteOperatorTest.php new file mode 100644 index 000000000..adf1d47e6 --- /dev/null +++ b/tests/Builder/Expression/MinuteOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::MinuteExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ModOperatorTest.php b/tests/Builder/Expression/ModOperatorTest.php new file mode 100644 index 000000000..5951779c4 --- /dev/null +++ b/tests/Builder/Expression/ModOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::ModExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/MonthOperatorTest.php b/tests/Builder/Expression/MonthOperatorTest.php new file mode 100644 index 000000000..0945b172a --- /dev/null +++ b/tests/Builder/Expression/MonthOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::MonthExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/MultiplyOperatorTest.php b/tests/Builder/Expression/MultiplyOperatorTest.php new file mode 100644 index 000000000..546c4185c --- /dev/null +++ b/tests/Builder/Expression/MultiplyOperatorTest.php @@ -0,0 +1,32 @@ +assertSamePipeline(Pipelines::MultiplyExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/NeOperatorTest.php b/tests/Builder/Expression/NeOperatorTest.php new file mode 100644 index 000000000..b052be640 --- /dev/null +++ b/tests/Builder/Expression/NeOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::NeExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/NotOperatorTest.php b/tests/Builder/Expression/NotOperatorTest.php new file mode 100644 index 000000000..cd816a0a7 --- /dev/null +++ b/tests/Builder/Expression/NotOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::NotExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ObjectToArrayOperatorTest.php b/tests/Builder/Expression/ObjectToArrayOperatorTest.php new file mode 100644 index 000000000..0abbde2ef --- /dev/null +++ b/tests/Builder/Expression/ObjectToArrayOperatorTest.php @@ -0,0 +1,53 @@ +assertSamePipeline(Pipelines::ObjectToArrayObjectToArrayExample, $pipeline); + } + + public function testObjectToArrayToSumNestedFields(): void + { + $pipeline = new Pipeline( + Stage::project( + warehouses: Expression::objectToArray( + Expression::objectFieldPath('instock'), + ), + ), + Stage::unwind( + Expression::arrayFieldPath('warehouses'), + ), + Stage::group( + _id: Expression::fieldPath('warehouses.k'), + total: Accumulator::sum( + Expression::fieldPath('warehouses.v'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ObjectToArrayObjectToArrayToSumNestedFields, $pipeline); + } +} diff --git a/tests/Builder/Expression/OrOperatorTest.php b/tests/Builder/Expression/OrOperatorTest.php new file mode 100644 index 000000000..feb544c1c --- /dev/null +++ b/tests/Builder/Expression/OrOperatorTest.php @@ -0,0 +1,37 @@ +assertSamePipeline(Pipelines::OrExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/PercentileOperatorTest.php b/tests/Builder/Expression/PercentileOperatorTest.php new file mode 100644 index 000000000..9d1538bce --- /dev/null +++ b/tests/Builder/Expression/PercentileOperatorTest.php @@ -0,0 +1,37 @@ +assertSamePipeline(Pipelines::PercentileUsePercentileInAProjectStage, $pipeline); + } +} diff --git a/tests/Builder/Expression/Pipelines.php b/tests/Builder/Expression/Pipelines.php new file mode 100644 index 000000000..f5a4bcec1 --- /dev/null +++ b/tests/Builder/Expression/Pipelines.php @@ -0,0 +1,6153 @@ +assertSamePipeline(Pipelines::PowExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/RadiansToDegreesOperatorTest.php b/tests/Builder/Expression/RadiansToDegreesOperatorTest.php new file mode 100644 index 000000000..b52375936 --- /dev/null +++ b/tests/Builder/Expression/RadiansToDegreesOperatorTest.php @@ -0,0 +1,35 @@ +assertSamePipeline(Pipelines::RadiansToDegreesExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/RandOperatorTest.php b/tests/Builder/Expression/RandOperatorTest.php new file mode 100644 index 000000000..b4964df69 --- /dev/null +++ b/tests/Builder/Expression/RandOperatorTest.php @@ -0,0 +1,61 @@ +assertSamePipeline(Pipelines::RandGenerateRandomDataPoints, $pipeline); + } + + public function testSelectRandomItemsFromACollection(): void + { + $pipeline = new Pipeline( + Stage::match( + district: 3, + ), + Stage::match( + Query::expr( + Expression::lt( + 0.5, + Expression::rand(), + ), + ), + ), + Stage::project( + _id: 0, + name: 1, + registered: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::RandSelectRandomItemsFromACollection, $pipeline); + } +} diff --git a/tests/Builder/Expression/RangeOperatorTest.php b/tests/Builder/Expression/RangeOperatorTest.php new file mode 100644 index 000000000..be9a1a3a7 --- /dev/null +++ b/tests/Builder/Expression/RangeOperatorTest.php @@ -0,0 +1,36 @@ + Expression::range( + 0, + Expression::intFieldPath('distance'), + 25, + ), + ], + _id: 0, + city: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::RangeExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ReduceOperatorTest.php b/tests/Builder/Expression/ReduceOperatorTest.php new file mode 100644 index 000000000..ee1f70720 --- /dev/null +++ b/tests/Builder/Expression/ReduceOperatorTest.php @@ -0,0 +1,141 @@ +assertSamePipeline(Pipelines::ReduceArrayConcatenation, $pipeline); + } + + public function testComputingAMultipleReductions(): void + { + $pipeline = new Pipeline( + Stage::project( + results: Expression::reduce( + Expression::arrayFieldPath('arr'), + [], + object( + collapsed: Expression::concatArrays( + Expression::variable('value.collapsed'), + Expression::variable('this'), + ), + firstValues: Expression::concatArrays( + Expression::variable('value.firstValues'), + Expression::slice( + Expression::variable('this'), + 1, + ), + ), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ReduceComputingAMultipleReductions, $pipeline); + } + + public function testDiscountedMerchandise(): void + { + $pipeline = new Pipeline( + Stage::project( + discountedPrice: Expression::reduce( + input: Expression::arrayFieldPath('discounts'), + initialValue: Expression::numberFieldPath('price'), + in: Expression::multiply( + Expression::variable('value'), + Expression::subtract( + 1, + Expression::variable('this'), + ), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ReduceDiscountedMerchandise, $pipeline); + } + + public function testMultiplication(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::objectIdFieldPath('experimentId'), + probabilityArr: Accumulator::push( + Expression::fieldPath('probability'), + ), + ), + Stage::project( + description: 1, + results: Expression::reduce( + input: Expression::arrayFieldPath('probabilityArr'), + initialValue: 1, + in: Expression::multiply( + Expression::variable('value'), + Expression::variable('this'), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ReduceMultiplication, $pipeline); + } + + public function testStringConcatenation(): void + { + $pipeline = new Pipeline( + Stage::match( + hobbies: Query::gt([]), + ), + Stage::project( + name: 1, + bio: Expression::reduce( + input: Expression::arrayFieldPath('hobbies'), + initialValue: 'My hobbies include:', + in: Expression::concat( + Expression::variable('value'), + Expression::cond( + if: Expression::eq( + Expression::variable('value'), + 'My hobbies include:', + ), + then: ' ', + else: ', ', + ), + Expression::variable('this'), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ReduceStringConcatenation, $pipeline); + } +} diff --git a/tests/Builder/Expression/RegexFindAllOperatorTest.php b/tests/Builder/Expression/RegexFindAllOperatorTest.php new file mode 100644 index 000000000..df8286c11 --- /dev/null +++ b/tests/Builder/Expression/RegexFindAllOperatorTest.php @@ -0,0 +1,100 @@ +assertSamePipeline(Pipelines::RegexFindAllIOption, $pipeline); + } + + public function testRegexFindAllAndItsOptions(): void + { + $pipeline = new Pipeline( + Stage::addFields( + returnObject: Expression::regexFindAll( + input: Expression::stringFieldPath('description'), + regex: new Regex('line'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::RegexFindAllRegexFindAllAndItsOptions, $pipeline); + } + + public function testUseCapturedGroupingsToParseUserName(): void + { + $pipeline = new Pipeline( + Stage::addFields( + names: Expression::regexFindAll( + input: Expression::stringFieldPath('comment'), + regex: new Regex('([a-z0-9_.+-]+)@[a-z0-9_.+-]+\\.[a-z0-9_.+-]+', 'i'), + ), + ), + Stage::set( + names: Expression::reduce( + input: Expression::arrayFieldPath('names.captures'), + initialValue: [], + in: Expression::concatArrays( + Expression::variable('value'), + Expression::variable('this'), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::RegexFindAllUseCapturedGroupingsToParseUserName, $pipeline); + } + + public function testUseRegexFindAllToParseEmailFromString(): void + { + $pipeline = new Pipeline( + Stage::addFields( + email: Expression::regexFindAll( + input: Expression::stringFieldPath('comment'), + regex: new Regex('[a-z0-9_.+-]+@[a-z0-9_.+-]+\\.[a-z0-9_.+-]+', 'i'), + ), + ), + Stage::set( + email: Expression::stringFieldPath('email.match'), + ), + ); + + $this->assertSamePipeline(Pipelines::RegexFindAllUseRegexFindAllToParseEmailFromString, $pipeline); + } +} diff --git a/tests/Builder/Expression/RegexFindOperatorTest.php b/tests/Builder/Expression/RegexFindOperatorTest.php new file mode 100644 index 000000000..d2ed7b453 --- /dev/null +++ b/tests/Builder/Expression/RegexFindOperatorTest.php @@ -0,0 +1,59 @@ +assertSamePipeline(Pipelines::RegexFindIOption, $pipeline); + } + + public function testRegexFindAndItsOptions(): void + { + $pipeline = new Pipeline( + Stage::addFields( + returnObject: Expression::regexFind( + input: Expression::stringFieldPath('description'), + regex: new Regex('line'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::RegexFindRegexFindAndItsOptions, $pipeline); + } +} diff --git a/tests/Builder/Expression/RegexMatchOperatorTest.php b/tests/Builder/Expression/RegexMatchOperatorTest.php new file mode 100644 index 000000000..55f39faf4 --- /dev/null +++ b/tests/Builder/Expression/RegexMatchOperatorTest.php @@ -0,0 +1,77 @@ +assertSamePipeline(Pipelines::RegexMatchIOption, $pipeline); + } + + public function testRegexMatchAndItsOptions(): void + { + $pipeline = new Pipeline( + Stage::addFields( + result: Expression::regexMatch( + input: Expression::stringFieldPath('description'), + regex: new Regex('line', ''), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::RegexMatchRegexMatchAndItsOptions, $pipeline); + } + + public function testUseRegexMatchToCheckEmailAddress(): void + { + $pipeline = new Pipeline( + Stage::addFields( + category: Expression::cond( + if: Expression::regexMatch( + input: Expression::stringFieldPath('comment'), + regex: new Regex('[a-z0-9_.+-]+@mongodb.com', 'i'), + ), + then: 'Employee', + else: 'External', + ), + ), + ); + + $this->assertSamePipeline(Pipelines::RegexMatchUseRegexMatchToCheckEmailAddress, $pipeline); + } +} diff --git a/tests/Builder/Expression/ReplaceAllOperatorTest.php b/tests/Builder/Expression/ReplaceAllOperatorTest.php new file mode 100644 index 000000000..1d1fa6ada --- /dev/null +++ b/tests/Builder/Expression/ReplaceAllOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::ReplaceAllExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ReplaceOneOperatorTest.php b/tests/Builder/Expression/ReplaceOneOperatorTest.php new file mode 100644 index 000000000..90bf8082b --- /dev/null +++ b/tests/Builder/Expression/ReplaceOneOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::ReplaceOneExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ReverseArrayOperatorTest.php b/tests/Builder/Expression/ReverseArrayOperatorTest.php new file mode 100644 index 000000000..f620cb4cc --- /dev/null +++ b/tests/Builder/Expression/ReverseArrayOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::ReverseArrayExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/RoundOperatorTest.php b/tests/Builder/Expression/RoundOperatorTest.php new file mode 100644 index 000000000..accae5074 --- /dev/null +++ b/tests/Builder/Expression/RoundOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::RoundExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/RtrimOperatorTest.php b/tests/Builder/Expression/RtrimOperatorTest.php new file mode 100644 index 000000000..077fe8dd4 --- /dev/null +++ b/tests/Builder/Expression/RtrimOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::RtrimExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SecondOperatorTest.php b/tests/Builder/Expression/SecondOperatorTest.php new file mode 100644 index 000000000..370686876 --- /dev/null +++ b/tests/Builder/Expression/SecondOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::SecondExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SetDifferenceOperatorTest.php b/tests/Builder/Expression/SetDifferenceOperatorTest.php new file mode 100644 index 000000000..1a1fafcc4 --- /dev/null +++ b/tests/Builder/Expression/SetDifferenceOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::SetDifferenceExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SetEqualsOperatorTest.php b/tests/Builder/Expression/SetEqualsOperatorTest.php new file mode 100644 index 000000000..e3974dad7 --- /dev/null +++ b/tests/Builder/Expression/SetEqualsOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::SetEqualsExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SetFieldOperatorTest.php b/tests/Builder/Expression/SetFieldOperatorTest.php new file mode 100644 index 000000000..eb844334a --- /dev/null +++ b/tests/Builder/Expression/SetFieldOperatorTest.php @@ -0,0 +1,114 @@ +assertSamePipeline(Pipelines::SetFieldAddFieldsThatContainPeriods, $pipeline); + } + + public function testAddFieldsThatStartWithADollarSign(): void + { + $pipeline = new Pipeline( + Stage::replaceWith( + Expression::setField( + field: Expression::literal('$price'), + input: Expression::variable('ROOT'), + value: Expression::fieldPath('price'), + ), + ), + Stage::unset('price'), + ); + + $this->assertSamePipeline(Pipelines::SetFieldAddFieldsThatStartWithADollarSign, $pipeline); + } + + public function testRemoveFieldsThatContainPeriods(): void + { + $pipeline = new Pipeline( + Stage::replaceWith( + Expression::setField( + field: 'price.usd', + input: Expression::variable('ROOT'), + value: Expression::variable('REMOVE'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetFieldRemoveFieldsThatContainPeriods, $pipeline); + } + + public function testRemoveFieldsThatStartWithADollarSign(): void + { + $pipeline = new Pipeline( + Stage::replaceWith( + Expression::setField( + field: Expression::literal('$price'), + input: Expression::variable('ROOT'), + value: Expression::variable('REMOVE'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetFieldRemoveFieldsThatStartWithADollarSign, $pipeline); + } + + public function testUpdateFieldsThatContainPeriods(): void + { + $pipeline = new Pipeline( + Stage::match( + _id: 1, + ), + Stage::replaceWith( + Expression::setField( + field: 'price.usd', + input: Expression::variable('ROOT'), + value: 49.99, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetFieldUpdateFieldsThatContainPeriods, $pipeline); + } + + public function testUpdateFieldsThatStartWithADollarSign(): void + { + $pipeline = new Pipeline( + Stage::match( + _id: 1, + ), + Stage::replaceWith( + Expression::setField( + field: Expression::literal('$price'), + input: Expression::variable('ROOT'), + value: 49.99, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetFieldUpdateFieldsThatStartWithADollarSign, $pipeline); + } +} diff --git a/tests/Builder/Expression/SetIntersectionOperatorTest.php b/tests/Builder/Expression/SetIntersectionOperatorTest.php new file mode 100644 index 000000000..7bce31d1c --- /dev/null +++ b/tests/Builder/Expression/SetIntersectionOperatorTest.php @@ -0,0 +1,55 @@ +assertSamePipeline(Pipelines::SetIntersectionElementsArrayExample, $pipeline); + } + + public function testRetrieveDocumentsForRolesGrantedToTheCurrentUser(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::expr( + Expression::not( + Expression::eq( + Expression::setIntersection( + Expression::arrayFieldPath('allowedRoles'), + Expression::variable('USER_ROLES.role'), + ), + [], + ), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetIntersectionRetrieveDocumentsForRolesGrantedToTheCurrentUser, $pipeline); + } +} diff --git a/tests/Builder/Expression/SetIsSubsetOperatorTest.php b/tests/Builder/Expression/SetIsSubsetOperatorTest.php new file mode 100644 index 000000000..b54e6bde6 --- /dev/null +++ b/tests/Builder/Expression/SetIsSubsetOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::SetIsSubsetExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SetUnionOperatorTest.php b/tests/Builder/Expression/SetUnionOperatorTest.php new file mode 100644 index 000000000..56bf8694c --- /dev/null +++ b/tests/Builder/Expression/SetUnionOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::SetUnionExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SinOperatorTest.php b/tests/Builder/Expression/SinOperatorTest.php new file mode 100644 index 000000000..5a585f3c9 --- /dev/null +++ b/tests/Builder/Expression/SinOperatorTest.php @@ -0,0 +1,34 @@ +assertSamePipeline(Pipelines::SinExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SinhOperatorTest.php b/tests/Builder/Expression/SinhOperatorTest.php new file mode 100644 index 000000000..5ba2241f3 --- /dev/null +++ b/tests/Builder/Expression/SinhOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::SinhExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SizeOperatorTest.php b/tests/Builder/Expression/SizeOperatorTest.php new file mode 100644 index 000000000..edd2c6cb4 --- /dev/null +++ b/tests/Builder/Expression/SizeOperatorTest.php @@ -0,0 +1,36 @@ +assertSamePipeline(Pipelines::SizeExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SliceOperatorTest.php b/tests/Builder/Expression/SliceOperatorTest.php new file mode 100644 index 000000000..b6326b051 --- /dev/null +++ b/tests/Builder/Expression/SliceOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::SliceExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SortArrayOperatorTest.php b/tests/Builder/Expression/SortArrayOperatorTest.php new file mode 100644 index 000000000..a2b738f38 --- /dev/null +++ b/tests/Builder/Expression/SortArrayOperatorTest.php @@ -0,0 +1,116 @@ +assertSamePipeline(Pipelines::SortArraySortAnArrayOfIntegers, $pipeline); + } + + public function testSortOnAField(): void + { + $pipeline = new Pipeline( + Stage::project( + _id: 0, + result: Expression::sortArray( + input: Expression::arrayFieldPath('team'), + // @todo This object should be typed as "sort spec" + sortBy: object( + name: Sort::Asc, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SortArraySortOnAField, $pipeline); + } + + public function testSortOnASubfield(): void + { + $pipeline = new Pipeline( + Stage::project( + _id: 0, + result: Expression::sortArray( + input: Expression::arrayFieldPath('team'), + sortBy: [ + 'address.city' => Sort::Desc, + ], + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SortArraySortOnASubfield, $pipeline); + } + + public function testSortOnMixedTypeFields(): void + { + $pipeline = new Pipeline( + Stage::project( + _id: 0, + result: Expression::sortArray( + input: [ + 20, + 4, + object(a: 'Free'), + 6, + 21, + 5, + 'Gratis', + ['a' => null], + object(a: object(sale: true, price: 19)), + new Decimal128('10.23'), + ['a' => 'On sale'], + ], + sortBy: Sort::Asc, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SortArraySortOnMixedTypeFields, $pipeline); + } + + public function testSortOnMultipleFields(): void + { + $pipeline = new Pipeline( + Stage::project( + _id: 0, + result: Expression::sortArray( + input: Expression::arrayFieldPath('team'), + // @todo This array should be typed as "sort spec" + sortBy: object( + age: Sort::Desc, + name: Sort::Asc, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SortArraySortOnMultipleFields, $pipeline); + } +} diff --git a/tests/Builder/Expression/SplitOperatorTest.php b/tests/Builder/Expression/SplitOperatorTest.php new file mode 100644 index 000000000..c302f4e9d --- /dev/null +++ b/tests/Builder/Expression/SplitOperatorTest.php @@ -0,0 +1,53 @@ +assertSamePipeline(Pipelines::SplitExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SqrtOperatorTest.php b/tests/Builder/Expression/SqrtOperatorTest.php new file mode 100644 index 000000000..de33969f5 --- /dev/null +++ b/tests/Builder/Expression/SqrtOperatorTest.php @@ -0,0 +1,44 @@ +assertSamePipeline(Pipelines::SqrtExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/StdDevPopOperatorTest.php b/tests/Builder/Expression/StdDevPopOperatorTest.php new file mode 100644 index 000000000..fdff25f99 --- /dev/null +++ b/tests/Builder/Expression/StdDevPopOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::StdDevPopUseInProjectStage, $pipeline); + } +} diff --git a/tests/Builder/Expression/StrLenBytesOperatorTest.php b/tests/Builder/Expression/StrLenBytesOperatorTest.php new file mode 100644 index 000000000..1c465b672 --- /dev/null +++ b/tests/Builder/Expression/StrLenBytesOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::StrLenBytesSingleByteAndMultibyteCharacterSet, $pipeline); + } +} diff --git a/tests/Builder/Expression/StrLenCPOperatorTest.php b/tests/Builder/Expression/StrLenCPOperatorTest.php new file mode 100644 index 000000000..78e995479 --- /dev/null +++ b/tests/Builder/Expression/StrLenCPOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::StrLenCPSingleByteAndMultibyteCharacterSet, $pipeline); + } +} diff --git a/tests/Builder/Expression/StrcasecmpOperatorTest.php b/tests/Builder/Expression/StrcasecmpOperatorTest.php new file mode 100644 index 000000000..543ac54d0 --- /dev/null +++ b/tests/Builder/Expression/StrcasecmpOperatorTest.php @@ -0,0 +1,31 @@ +assertSamePipeline(Pipelines::StrcasecmpExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SubstrBytesOperatorTest.php b/tests/Builder/Expression/SubstrBytesOperatorTest.php new file mode 100644 index 000000000..34d4d6f61 --- /dev/null +++ b/tests/Builder/Expression/SubstrBytesOperatorTest.php @@ -0,0 +1,58 @@ +assertSamePipeline(Pipelines::SubstrBytesSingleByteAndMultibyteCharacterSet, $pipeline); + } + + public function testSingleByteCharacterSet(): void + { + $pipeline = new Pipeline( + Stage::project( + item: 1, + yearSubstring: Expression::substrBytes( + Expression::stringFieldPath('quarter'), + 0, + 2, + ), + quarterSubtring: Expression::substrBytes( + Expression::stringFieldPath('quarter'), + 2, + Expression::subtract( + Expression::strLenBytes( + Expression::stringFieldPath('quarter'), + ), + 2, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SubstrBytesSingleByteCharacterSet, $pipeline); + } +} diff --git a/tests/Builder/Expression/SubstrCPOperatorTest.php b/tests/Builder/Expression/SubstrCPOperatorTest.php new file mode 100644 index 000000000..9a24b1fa9 --- /dev/null +++ b/tests/Builder/Expression/SubstrCPOperatorTest.php @@ -0,0 +1,58 @@ +assertSamePipeline(Pipelines::SubstrCPSingleByteAndMultibyteCharacterSet, $pipeline); + } + + public function testSingleByteCharacterSet(): void + { + $pipeline = new Pipeline( + Stage::project( + item: 1, + yearSubstring: Expression::substrCP( + Expression::stringFieldPath('quarter'), + 0, + 2, + ), + quarterSubtring: Expression::substrCP( + Expression::stringFieldPath('quarter'), + 2, + Expression::subtract( + Expression::strLenCP( + Expression::stringFieldPath('quarter'), + ), + 2, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SubstrCPSingleByteCharacterSet, $pipeline); + } +} diff --git a/tests/Builder/Expression/SubstrOperatorTest.php b/tests/Builder/Expression/SubstrOperatorTest.php new file mode 100644 index 000000000..f2a6f432d --- /dev/null +++ b/tests/Builder/Expression/SubstrOperatorTest.php @@ -0,0 +1,37 @@ +assertSamePipeline(Pipelines::SubstrExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/SubtractOperatorTest.php b/tests/Builder/Expression/SubtractOperatorTest.php new file mode 100644 index 000000000..95b691a95 --- /dev/null +++ b/tests/Builder/Expression/SubtractOperatorTest.php @@ -0,0 +1,64 @@ +assertSamePipeline(Pipelines::SubtractSubtractMillisecondsFromADate, $pipeline); + } + + public function testSubtractNumbers(): void + { + $pipeline = new Pipeline( + Stage::project( + item: 1, + total: Expression::subtract( + Expression::add( + Expression::numberFieldPath('price'), + Expression::numberFieldPath('fee'), + ), + Expression::numberFieldPath('discount'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SubtractSubtractNumbers, $pipeline); + } + + public function testSubtractTwoDates(): void + { + $pipeline = new Pipeline( + Stage::project( + item: 1, + dateDifference: Expression::subtract( + Expression::variable('NOW'), + Expression::dateFieldPath('date'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SubtractSubtractTwoDates, $pipeline); + } +} diff --git a/tests/Builder/Expression/SumOperatorTest.php b/tests/Builder/Expression/SumOperatorTest.php new file mode 100644 index 000000000..923162a20 --- /dev/null +++ b/tests/Builder/Expression/SumOperatorTest.php @@ -0,0 +1,36 @@ +assertSamePipeline(Pipelines::SumUseInProjectStage, $pipeline); + } +} diff --git a/tests/Builder/Expression/SwitchOperatorTest.php b/tests/Builder/Expression/SwitchOperatorTest.php new file mode 100644 index 000000000..1d288ea7a --- /dev/null +++ b/tests/Builder/Expression/SwitchOperatorTest.php @@ -0,0 +1,67 @@ +assertSamePipeline(Pipelines::SwitchExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/TanOperatorTest.php b/tests/Builder/Expression/TanOperatorTest.php new file mode 100644 index 000000000..34b210d0a --- /dev/null +++ b/tests/Builder/Expression/TanOperatorTest.php @@ -0,0 +1,34 @@ +assertSamePipeline(Pipelines::TanExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/TanhOperatorTest.php b/tests/Builder/Expression/TanhOperatorTest.php new file mode 100644 index 000000000..4ae799e55 --- /dev/null +++ b/tests/Builder/Expression/TanhOperatorTest.php @@ -0,0 +1,32 @@ +assertSamePipeline(Pipelines::TanhExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToBoolOperatorTest.php b/tests/Builder/Expression/ToBoolOperatorTest.php new file mode 100644 index 000000000..2b1aad9f9 --- /dev/null +++ b/tests/Builder/Expression/ToBoolOperatorTest.php @@ -0,0 +1,50 @@ +assertSamePipeline(Pipelines::ToBoolExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToDateOperatorTest.php b/tests/Builder/Expression/ToDateOperatorTest.php new file mode 100644 index 000000000..9b03b1121 --- /dev/null +++ b/tests/Builder/Expression/ToDateOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::ToDateExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToDecimalOperatorTest.php b/tests/Builder/Expression/ToDecimalOperatorTest.php new file mode 100644 index 000000000..d12a72755 --- /dev/null +++ b/tests/Builder/Expression/ToDecimalOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::ToDecimalExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToDoubleOperatorTest.php b/tests/Builder/Expression/ToDoubleOperatorTest.php new file mode 100644 index 000000000..d0819e79d --- /dev/null +++ b/tests/Builder/Expression/ToDoubleOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::ToDoubleExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToHashedIndexKeyOperatorTest.php b/tests/Builder/Expression/ToHashedIndexKeyOperatorTest.php new file mode 100644 index 000000000..8bb4bb852 --- /dev/null +++ b/tests/Builder/Expression/ToHashedIndexKeyOperatorTest.php @@ -0,0 +1,34 @@ +assertSamePipeline(Pipelines::ToHashedIndexKeyExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToIntOperatorTest.php b/tests/Builder/Expression/ToIntOperatorTest.php new file mode 100644 index 000000000..cc88ca63d --- /dev/null +++ b/tests/Builder/Expression/ToIntOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::ToIntExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToLongOperatorTest.php b/tests/Builder/Expression/ToLongOperatorTest.php new file mode 100644 index 000000000..53c6f1f07 --- /dev/null +++ b/tests/Builder/Expression/ToLongOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::ToLongExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToLowerOperatorTest.php b/tests/Builder/Expression/ToLowerOperatorTest.php new file mode 100644 index 000000000..6f8cc154d --- /dev/null +++ b/tests/Builder/Expression/ToLowerOperatorTest.php @@ -0,0 +1,32 @@ +assertSamePipeline(Pipelines::ToLowerExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToObjectIdOperatorTest.php b/tests/Builder/Expression/ToObjectIdOperatorTest.php new file mode 100644 index 000000000..396507241 --- /dev/null +++ b/tests/Builder/Expression/ToObjectIdOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::ToObjectIdExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToStringOperatorTest.php b/tests/Builder/Expression/ToStringOperatorTest.php new file mode 100644 index 000000000..d33cb2e3b --- /dev/null +++ b/tests/Builder/Expression/ToStringOperatorTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::ToStringExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ToUpperOperatorTest.php b/tests/Builder/Expression/ToUpperOperatorTest.php new file mode 100644 index 000000000..529892ea1 --- /dev/null +++ b/tests/Builder/Expression/ToUpperOperatorTest.php @@ -0,0 +1,32 @@ +assertSamePipeline(Pipelines::ToUpperExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/TrimOperatorTest.php b/tests/Builder/Expression/TrimOperatorTest.php new file mode 100644 index 000000000..81c2e2353 --- /dev/null +++ b/tests/Builder/Expression/TrimOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::TrimExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/TruncOperatorTest.php b/tests/Builder/Expression/TruncOperatorTest.php new file mode 100644 index 000000000..45f1826be --- /dev/null +++ b/tests/Builder/Expression/TruncOperatorTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::TruncExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/TsIncrementOperatorTest.php b/tests/Builder/Expression/TsIncrementOperatorTest.php new file mode 100644 index 000000000..092f57660 --- /dev/null +++ b/tests/Builder/Expression/TsIncrementOperatorTest.php @@ -0,0 +1,53 @@ +assertSamePipeline(Pipelines::TsIncrementObtainTheIncrementingOrdinalFromATimestampField, $pipeline); + } + + public function testUseTsSecondInAChangeStreamCursorToMonitorCollectionChanges(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::expr( + Expression::eq( + Expression::mod( + Expression::tsIncrement( + Expression::timestampFieldPath('clusterTime'), + ), + 2, + ), + 0, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::TsIncrementUseTsSecondInAChangeStreamCursorToMonitorCollectionChanges, $pipeline); + } +} diff --git a/tests/Builder/Expression/TsSecondOperatorTest.php b/tests/Builder/Expression/TsSecondOperatorTest.php new file mode 100644 index 000000000..16f5b59e6 --- /dev/null +++ b/tests/Builder/Expression/TsSecondOperatorTest.php @@ -0,0 +1,44 @@ +assertSamePipeline(Pipelines::TsSecondObtainTheNumberOfSecondsFromATimestampField, $pipeline); + } + + public function testUseTsSecondInAChangeStreamCursorToMonitorCollectionChanges(): void + { + $pipeline = new Pipeline( + Stage::addFields( + clusterTimeSeconds: Expression::tsSecond( + Expression::timestampFieldPath('clusterTime'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::TsSecondUseTsSecondInAChangeStreamCursorToMonitorCollectionChanges, $pipeline); + } +} diff --git a/tests/Builder/Expression/TypeOperatorTest.php b/tests/Builder/Expression/TypeOperatorTest.php new file mode 100644 index 000000000..797536c27 --- /dev/null +++ b/tests/Builder/Expression/TypeOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::TypeExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/UnsetFieldOperatorTest.php b/tests/Builder/Expression/UnsetFieldOperatorTest.php new file mode 100644 index 000000000..fba80d191 --- /dev/null +++ b/tests/Builder/Expression/UnsetFieldOperatorTest.php @@ -0,0 +1,62 @@ +assertSamePipeline(Pipelines::UnsetFieldRemoveASubfield, $pipeline); + } + + public function testRemoveFieldsThatContainPeriods(): void + { + $pipeline = new Pipeline( + Stage::replaceWith( + Expression::unsetField( + field: 'price.usd', + input: Expression::variable('ROOT'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::UnsetFieldRemoveFieldsThatContainPeriods, $pipeline); + } + + public function testRemoveFieldsThatStartWithADollarSign(): void + { + $pipeline = new Pipeline( + Stage::replaceWith( + Expression::unsetField( + field: Expression::literal('$price'), + input: Expression::variable('ROOT'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::UnsetFieldRemoveFieldsThatStartWithADollarSign, $pipeline); + } +} diff --git a/tests/Builder/Expression/WeekOperatorTest.php b/tests/Builder/Expression/WeekOperatorTest.php new file mode 100644 index 000000000..334cc26ff --- /dev/null +++ b/tests/Builder/Expression/WeekOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::WeekExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/YearOperatorTest.php b/tests/Builder/Expression/YearOperatorTest.php new file mode 100644 index 000000000..a8d02fea6 --- /dev/null +++ b/tests/Builder/Expression/YearOperatorTest.php @@ -0,0 +1,29 @@ +assertSamePipeline(Pipelines::YearExample, $pipeline); + } +} diff --git a/tests/Builder/Expression/ZipOperatorTest.php b/tests/Builder/Expression/ZipOperatorTest.php new file mode 100644 index 000000000..d0039c5d6 --- /dev/null +++ b/tests/Builder/Expression/ZipOperatorTest.php @@ -0,0 +1,58 @@ +assertSamePipeline(Pipelines::ZipFilteringAndPreservingIndexes, $pipeline); + } + + public function testMatrixTransposition(): void + { + $pipeline = new Pipeline( + Stage::project( + _id: false, + transposed: Expression::zip([ + Expression::arrayElemAt(Expression::arrayFieldPath('matrix'), 0), + Expression::arrayElemAt(Expression::arrayFieldPath('matrix'), 1), + Expression::arrayElemAt(Expression::arrayFieldPath('matrix'), 2), + ]), + ), + ); + + $this->assertSamePipeline(Pipelines::ZipMatrixTransposition, $pipeline); + } +} diff --git a/tests/Builder/FieldPathTest.php b/tests/Builder/FieldPathTest.php new file mode 100644 index 000000000..ada1cc164 --- /dev/null +++ b/tests/Builder/FieldPathTest.php @@ -0,0 +1,58 @@ +assertSame('foo', $fieldPath->name); + $this->assertInstanceOf($resolveClass, $fieldPath); + $this->assertInstanceOf(FieldPathInterface::class, $fieldPath); + + // Ensure FieldPath resolves to any type + $this->assertTrue(is_subclass_of(Expression\FieldPath::class, $resolveClass), sprintf('%s instanceof %s', Expression\FieldPath::class, $resolveClass)); + } + + /** @dataProvider provideFieldPath */ + public function testRejectDollarPrefix(string $fieldPathClass): void + { + $this->expectException(InvalidArgumentException::class); + + Expression::{$fieldPathClass}('$foo'); + } + + public function provideFieldPath(): Generator + { + yield 'double' => ['doubleFieldPath', Expression\ResolvesToDouble::class]; + yield 'string' => ['stringFieldPath', Expression\ResolvesToString::class]; + yield 'object' => ['objectFieldPath', Expression\ResolvesToObject::class]; + yield 'array' => ['arrayFieldPath', Expression\ResolvesToArray::class]; + yield 'binData' => ['binDataFieldPath', Expression\ResolvesToBinData::class]; + yield 'objectId' => ['objectIdFieldPath', Expression\ResolvesToObjectId::class]; + yield 'bool' => ['boolFieldPath', Expression\ResolvesToBool::class]; + yield 'date' => ['dateFieldPath', Expression\ResolvesToDate::class]; + yield 'null' => ['nullFieldPath', Expression\ResolvesToNull::class]; + yield 'regex' => ['regexFieldPath', Expression\ResolvesToRegex::class]; + yield 'javascript' => ['javascriptFieldPath', Expression\ResolvesToJavascript::class]; + yield 'int' => ['intFieldPath', Expression\ResolvesToInt::class]; + yield 'timestamp' => ['timestampFieldPath', Expression\ResolvesToTimestamp::class]; + yield 'long' => ['longFieldPath', Expression\ResolvesToLong::class]; + yield 'decimal' => ['decimalFieldPath', Expression\ResolvesToDecimal::class]; + yield 'number' => ['numberFieldPath', Expression\ResolvesToNumber::class]; + yield 'any' => ['fieldPath', Expression\ResolvesToAny::class]; + } +} diff --git a/tests/Builder/FluentPipelineFactoryTest.php b/tests/Builder/FluentPipelineFactoryTest.php new file mode 100644 index 000000000..e1d84a2e4 --- /dev/null +++ b/tests/Builder/FluentPipelineFactoryTest.php @@ -0,0 +1,34 @@ +match(x: Query::eq(1)) + ->project(_id: false, x: true) + ->sort(x: Sort::Asc) + ->getPipeline(); + + $expected = <<<'json' + [ + {"$match": {"x": {"$eq": {"$numberInt": "1"}}}}, + {"$project": {"_id": false, "x": true}}, + {"$sort": {"x": {"$numberInt": "1"}}} + ] + json; + + $this->assertSamePipeline($expected, $pipeline); + } +} diff --git a/tests/Builder/PipelineTest.php b/tests/Builder/PipelineTest.php new file mode 100644 index 000000000..b274fecab --- /dev/null +++ b/tests/Builder/PipelineTest.php @@ -0,0 +1,75 @@ +assertSame([], iterator_to_array($pipeline)); + } + + public function testFromArray(): void + { + $pipeline = new Pipeline( + ['$match' => ['tag' => 'foo']], + [ + ['$sort' => ['_id' => 1]], + ['$skip' => 10], + ], + ['$limit' => 5], + ); + + $expected = [ + ['$match' => ['tag' => 'foo']], + ['$sort' => ['_id' => 1]], + ['$skip' => 10], + ['$limit' => 5], + ]; + + $this->assertSame($expected, iterator_to_array($pipeline)); + } + + public function testMergingPipeline(): void + { + $stages = array_map( + fn (int $i) => $this->createMock(StageInterface::class), + range(0, 7), + ); + + $pipeline = new Pipeline( + $stages[0], + $stages[1], + new Pipeline($stages[2], $stages[3]), + [$stages[4], $stages[5]], + new Pipeline($stages[6]), + [$stages[7]], + ); + + $this->assertSame($stages, iterator_to_array($pipeline)); + } + + public function testRejectNamedArguments(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Named arguments are not supported for pipelines'); + + new Pipeline( + $this->createMock(StageInterface::class), + foo: $this->createMock(StageInterface::class), + ); + } +} diff --git a/tests/Builder/PipelineTestCase.php b/tests/Builder/PipelineTestCase.php new file mode 100644 index 000000000..1975a4ab3 --- /dev/null +++ b/tests/Builder/PipelineTestCase.php @@ -0,0 +1,31 @@ +value; + } + + // BSON Documents doesn't support top-level arrays. + $expected = '{"pipeline":' . $expectedJson . '}'; + + $codec = new BuilderEncoder(); + $actual = $codec->encode($pipeline); + // Normalize with BSON round-trip + $actual = Document::fromPHP(['pipeline' => $actual])->toCanonicalExtendedJSON(); + + self::assertJsonStringEqualsJsonString($expected, $actual); + } +} diff --git a/tests/Builder/Query/AllOperatorTest.php b/tests/Builder/Query/AllOperatorTest.php new file mode 100644 index 000000000..ce0dbe0c8 --- /dev/null +++ b/tests/Builder/Query/AllOperatorTest.php @@ -0,0 +1,51 @@ +assertSamePipeline(Pipelines::AllUseAllToMatchValues, $pipeline); + } + + public function testUseAllWithElemMatch(): void + { + $pipeline = new Pipeline( + Stage::match( + qty: Query::all( + Query::elemMatch( + Query::query( + size: 'M', + num: Query::gt(50), + ), + ), + Query::elemMatch( + Query::query( + num: 100, + color: 'green', + ), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::AllUseAllWithElemMatch, $pipeline); + } +} diff --git a/tests/Builder/Query/AndOperatorTest.php b/tests/Builder/Query/AndOperatorTest.php new file mode 100644 index 000000000..778aa3f97 --- /dev/null +++ b/tests/Builder/Query/AndOperatorTest.php @@ -0,0 +1,62 @@ +assertSamePipeline(Pipelines::AndANDQueriesWithMultipleExpressionsSpecifyingTheSameField, $pipeline); + } + + public function testANDQueriesWithMultipleExpressionsSpecifyingTheSameOperator(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::and( + Query::or( + Query::query( + qty: Query::lt(10), + ), + Query::query( + qty: Query::gt(50), + ), + ), + Query::or( + Query::query( + sale: true, + ), + Query::query( + price: Query::lt(5), + ), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::AndANDQueriesWithMultipleExpressionsSpecifyingTheSameOperator, $pipeline); + } +} diff --git a/tests/Builder/Query/BitsAllClearOperatorTest.php b/tests/Builder/Query/BitsAllClearOperatorTest.php new file mode 100644 index 000000000..ccc670fba --- /dev/null +++ b/tests/Builder/Query/BitsAllClearOperatorTest.php @@ -0,0 +1,54 @@ +assertSamePipeline(Pipelines::BitsAllClearBinDataBitmask, $pipeline); + } + + public function testBitPositionArray(): void + { + $pipeline = new Pipeline( + Stage::match( + a: Query::bitsAllClear([1, 5]), + ), + ); + + $this->assertSamePipeline(Pipelines::BitsAllClearBitPositionArray, $pipeline); + } + + public function testIntegerBitmask(): void + { + $pipeline = new Pipeline( + Stage::match( + a: Query::bitsAllClear(35), + ), + ); + + $this->assertSamePipeline(Pipelines::BitsAllClearIntegerBitmask, $pipeline); + } +} diff --git a/tests/Builder/Query/BitsAllSetOperatorTest.php b/tests/Builder/Query/BitsAllSetOperatorTest.php new file mode 100644 index 000000000..c1eaa0e47 --- /dev/null +++ b/tests/Builder/Query/BitsAllSetOperatorTest.php @@ -0,0 +1,54 @@ +assertSamePipeline(Pipelines::BitsAllSetBinDataBitmask, $pipeline); + } + + public function testBitPositionArray(): void + { + $pipeline = new Pipeline( + Stage::match( + a: Query::bitsAllSet([1, 5]), + ), + ); + + $this->assertSamePipeline(Pipelines::BitsAllSetBitPositionArray, $pipeline); + } + + public function testIntegerBitmask(): void + { + $pipeline = new Pipeline( + Stage::match( + a: Query::bitsAllSet(50), + ), + ); + + $this->assertSamePipeline(Pipelines::BitsAllSetIntegerBitmask, $pipeline); + } +} diff --git a/tests/Builder/Query/BitsAnyClearOperatorTest.php b/tests/Builder/Query/BitsAnyClearOperatorTest.php new file mode 100644 index 000000000..3f32fdb86 --- /dev/null +++ b/tests/Builder/Query/BitsAnyClearOperatorTest.php @@ -0,0 +1,54 @@ +assertSamePipeline(Pipelines::BitsAnyClearBinDataBitmask, $pipeline); + } + + public function testBitPositionArray(): void + { + $pipeline = new Pipeline( + Stage::match( + a: Query::bitsAnyClear([1, 5]), + ), + ); + + $this->assertSamePipeline(Pipelines::BitsAnyClearBitPositionArray, $pipeline); + } + + public function testIntegerBitmask(): void + { + $pipeline = new Pipeline( + Stage::match( + a: Query::bitsAnyClear(35), + ), + ); + + $this->assertSamePipeline(Pipelines::BitsAnyClearIntegerBitmask, $pipeline); + } +} diff --git a/tests/Builder/Query/BitsAnySetOperatorTest.php b/tests/Builder/Query/BitsAnySetOperatorTest.php new file mode 100644 index 000000000..1d90c6893 --- /dev/null +++ b/tests/Builder/Query/BitsAnySetOperatorTest.php @@ -0,0 +1,54 @@ +assertSamePipeline(Pipelines::BitsAnySetBinDataBitmask, $pipeline); + } + + public function testBitPositionArray(): void + { + $pipeline = new Pipeline( + Stage::match( + a: Query::bitsAnySet([1, 5]), + ), + ); + + $this->assertSamePipeline(Pipelines::BitsAnySetBitPositionArray, $pipeline); + } + + public function testIntegerBitmask(): void + { + $pipeline = new Pipeline( + Stage::match( + a: Query::bitsAnySet(35), + ), + ); + + $this->assertSamePipeline(Pipelines::BitsAnySetIntegerBitmask, $pipeline); + } +} diff --git a/tests/Builder/Query/CommentOperatorTest.php b/tests/Builder/Query/CommentOperatorTest.php new file mode 100644 index 000000000..4b94bb56b --- /dev/null +++ b/tests/Builder/Query/CommentOperatorTest.php @@ -0,0 +1,39 @@ +assertSamePipeline(Pipelines::CommentAttachACommentToAnAggregationExpression, $pipeline); + } +} diff --git a/tests/Builder/Query/ElemMatchOperatorTest.php b/tests/Builder/Query/ElemMatchOperatorTest.php new file mode 100644 index 000000000..b179df446 --- /dev/null +++ b/tests/Builder/Query/ElemMatchOperatorTest.php @@ -0,0 +1,92 @@ +assertSamePipeline(Pipelines::ElemMatchArrayOfEmbeddedDocuments, $pipeline); + } + + public function testElementMatch(): void + { + $pipeline = new Pipeline( + Stage::match( + results: Query::elemMatch( + Query::fieldQuery( + Query::gte(80), + Query::lt(85), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ElemMatchElementMatch, $pipeline); + } + + public function testSingleFieldOperator(): void + { + $pipeline = new Pipeline( + Stage::match( + results: Query::elemMatch( + Query::gt(10), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ElemMatchSingleFieldOperator, $pipeline); + } + + public function testSingleQueryCondition(): void + { + $pipeline = new Pipeline( + Stage::match( + results: Query::elemMatch( + Query::query( + product: Query::ne('xyz'), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ElemMatchSingleQueryCondition, $pipeline); + } + + public function testUsingOrWithElemMatch(): void + { + $pipeline = new Pipeline( + Stage::match( + game: Query::elemMatch( + Query::or( + Query::query(score: Query::gt(10)), + Query::query(score: Query::lt(5)), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ElemMatchUsingOrWithElemMatch, $pipeline); + } +} diff --git a/tests/Builder/Query/EqOperatorTest.php b/tests/Builder/Query/EqOperatorTest.php new file mode 100644 index 000000000..813c780ac --- /dev/null +++ b/tests/Builder/Query/EqOperatorTest.php @@ -0,0 +1,70 @@ +assertSamePipeline(Pipelines::EqEqualsASpecifiedValue, $pipeline); + } + + public function testEqualsAnArrayValue(): void + { + $pipeline = new Pipeline( + Stage::match( + tags: Query::eq(['A', 'B']), + ), + ); + + $this->assertSamePipeline(Pipelines::EqEqualsAnArrayValue, $pipeline); + } + + public function testFieldInEmbeddedDocumentEqualsAValue(): void + { + $pipeline = new Pipeline( + Stage::match( + ...['item.name' => Query::eq('ab')], + ), + ); + + $this->assertSamePipeline(Pipelines::EqFieldInEmbeddedDocumentEqualsAValue, $pipeline); + } + + public function testRegexMatchBehaviour(): void + { + $pipeline = new Pipeline( + Stage::match( + company: 'MongoDB', + ), + Stage::match( + company: Query::eq('MongoDB'), + ), + Stage::match( + company: new Regex('^MongoDB'), + ), + Stage::match( + company: Query::eq(new Regex('^MongoDB')), + ), + ); + + $this->assertSamePipeline(Pipelines::EqRegexMatchBehaviour, $pipeline); + } +} diff --git a/tests/Builder/Query/ExistsOperatorTest.php b/tests/Builder/Query/ExistsOperatorTest.php new file mode 100644 index 000000000..27ebeec77 --- /dev/null +++ b/tests/Builder/Query/ExistsOperatorTest.php @@ -0,0 +1,52 @@ +assertSamePipeline(Pipelines::ExistsExistsAndNotEqualTo, $pipeline); + } + + public function testMissingField(): void + { + $pipeline = new Pipeline( + Stage::match( + qty: Query::exists(false), + ), + ); + + $this->assertSamePipeline(Pipelines::ExistsMissingField, $pipeline); + } + + public function testNullValues(): void + { + $pipeline = new Pipeline( + Stage::match( + qty: Query::exists(), + ), + ); + + $this->assertSamePipeline(Pipelines::ExistsNullValues, $pipeline); + } +} diff --git a/tests/Builder/Query/ExprOperatorTest.php b/tests/Builder/Query/ExprOperatorTest.php new file mode 100644 index 000000000..3b87fc35b --- /dev/null +++ b/tests/Builder/Query/ExprOperatorTest.php @@ -0,0 +1,58 @@ +assertSamePipeline(Pipelines::ExprCompareTwoFieldsFromASingleDocument, $pipeline); + } + + public function testUsingExprWithConditionalStatements(): void + { + $discountedPrice = Expression::cond( + if: Expression::gte(Expression::fieldPath('qty'), 100), + then: Expression::multiply( + Expression::numberfieldPath('price'), + 0.5, + ), + else: Expression::multiply( + Expression::numberfieldPath('price'), + 0.75, + ), + ); + + $pipeline = new Pipeline( + Stage::match( + Query::expr( + Expression::lt($discountedPrice, 5), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ExprUsingExprWithConditionalStatements, $pipeline); + } +} diff --git a/tests/Builder/Query/GeoIntersectsOperatorTest.php b/tests/Builder/Query/GeoIntersectsOperatorTest.php new file mode 100644 index 000000000..92fc88c83 --- /dev/null +++ b/tests/Builder/Query/GeoIntersectsOperatorTest.php @@ -0,0 +1,56 @@ +assertSamePipeline(Pipelines::GeoIntersectsIntersectsABigPolygon, $pipeline); + } + + public function testIntersectsAPolygon(): void + { + $pipeline = new Pipeline( + Stage::match( + loc: Query::geoIntersects( + Query::geometry( + type: 'Polygon', + coordinates: [[[0, 0], [3, 6], [6, 1], [0, 0]]], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::GeoIntersectsIntersectsAPolygon, $pipeline); + } +} diff --git a/tests/Builder/Query/GeoWithinOperatorTest.php b/tests/Builder/Query/GeoWithinOperatorTest.php new file mode 100644 index 000000000..d510b6cea --- /dev/null +++ b/tests/Builder/Query/GeoWithinOperatorTest.php @@ -0,0 +1,56 @@ +assertSamePipeline(Pipelines::GeoWithinWithinABigPolygon, $pipeline); + } + + public function testWithinAPolygon(): void + { + $pipeline = new Pipeline( + Stage::match( + loc: Query::geoWithin( + Query::geometry( + type: 'Polygon', + coordinates: [[[0, 0], [3, 6], [6, 1], [0, 0]]], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::GeoWithinWithinAPolygon, $pipeline); + } +} diff --git a/tests/Builder/Query/GtOperatorTest.php b/tests/Builder/Query/GtOperatorTest.php new file mode 100644 index 000000000..c2838d00c --- /dev/null +++ b/tests/Builder/Query/GtOperatorTest.php @@ -0,0 +1,27 @@ +assertSamePipeline(Pipelines::GtMatchDocumentFields, $pipeline); + } +} diff --git a/tests/Builder/Query/GteOperatorTest.php b/tests/Builder/Query/GteOperatorTest.php new file mode 100644 index 000000000..48198adf8 --- /dev/null +++ b/tests/Builder/Query/GteOperatorTest.php @@ -0,0 +1,27 @@ +assertSamePipeline(Pipelines::GteMatchDocumentFields, $pipeline); + } +} diff --git a/tests/Builder/Query/InOperatorTest.php b/tests/Builder/Query/InOperatorTest.php new file mode 100644 index 000000000..25a9001fd --- /dev/null +++ b/tests/Builder/Query/InOperatorTest.php @@ -0,0 +1,39 @@ +assertSamePipeline(Pipelines::InUseTheInOperatorToMatchValuesInAnArray, $pipeline); + } + + public function testUseTheInOperatorWithARegularExpression(): void + { + $pipeline = new Pipeline( + Stage::match( + tags: Query::in([new Regex('^be'), new Regex('^st')]), + ), + ); + + $this->assertSamePipeline(Pipelines::InUseTheInOperatorWithARegularExpression, $pipeline); + } +} diff --git a/tests/Builder/Query/JsonSchemaOperatorTest.php b/tests/Builder/Query/JsonSchemaOperatorTest.php new file mode 100644 index 000000000..0d4b1a92f --- /dev/null +++ b/tests/Builder/Query/JsonSchemaOperatorTest.php @@ -0,0 +1,45 @@ +assertSamePipeline(Pipelines::JsonSchemaExample, $pipeline); + } +} diff --git a/tests/Builder/Query/LtOperatorTest.php b/tests/Builder/Query/LtOperatorTest.php new file mode 100644 index 000000000..119f08a4b --- /dev/null +++ b/tests/Builder/Query/LtOperatorTest.php @@ -0,0 +1,27 @@ +assertSamePipeline(Pipelines::LtMatchDocumentFields, $pipeline); + } +} diff --git a/tests/Builder/Query/LteOperatorTest.php b/tests/Builder/Query/LteOperatorTest.php new file mode 100644 index 000000000..5a0d5da7f --- /dev/null +++ b/tests/Builder/Query/LteOperatorTest.php @@ -0,0 +1,27 @@ +assertSamePipeline(Pipelines::LteMatchDocumentFields, $pipeline); + } +} diff --git a/tests/Builder/Query/ModOperatorTest.php b/tests/Builder/Query/ModOperatorTest.php new file mode 100644 index 000000000..e4bbf90dc --- /dev/null +++ b/tests/Builder/Query/ModOperatorTest.php @@ -0,0 +1,44 @@ +assertSamePipeline(Pipelines::ModFloatingPointArguments, $pipeline); + } + + public function testUseModToSelectDocuments(): void + { + $pipeline = new Pipeline( + Stage::match( + qty: Query::mod(4, 0), + ), + ); + + $this->assertSamePipeline(Pipelines::ModUseModToSelectDocuments, $pipeline); + } +} diff --git a/tests/Builder/Query/NeOperatorTest.php b/tests/Builder/Query/NeOperatorTest.php new file mode 100644 index 000000000..dd6196293 --- /dev/null +++ b/tests/Builder/Query/NeOperatorTest.php @@ -0,0 +1,27 @@ +assertSamePipeline(Pipelines::NeMatchDocumentFields, $pipeline); + } +} diff --git a/tests/Builder/Query/NearOperatorTest.php b/tests/Builder/Query/NearOperatorTest.php new file mode 100644 index 000000000..3ca7a16c7 --- /dev/null +++ b/tests/Builder/Query/NearOperatorTest.php @@ -0,0 +1,34 @@ +assertSamePipeline(Pipelines::NearQueryOnGeoJSONData, $pipeline); + } +} diff --git a/tests/Builder/Query/NearSphereOperatorTest.php b/tests/Builder/Query/NearSphereOperatorTest.php new file mode 100644 index 000000000..40ef97a6c --- /dev/null +++ b/tests/Builder/Query/NearSphereOperatorTest.php @@ -0,0 +1,34 @@ +assertSamePipeline(Pipelines::NearSphereSpecifyCenterPointUsingGeoJSON, $pipeline); + } +} diff --git a/tests/Builder/Query/NinOperatorTest.php b/tests/Builder/Query/NinOperatorTest.php new file mode 100644 index 000000000..04799b791 --- /dev/null +++ b/tests/Builder/Query/NinOperatorTest.php @@ -0,0 +1,38 @@ +assertSamePipeline(Pipelines::NinSelectOnElementsNotInAnArray, $pipeline); + } + + public function testSelectOnUnmatchingDocuments(): void + { + $pipeline = new Pipeline( + Stage::match( + quantity: Query::nin([5, 15]), + ), + ); + + $this->assertSamePipeline(Pipelines::NinSelectOnUnmatchingDocuments, $pipeline); + } +} diff --git a/tests/Builder/Query/NorOperatorTest.php b/tests/Builder/Query/NorOperatorTest.php new file mode 100644 index 000000000..db03502dc --- /dev/null +++ b/tests/Builder/Query/NorOperatorTest.php @@ -0,0 +1,79 @@ +assertSamePipeline(Pipelines::NorAdditionalComparisons, $pipeline); + } + + public function testNorAndExists(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::nor( + Query::query( + price: 1.99, + ), + Query::query( + price: Query::exists(false), + ), + Query::query( + sale: true, + ), + Query::query( + sale: Query::exists(false), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::NorNorAndExists, $pipeline); + } + + public function testQueryWithTwoExpressions(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::nor( + Query::query( + price: 1.99, + ), + Query::query( + sale: true, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::NorQueryWithTwoExpressions, $pipeline); + } +} diff --git a/tests/Builder/Query/NotOperatorTest.php b/tests/Builder/Query/NotOperatorTest.php new file mode 100644 index 000000000..e69062f9c --- /dev/null +++ b/tests/Builder/Query/NotOperatorTest.php @@ -0,0 +1,43 @@ +assertSamePipeline(Pipelines::NotRegularExpressions, $pipeline); + } + + public function testSyntax(): void + { + $pipeline = new Pipeline( + Stage::match( + price: Query::not( + Query::gt(1.99), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::NotSyntax, $pipeline); + } +} diff --git a/tests/Builder/Query/OrOperatorTest.php b/tests/Builder/Query/OrOperatorTest.php new file mode 100644 index 000000000..3ad68b20b --- /dev/null +++ b/tests/Builder/Query/OrOperatorTest.php @@ -0,0 +1,59 @@ +assertSamePipeline(Pipelines::OrErrorHandling, $pipeline); + } + + public function testOrClauses(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::or( + Query::query( + quantity: Query::lt(20), + ), + Query::query( + price: 10, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::OrOrClauses, $pipeline); + } +} diff --git a/tests/Builder/Query/Pipelines.php b/tests/Builder/Query/Pipelines.php new file mode 100644 index 000000000..78611649d --- /dev/null +++ b/tests/Builder/Query/Pipelines.php @@ -0,0 +1,2073 @@ +assertSamePipeline(Pipelines::RandSelectRandomItemsFromACollection, $pipeline); + } +} diff --git a/tests/Builder/Query/RegexOperatorTest.php b/tests/Builder/Query/RegexOperatorTest.php new file mode 100644 index 000000000..6f9109387 --- /dev/null +++ b/tests/Builder/Query/RegexOperatorTest.php @@ -0,0 +1,38 @@ +assertSamePipeline(Pipelines::RegexPerformALIKEMatch, $pipeline); + } + + public function testPerformCaseInsensitiveRegularExpressionMatch(): void + { + $pipeline = new Pipeline( + Stage::match( + sku: Query::regex('^ABC', 'i'), + ), + ); + + $this->assertSamePipeline(Pipelines::RegexPerformCaseInsensitiveRegularExpressionMatch, $pipeline); + } +} diff --git a/tests/Builder/Query/SampleRateOperatorTest.php b/tests/Builder/Query/SampleRateOperatorTest.php new file mode 100644 index 000000000..95abec400 --- /dev/null +++ b/tests/Builder/Query/SampleRateOperatorTest.php @@ -0,0 +1,28 @@ +assertSamePipeline(Pipelines::SampleRateExample, $pipeline); + } +} diff --git a/tests/Builder/Query/SizeOperatorTest.php b/tests/Builder/Query/SizeOperatorTest.php new file mode 100644 index 000000000..ea612dbe7 --- /dev/null +++ b/tests/Builder/Query/SizeOperatorTest.php @@ -0,0 +1,27 @@ +assertSamePipeline(Pipelines::SizeQueryAnArrayByArrayLength, $pipeline); + } +} diff --git a/tests/Builder/Query/TextOperatorTest.php b/tests/Builder/Query/TextOperatorTest.php new file mode 100644 index 000000000..813c5460c --- /dev/null +++ b/tests/Builder/Query/TextOperatorTest.php @@ -0,0 +1,120 @@ +assertSamePipeline(Pipelines::TextCaseAndDiacriticInsensitiveSearch, $pipeline); + } + + public function testDiacriticSensitiveSearch(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::text( + search: 'CAFÉ', + diacriticSensitive: true, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::TextDiacriticSensitiveSearch, $pipeline); + } + + public function testMatchAnyOfTheSearchTerms(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::text('bake coffee cake'), + ), + ); + + $this->assertSamePipeline(Pipelines::TextMatchAnyOfTheSearchTerms, $pipeline); + } + + public function testPerformCaseSensitiveSearch(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::text( + search: 'Coffee', + caseSensitive: true, + ), + ), + Stage::match( + Query::text( + search: '\"Café Con Leche\"', + caseSensitive: true, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::TextPerformCaseSensitiveSearch, $pipeline); + } + + public function testSearchADifferentLanguage(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::text( + search: 'leche', + language: 'es', + ), + ), + ); + + $this->assertSamePipeline(Pipelines::TextSearchADifferentLanguage, $pipeline); + } + + public function testSearchForASingleWord(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::text('coffee'), + ), + ); + + $this->assertSamePipeline(Pipelines::TextSearchForASingleWord, $pipeline); + } + + public function testTextSearchScoreExamples(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::text( + search: 'CAFÉ', + diacriticSensitive: true, + ), + ), + Stage::project( + score: Expression::meta('textScore'), + ), + Stage::sort( + score: Sort::TextScore, + ), + Stage::limit(5), + ); + + $this->assertSamePipeline(Pipelines::TextTextSearchScoreExamples, $pipeline); + } +} diff --git a/tests/Builder/Query/TypeOperatorTest.php b/tests/Builder/Query/TypeOperatorTest.php new file mode 100644 index 000000000..21d727414 --- /dev/null +++ b/tests/Builder/Query/TypeOperatorTest.php @@ -0,0 +1,78 @@ +assertSamePipeline(Pipelines::TypeQueryingByArrayType, $pipeline); + } + + public function testQueryingByDataType(): void + { + $pipeline = new Pipeline( + Stage::match( + zipCode: Query::type(2), + ), + Stage::match( + zipCode: Query::type('string'), + ), + Stage::match( + zipCode: Query::type(1), + ), + Stage::match( + zipCode: Query::type('double'), + ), + Stage::match( + zipCode: Query::type('number'), + ), + ); + + $this->assertSamePipeline(Pipelines::TypeQueryingByDataType, $pipeline); + } + + public function testQueryingByMinKeyAndMaxKey(): void + { + $pipeline = new Pipeline( + Stage::match( + zipCode: Query::type('minKey'), + ), + Stage::match( + zipCode: Query::type('maxKey'), + ), + ); + + $this->assertSamePipeline(Pipelines::TypeQueryingByMinKeyAndMaxKey, $pipeline); + } + + public function testQueryingByMultipleDataType(): void + { + $pipeline = new Pipeline( + Stage::match( + zipCode: Query::type(2, 1), + ), + Stage::match( + zipCode: Query::type('string', 'double'), + ), + ); + + $this->assertSamePipeline(Pipelines::TypeQueryingByMultipleDataType, $pipeline); + } +} diff --git a/tests/Builder/Query/WhereOperatorTest.php b/tests/Builder/Query/WhereOperatorTest.php new file mode 100644 index 000000000..ed87faa4a --- /dev/null +++ b/tests/Builder/Query/WhereOperatorTest.php @@ -0,0 +1,45 @@ +assertSamePipeline(Pipelines::WhereExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/AddFieldsStageTest.php b/tests/Builder/Stage/AddFieldsStageTest.php new file mode 100644 index 000000000..970471ba2 --- /dev/null +++ b/tests/Builder/Stage/AddFieldsStageTest.php @@ -0,0 +1,74 @@ +assertSamePipeline(Pipelines::AddFieldsAddElementToAnArray, $pipeline); + } + + public function testAddingFieldsToAnEmbeddedDocument(): void + { + $pipeline = new Pipeline( + Stage::addFields( + ...['specs.fuel_type' => 'unleaded'], + ), + ); + + $this->assertSamePipeline(Pipelines::AddFieldsAddingFieldsToAnEmbeddedDocument, $pipeline); + } + + public function testOverwritingAnExistingField(): void + { + $pipeline = new Pipeline( + Stage::addFields( + cats: 20, + ), + ); + + $this->assertSamePipeline(Pipelines::AddFieldsOverwritingAnExistingField, $pipeline); + } + + public function testUsingTwoAddFieldsStages(): void + { + $pipeline = new Pipeline( + Stage::addFields( + totalHomework: Expression::sum(Expression::fieldPath('homework')), + totalQuiz: Expression::sum(Expression::fieldPath('quiz')), + ), + Stage::addFields( + totalScore: Expression::add( + Expression::fieldPath('totalHomework'), + Expression::fieldPath('totalQuiz'), + Expression::fieldPath('extraCredit'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::AddFieldsUsingTwoAddFieldsStages, $pipeline); + } +} diff --git a/tests/Builder/Stage/BucketAutoStageTest.php b/tests/Builder/Stage/BucketAutoStageTest.php new file mode 100644 index 000000000..ecae48f20 --- /dev/null +++ b/tests/Builder/Stage/BucketAutoStageTest.php @@ -0,0 +1,28 @@ +assertSamePipeline(Pipelines::BucketAutoSingleFacetAggregation, $pipeline); + } +} diff --git a/tests/Builder/Stage/BucketStageTest.php b/tests/Builder/Stage/BucketStageTest.php new file mode 100644 index 000000000..95119a5e3 --- /dev/null +++ b/tests/Builder/Stage/BucketStageTest.php @@ -0,0 +1,94 @@ +assertSamePipeline(Pipelines::BucketBucketByYearAndFilterByBucketResults, $pipeline); + } + + public function testUseBucketWithFacetToBucketByMultipleFields(): void + { + $pipeline = new Pipeline( + Stage::facet( + price: new Pipeline( + Stage::bucket( + groupBy: Expression::numberFieldPath('price'), + boundaries: [0, 200, 400], + default: 'Other', + output: object( + count: Accumulator::sum(1), + artwork: Accumulator::push( + object( + title: Expression::stringFieldPath('title'), + price: Expression::stringFieldPath('price'), + ), + ), + averagePrice: Accumulator::avg( + Expression::numberFieldPath('price'), + ), + ), + ), + ), + year: new Pipeline( + Stage::bucket( + groupBy: Expression::stringFieldPath('year'), + boundaries: [1890, 1910, 1920, 1940], + default: 'Unknown', + output: object( + count: Accumulator::sum(1), + artwork: Accumulator::push( + object( + title: Expression::stringFieldPath('title'), + year: Expression::stringFieldPath('year'), + ), + ), + ), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::BucketUseBucketWithFacetToBucketByMultipleFields, $pipeline); + } +} diff --git a/tests/Builder/Stage/ChangeStreamSplitLargeEventStageTest.php b/tests/Builder/Stage/ChangeStreamSplitLargeEventStageTest.php new file mode 100644 index 000000000..c7652e2de --- /dev/null +++ b/tests/Builder/Stage/ChangeStreamSplitLargeEventStageTest.php @@ -0,0 +1,24 @@ +assertSamePipeline(Pipelines::ChangeStreamSplitLargeEventExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/ChangeStreamStageTest.php b/tests/Builder/Stage/ChangeStreamStageTest.php new file mode 100644 index 000000000..03a5bf523 --- /dev/null +++ b/tests/Builder/Stage/ChangeStreamStageTest.php @@ -0,0 +1,24 @@ +assertSamePipeline(Pipelines::ChangeStreamExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/CollStatsStageTest.php b/tests/Builder/Stage/CollStatsStageTest.php new file mode 100644 index 000000000..5b9277d27 --- /dev/null +++ b/tests/Builder/Stage/CollStatsStageTest.php @@ -0,0 +1,63 @@ +assertSamePipeline(Pipelines::CollStatsCountField, $pipeline); + } + + public function testLatencyStatsDocument(): void + { + $pipeline = new Pipeline( + Stage::collStats( + latencyStats: object( + histograms: true, + ), + ), + ); + + $this->assertSamePipeline(Pipelines::CollStatsLatencyStatsDocument, $pipeline); + } + + public function testQueryExecStatsDocument(): void + { + $pipeline = new Pipeline( + Stage::collStats( + queryExecStats: object(), + ), + ); + + $this->assertSamePipeline(Pipelines::CollStatsQueryExecStatsDocument, $pipeline); + } + + public function testStorageStatsDocument(): void + { + $pipeline = new Pipeline( + Stage::collStats( + storageStats: object(), + ), + ); + + $this->assertSamePipeline(Pipelines::CollStatsStorageStatsDocument, $pipeline); + } +} diff --git a/tests/Builder/Stage/CountStageTest.php b/tests/Builder/Stage/CountStageTest.php new file mode 100644 index 000000000..68c45c52e --- /dev/null +++ b/tests/Builder/Stage/CountStageTest.php @@ -0,0 +1,28 @@ +assertSamePipeline(Pipelines::CountExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/CurrentOpStageTest.php b/tests/Builder/Stage/CurrentOpStageTest.php new file mode 100644 index 000000000..6e50cb0b9 --- /dev/null +++ b/tests/Builder/Stage/CurrentOpStageTest.php @@ -0,0 +1,47 @@ +assertSamePipeline(Pipelines::CurrentOpInactiveSessions, $pipeline); + } + + public function testSampledQueries(): void + { + $pipeline = new Pipeline( + Stage::currentOp( + allUsers: true, + localOps: true, + ), + Stage::match( + desc: 'query analyzer', + ), + ); + + $this->assertSamePipeline(Pipelines::CurrentOpSampledQueries, $pipeline); + } +} diff --git a/tests/Builder/Stage/DensifyStageTest.php b/tests/Builder/Stage/DensifyStageTest.php new file mode 100644 index 000000000..142233fcf --- /dev/null +++ b/tests/Builder/Stage/DensifyStageTest.php @@ -0,0 +1,55 @@ +assertSamePipeline(Pipelines::DensifyDensifictionWithPartitions, $pipeline); + } + + public function testDensifyTimeSeriesData(): void + { + $pipeline = new Pipeline( + Stage::densify( + field: 'timestamp', + range: object( + step: 1, + unit: TimeUnit::Hour, + bounds: [ + new UTCDateTime(new DateTimeImmutable('2021-05-18T00:00:00.000Z')), + new UTCDateTime(new DateTimeImmutable('2021-05-18T08:00:00.000Z')), + ], + ), + ), + ); + + $this->assertSamePipeline(Pipelines::DensifyDensifyTimeSeriesData, $pipeline); + } +} diff --git a/tests/Builder/Stage/DocumentsStageTest.php b/tests/Builder/Stage/DocumentsStageTest.php new file mode 100644 index 000000000..e0b2e4ce0 --- /dev/null +++ b/tests/Builder/Stage/DocumentsStageTest.php @@ -0,0 +1,56 @@ +assertSamePipeline(Pipelines::DocumentsTestAPipelineStage, $pipeline); + } + + public function testUseADocumentsStageInALookupStage(): void + { + $pipeline = new Pipeline( + Stage::match(), + Stage::lookup( + localField: 'zip', + foreignField: 'zip_id', + as: 'city_state', + pipeline: new Pipeline( + Stage::documents([ + Document::fromPHP(object(zip_id: 94301, name: 'Palo Alto, CA')), + Document::fromPHP(object(zip_id: 10019, name: 'New York, NY')), + ]), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::DocumentsUseADocumentsStageInALookupStage, $pipeline); + } +} diff --git a/tests/Builder/Stage/FacetStageTest.php b/tests/Builder/Stage/FacetStageTest.php new file mode 100644 index 000000000..121131617 --- /dev/null +++ b/tests/Builder/Stage/FacetStageTest.php @@ -0,0 +1,62 @@ + new Pipeline( + Stage::bucketAuto( + groupBy: Expression::stringFieldPath('year'), + buckets: 4, + ), + ), + ], + categorizedByTags: new Pipeline( + Stage::unwind( + Expression::arrayFieldPath('tags'), + ), + Stage::sortByCount( + Expression::arrayFieldPath('tags'), + ), + ), + categorizedByPrice: new Pipeline( + Stage::match( + price: Query::exists(), + ), + Stage::bucket( + groupBy: Expression::numberFieldPath('price'), + boundaries: [0, 150, 200, 300, 400], + default: 'Other', + output: object( + count: Accumulator::sum(1), + titles: Accumulator::push( + Expression::stringFieldPath('title'), + ), + ), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FacetExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/FillStageTest.php b/tests/Builder/Stage/FillStageTest.php new file mode 100644 index 000000000..e464cfaef --- /dev/null +++ b/tests/Builder/Stage/FillStageTest.php @@ -0,0 +1,111 @@ +assertSamePipeline(Pipelines::FillFillDataForDistinctPartitions, $pipeline); + } + + public function testFillMissingFieldValuesBasedOnTheLastObservedValue(): void + { + $pipeline = new Pipeline( + Stage::fill( + sortBy: object( + date: Sort::Asc, + ), + output: object( + score: object(method: 'locf'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FillFillMissingFieldValuesBasedOnTheLastObservedValue, $pipeline); + } + + public function testFillMissingFieldValuesWithAConstantValue(): void + { + $pipeline = new Pipeline( + Stage::fill( + output: object( + bootsSold: object(value: 0), + sandalsSold: object(value: 0), + sneakersSold: object(value: 0), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FillFillMissingFieldValuesWithAConstantValue, $pipeline); + } + + public function testFillMissingFieldValuesWithLinearInterpolation(): void + { + $pipeline = new Pipeline( + Stage::fill( + sortBy: object( + time: Sort::Asc, + ), + output: object( + price: object(method: 'linear'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FillFillMissingFieldValuesWithLinearInterpolation, $pipeline); + } + + public function testIndicateIfAFieldWasPopulatedUsingFill(): void + { + $pipeline = new Pipeline( + Stage::set( + valueExisted: Expression::ifNull( + Expression::toBool( + Expression::toString( + Expression::fieldPath('score'), + ), + ), + false, + ), + ), + Stage::fill( + sortBy: object( + date: Sort::Asc, + ), + output: object( + score: object(method: 'locf'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::FillIndicateIfAFieldWasPopulatedUsingFill, $pipeline); + } +} diff --git a/tests/Builder/Stage/GeoNearStageTest.php b/tests/Builder/Stage/GeoNearStageTest.php new file mode 100644 index 000000000..93acfc277 --- /dev/null +++ b/tests/Builder/Stage/GeoNearStageTest.php @@ -0,0 +1,123 @@ +assertSamePipeline(Pipelines::GeoNearMaximumDistance, $pipeline); + } + + public function testMinimumDistance(): void + { + $pipeline = new Pipeline( + Stage::geoNear( + near: object( + type: 'Point', + coordinates: [-73.99279, 40.719296], + ), + distanceField: 'dist.calculated', + minDistance: 2, + query: Query::query( + category: 'Parks', + ), + includeLocs: 'dist.location', + spherical: true, + ), + ); + + $this->assertSamePipeline(Pipelines::GeoNearMinimumDistance, $pipeline); + } + + public function testSpecifyWhichGeospatialIndexToUse(): void + { + $pipeline = new Pipeline( + Stage::geoNear( + near: object( + type: 'Point', + coordinates: [-73.98142, 40.71782], + ), + key: 'location', + distanceField: 'dist.calculated', + query: Query::query( + category: 'Parks', + ), + ), + Stage::limit(5), + ); + + $this->assertSamePipeline(Pipelines::GeoNearSpecifyWhichGeospatialIndexToUse, $pipeline); + } + + public function testWithBoundLetOption(): void + { + $pipeline = new Pipeline( + Stage::lookup( + from: 'places', + let: object( + pt: Expression::stringFieldPath('location'), + ), + pipeline: new Pipeline( + Stage::geoNear( + near: Expression::variable('pt'), + distanceField: 'distance', + ), + ), + as: 'joinedField', + ), + Stage::match( + name: 'Sara D. Roosevelt Park', + ), + ); + + $this->assertSamePipeline(Pipelines::GeoNearWithBoundLetOption, $pipeline); + } + + public function testWithTheLetOption(): void + { + $pipeline = new Pipeline( + Stage::geoNear( + near: Expression::variable('pt'), + distanceField: 'distance', + maxDistance: 2, + query: Query::query( + category: 'Parks', + ), + includeLocs: 'dist.location', + spherical: true, + ), + ); + + $this->assertSamePipeline(Pipelines::GeoNearWithTheLetOption, $pipeline); + } +} diff --git a/tests/Builder/Stage/GraphLookupStageTest.php b/tests/Builder/Stage/GraphLookupStageTest.php new file mode 100644 index 000000000..fd4a97b4c --- /dev/null +++ b/tests/Builder/Stage/GraphLookupStageTest.php @@ -0,0 +1,77 @@ +assertSamePipeline(Pipelines::GraphLookupAcrossMultipleCollections, $pipeline); + } + + public function testWithAQueryFilter(): void + { + $pipeline = new Pipeline( + Stage::match( + name: 'Tanya Jordan', + ), + Stage::graphLookup( + from: 'people', + startWith: Expression::stringFieldPath('friends'), + connectFromField: 'friends', + connectToField: 'name', + as: 'golfers', + restrictSearchWithMatch: Query::query( + hobbies: 'golf', + ), + ), + Stage::project( + ...[ + 'connections who play golf' => Expression::stringFieldPath('golfers.name'), + ], + name: 1, + friends: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::GraphLookupWithAQueryFilter, $pipeline); + } + + public function testWithinASingleCollection(): void + { + $pipeline = new Pipeline( + Stage::graphLookup( + from: 'employees', + startWith: Expression::stringFieldPath('reportsTo'), + connectFromField: 'reportsTo', + connectToField: 'name', + as: 'reportingHierarchy', + ), + ); + + $this->assertSamePipeline(Pipelines::GraphLookupWithinASingleCollection, $pipeline); + } +} diff --git a/tests/Builder/Stage/GroupStageTest.php b/tests/Builder/Stage/GroupStageTest.php new file mode 100644 index 000000000..ca742312f --- /dev/null +++ b/tests/Builder/Stage/GroupStageTest.php @@ -0,0 +1,148 @@ +assertSamePipeline(Pipelines::GroupCalculateCountSumAndAverage, $pipeline); + } + + public function testCountTheNumberOfDocumentsInACollection(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: null, + count: Accumulator::count(), + ), + ); + + $this->assertSamePipeline(Pipelines::GroupCountTheNumberOfDocumentsInACollection, $pipeline); + } + + public function testGroupByItemHaving(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('item'), + totalSaleAmount: Accumulator::sum( + Expression::multiply( + Expression::numberFieldPath('price'), + Expression::numberFieldPath('quantity'), + ), + ), + ), + Stage::match( + totalSaleAmount: Query::gte(100), + ), + ); + + $this->assertSamePipeline(Pipelines::GroupGroupByItemHaving, $pipeline); + } + + public function testGroupByNull(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: null, + totalSaleAmount: Accumulator::sum( + Expression::multiply( + Expression::numberFieldPath('price'), + Expression::numberFieldPath('quantity'), + ), + ), + averageQuantity: Accumulator::avg( + Expression::numberFieldPath('quantity'), + ), + count: Accumulator::sum(1), + ), + ); + + $this->assertSamePipeline(Pipelines::GroupGroupByNull, $pipeline); + } + + public function testGroupDocumentsByAuthor(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('author'), + books: Accumulator::push( + Expression::variable('ROOT'), + ), + ), + Stage::addFields( + totalCopies: Expression::sum( + Expression::numberFieldPath('books.copies'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::GroupGroupDocumentsByAuthor, $pipeline); + } + + public function testPivotData(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::fieldPath('author'), + books: Accumulator::push( + Expression::stringFieldPath('title'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::GroupPivotData, $pipeline); + } + + public function testRetrieveDistinctValues(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::stringFieldPath('item'), + ), + ); + + $this->assertSamePipeline(Pipelines::GroupRetrieveDistinctValues, $pipeline); + } +} diff --git a/tests/Builder/Stage/IndexStatsStageTest.php b/tests/Builder/Stage/IndexStatsStageTest.php new file mode 100644 index 000000000..9b121417b --- /dev/null +++ b/tests/Builder/Stage/IndexStatsStageTest.php @@ -0,0 +1,24 @@ +assertSamePipeline(Pipelines::IndexStatsExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/LimitStageTest.php b/tests/Builder/Stage/LimitStageTest.php new file mode 100644 index 000000000..04c9c6bbd --- /dev/null +++ b/tests/Builder/Stage/LimitStageTest.php @@ -0,0 +1,24 @@ +assertSamePipeline(Pipelines::LimitExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/ListLocalSessionsStageTest.php b/tests/Builder/Stage/ListLocalSessionsStageTest.php new file mode 100644 index 000000000..57d1bf06d --- /dev/null +++ b/tests/Builder/Stage/ListLocalSessionsStageTest.php @@ -0,0 +1,50 @@ +assertSamePipeline(Pipelines::ListLocalSessionsListAllLocalSessions, $pipeline); + } + + public function testListAllLocalSessionsForTheCurrentUser(): void + { + $pipeline = new Pipeline( + Stage::listLocalSessions(), + ); + + $this->assertSamePipeline(Pipelines::ListLocalSessionsListAllLocalSessionsForTheCurrentUser, $pipeline); + } + + public function testListAllLocalSessionsForTheSpecifiedUsers(): void + { + $pipeline = new Pipeline( + Stage::listLocalSessions( + users: [ + object(user: 'myAppReader', db: 'test'), + ], + ), + ); + + $this->assertSamePipeline(Pipelines::ListLocalSessionsListAllLocalSessionsForTheSpecifiedUsers, $pipeline); + } +} diff --git a/tests/Builder/Stage/ListSampledQueriesStageTest.php b/tests/Builder/Stage/ListSampledQueriesStageTest.php new file mode 100644 index 000000000..3d1fccf29 --- /dev/null +++ b/tests/Builder/Stage/ListSampledQueriesStageTest.php @@ -0,0 +1,35 @@ +assertSamePipeline(Pipelines::ListSampledQueriesListSampledQueriesForASpecificCollection, $pipeline); + } + + public function testListSampledQueriesForAllCollections(): void + { + $pipeline = new Pipeline( + Stage::listSampledQueries(), + ); + + $this->assertSamePipeline(Pipelines::ListSampledQueriesListSampledQueriesForAllCollections, $pipeline); + } +} diff --git a/tests/Builder/Stage/ListSearchIndexesStageTest.php b/tests/Builder/Stage/ListSearchIndexesStageTest.php new file mode 100644 index 000000000..e5bdbfd18 --- /dev/null +++ b/tests/Builder/Stage/ListSearchIndexesStageTest.php @@ -0,0 +1,46 @@ +assertSamePipeline(Pipelines::ListSearchIndexesReturnASingleSearchIndexById, $pipeline); + } + + public function testReturnASingleSearchIndexByName(): void + { + $pipeline = new Pipeline( + Stage::listSearchIndexes( + name: 'synonym-mappings', + ), + ); + + $this->assertSamePipeline(Pipelines::ListSearchIndexesReturnASingleSearchIndexByName, $pipeline); + } + + public function testReturnAllSearchIndexes(): void + { + $pipeline = new Pipeline( + Stage::listSearchIndexes(), + ); + + $this->assertSamePipeline(Pipelines::ListSearchIndexesReturnAllSearchIndexes, $pipeline); + } +} diff --git a/tests/Builder/Stage/ListSessionsStageTest.php b/tests/Builder/Stage/ListSessionsStageTest.php new file mode 100644 index 000000000..a6d68e3b1 --- /dev/null +++ b/tests/Builder/Stage/ListSessionsStageTest.php @@ -0,0 +1,50 @@ +assertSamePipeline(Pipelines::ListSessionsListAllSessions, $pipeline); + } + + public function testListAllSessionsForTheCurrentUser(): void + { + $pipeline = new Pipeline( + Stage::listSessions(), + ); + + $this->assertSamePipeline(Pipelines::ListSessionsListAllSessionsForTheCurrentUser, $pipeline); + } + + public function testListAllSessionsForTheSpecifiedUsers(): void + { + $pipeline = new Pipeline( + Stage::listSessions( + users: [ + object(user: 'myAppReader', db: 'test'), + ], + ), + ); + + $this->assertSamePipeline(Pipelines::ListSessionsListAllSessionsForTheSpecifiedUsers, $pipeline); + } +} diff --git a/tests/Builder/Stage/LookupStageTest.php b/tests/Builder/Stage/LookupStageTest.php new file mode 100644 index 000000000..3b541ebb4 --- /dev/null +++ b/tests/Builder/Stage/LookupStageTest.php @@ -0,0 +1,161 @@ +assertSamePipeline(Pipelines::LookupPerformAConciseCorrelatedSubqueryWithLookup, $pipeline); + } + + public function testPerformASingleEqualityJoinWithLookup(): void + { + $pipeline = new Pipeline( + Stage::lookup( + from: 'inventory', + localField: 'item', + foreignField: 'sku', + as: 'inventory_docs', + ), + ); + + $this->assertSamePipeline(Pipelines::LookupPerformASingleEqualityJoinWithLookup, $pipeline); + } + + public function testPerformAnUncorrelatedSubqueryWithLookup(): void + { + $pipeline = new Pipeline( + Stage::lookup( + from: 'holidays', + pipeline: new Pipeline( + Stage::match( + year: 2018, + ), + Stage::project( + _id: 0, + date: object( + name: Expression::stringFieldPath('name'), + date: Expression::dateFieldPath('date'), + ), + ), + Stage::replaceRoot(Expression::objectFieldPath('date')), + ), + as: 'holidays', + ), + ); + + $this->assertSamePipeline(Pipelines::LookupPerformAnUncorrelatedSubqueryWithLookup, $pipeline); + } + + public function testPerformMultipleJoinsAndACorrelatedSubqueryWithLookup(): void + { + $pipeline = new Pipeline( + Stage::lookup( + from: 'warehouses', + let: object( + order_item: Expression::fieldPath('item'), + order_qty: Expression::intFieldPath('ordered'), + ), + pipeline: new Pipeline( + Stage::match( + Query::expr( + Expression::and( + Expression::eq( + Expression::stringFieldPath('stock_item'), + Expression::variable('order_item'), + ), + Expression::gte( + Expression::intFieldPath('instock'), + Expression::variable('order_qty'), + ), + ), + ), + ), + Stage::project( + stock_item: 0, + _id: 0, + ), + ), + as: 'stockdata', + ), + ); + + $this->assertSamePipeline(Pipelines::LookupPerformMultipleJoinsAndACorrelatedSubqueryWithLookup, $pipeline); + } + + public function testUseLookupWithAnArray(): void + { + $pipeline = new Pipeline( + Stage::lookup( + from: 'members', + localField: 'enrollmentlist', + foreignField: 'name', + as: 'enrollee_info', + ), + ); + + $this->assertSamePipeline(Pipelines::LookupUseLookupWithAnArray, $pipeline); + } + + public function testUseLookupWithMergeObjects(): void + { + $pipeline = new Pipeline( + Stage::lookup( + from: 'items', + localField: 'item', + foreignField: 'item', + as: 'fromItems', + ), + Stage::replaceRoot( + Expression::mergeObjects( + Expression::arrayElemAt( + Expression::arrayFieldPath('fromItems'), + 0, + ), + Expression::variable('ROOT'), + ), + ), + Stage::project( + fromItems: 0, + ), + ); + + $this->assertSamePipeline(Pipelines::LookupUseLookupWithMergeObjects, $pipeline); + } +} diff --git a/tests/Builder/Stage/MatchStageTest.php b/tests/Builder/Stage/MatchStageTest.php new file mode 100644 index 000000000..31c2d9554 --- /dev/null +++ b/tests/Builder/Stage/MatchStageTest.php @@ -0,0 +1,53 @@ +assertSamePipeline(Pipelines::MatchEqualityMatch, $pipeline); + } + + public function testPerformACount(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::or( + Query::query( + score: [ + Query::gt(70), + Query::lt(90), + ], + ), + Query::query( + views: Query::gte(1000), + ), + ), + ), + Stage::group( + _id: null, + count: Accumulator::sum(1), + ), + ); + + $this->assertSamePipeline(Pipelines::MatchPerformACount, $pipeline); + } +} diff --git a/tests/Builder/Stage/MergeStageTest.php b/tests/Builder/Stage/MergeStageTest.php new file mode 100644 index 000000000..7c145fc29 --- /dev/null +++ b/tests/Builder/Stage/MergeStageTest.php @@ -0,0 +1,188 @@ +assertSamePipeline(Pipelines::MergeMergeResultsFromMultipleCollections, $pipeline); + } + + public function testOnDemandMaterializedViewInitialCreation(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: object( + fiscal_year: Expression::stringFieldPath('fiscal_year'), + dept: Expression::stringFieldPath('dept'), + ), + salaries: Accumulator::sum( + Expression::numberFieldPath('salary'), + ), + ), + Stage::merge( + into: object( + db: 'reporting', + coll: 'budgets', + ), + on: '_id', + whenMatched: 'replace', + whenNotMatched: 'insert', + ), + ); + + $this->assertSamePipeline(Pipelines::MergeOnDemandMaterializedViewInitialCreation, $pipeline); + } + + public function testOnDemandMaterializedViewUpdateReplaceData(): void + { + $pipeline = new Pipeline( + Stage::match( + fiscal_year: Query::gte(2019), + ), + Stage::group( + _id: object( + fiscal_year: Expression::stringFieldPath('fiscal_year'), + dept: Expression::stringFieldPath('dept'), + ), + salaries: Accumulator::sum( + Expression::numberFieldPath('salary'), + ), + ), + Stage::merge( + into: object( + db: 'reporting', + coll: 'budgets', + ), + on: '_id', + whenMatched: 'replace', + whenNotMatched: 'insert', + ), + ); + + $this->assertSamePipeline(Pipelines::MergeOnDemandMaterializedViewUpdateReplaceData, $pipeline); + } + + public function testOnlyInsertNewData(): void + { + $pipeline = new Pipeline( + Stage::match( + fiscal_year: 2019, + ), + Stage::group( + _id: object( + fiscal_year: Expression::stringFieldPath('fiscal_year'), + dept: Expression::stringFieldPath('dept'), + ), + employees: Accumulator::push( + Expression::numberFieldPath('employee'), + ), + ), + Stage::project( + _id: 0, + dept: Expression::fieldPath('_id.dept'), + fiscal_year: Expression::fieldPath('_id.fiscal_year'), + employees: 1, + ), + Stage::merge( + into: object( + db: 'reporting', + coll: 'orgArchive', + ), + on: ['dept', 'fiscal_year'], + whenMatched: 'fail', + ), + ); + + $this->assertSamePipeline(Pipelines::MergeOnlyInsertNewData, $pipeline); + } + + public function testUseThePipelineToCustomizeTheMerge(): void + { + $pipeline = new Pipeline( + Stage::match( + date: [ + Query::gte(new UTCDateTime(1557187200000)), + Query::lt(new UTCDateTime(1557273600000)), + ], + ), + Stage::project( + _id: Expression::dateToString( + format: '%Y-%m', + date: Expression::dateFieldPath('date'), + ), + thumbsup: 1, + thumbsdown: 1, + ), + Stage::merge( + into: 'monthlytotals', + on: '_id', + whenMatched: new Pipeline( + Stage::addFields( + thumbsup: Expression::add( + Expression::numberFieldPath('thumbsup'), + Expression::variable('new.thumbsup'), + ), + thumbsdown: Expression::add( + Expression::numberFieldPath('thumbsdown'), + Expression::variable('new.thumbsdown'), + ), + ), + ), + whenNotMatched: 'insert', + ), + ); + + $this->assertSamePipeline(Pipelines::MergeUseThePipelineToCustomizeTheMerge, $pipeline); + } + + public function testUseVariablesToCustomizeTheMerge(): void + { + $pipeline = new Pipeline( + Stage::merge( + into: 'cakeSales', + let: object( + year: '2020', + ), + whenMatched: new Pipeline( + Stage::addFields( + salesYear: Expression::variable('year'), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::MergeUseVariablesToCustomizeTheMerge, $pipeline); + } +} diff --git a/tests/Builder/Stage/OutStageTest.php b/tests/Builder/Stage/OutStageTest.php new file mode 100644 index 000000000..a8ec9fb77 --- /dev/null +++ b/tests/Builder/Stage/OutStageTest.php @@ -0,0 +1,54 @@ +assertSamePipeline(Pipelines::OutOutputToADifferentDatabase, $pipeline); + } + + public function testOutputToSameDatabase(): void + { + $pipeline = new Pipeline( + Stage::group( + _id: Expression::stringFieldPath('author'), + books: Accumulator::push( + Expression::stringFieldPath('title'), + ), + ), + Stage::out('authors'), + ); + + $this->assertSamePipeline(Pipelines::OutOutputToSameDatabase, $pipeline); + } +} diff --git a/tests/Builder/Stage/Pipelines.php b/tests/Builder/Stage/Pipelines.php new file mode 100644 index 000000000..8b76342ba --- /dev/null +++ b/tests/Builder/Stage/Pipelines.php @@ -0,0 +1,3365 @@ +assertSamePipeline(Pipelines::PlanCacheStatsFindCacheEntryDetailsForAQueryHash, $pipeline); + } + + public function testReturnInformationForAllEntriesInTheQueryCache(): void + { + $pipeline = new Pipeline( + Stage::planCacheStats(), + ); + + $this->assertSamePipeline(Pipelines::PlanCacheStatsReturnInformationForAllEntriesInTheQueryCache, $pipeline); + } +} diff --git a/tests/Builder/Stage/ProjectStageTest.php b/tests/Builder/Stage/ProjectStageTest.php new file mode 100644 index 000000000..9e30d731e --- /dev/null +++ b/tests/Builder/Stage/ProjectStageTest.php @@ -0,0 +1,164 @@ + 1, + 'author.last' => 1, + 'author.middle' => Expression::cond( + if: Expression::eq( + '', + Expression::stringFieldPath('author.middle'), + ), + then: Expression::variable('REMOVE'), + else: Expression::stringFieldPath('author.middle'), + ), + ], + title: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::ProjectConditionallyExcludeFields, $pipeline); + } + + public function testExcludeFieldsFromEmbeddedDocuments(): void + { + $pipeline = new Pipeline( + // Both stages are equivalents + Stage::project( + ...['author.first' => 0], + ...['lastModified' => 0], + ), + Stage::project( + author: object(first: 0), + lastModified: 0, + ), + ); + + $this->assertSamePipeline(Pipelines::ProjectExcludeFieldsFromEmbeddedDocuments, $pipeline); + } + + public function testExcludeFieldsFromOutputDocuments(): void + { + $pipeline = new Pipeline( + Stage::project( + lastModified: 0, + ), + ); + + $this->assertSamePipeline(Pipelines::ProjectExcludeFieldsFromOutputDocuments, $pipeline); + } + + public function testIncludeComputedFields(): void + { + $pipeline = new Pipeline( + Stage::project( + title: 1, + isbn: object( + prefix: Expression::substr( + Expression::stringFieldPath('isbn'), + 0, + 3, + ), + group: Expression::substr( + Expression::stringFieldPath('isbn'), + 3, + 2, + ), + publisher: Expression::substr( + Expression::stringFieldPath('isbn'), + 5, + 4, + ), + title: Expression::substr( + Expression::stringFieldPath('isbn'), + 9, + 3, + ), + checkDigit: Expression::substr( + Expression::stringFieldPath('isbn'), + 12, + 1, + ), + ), + lastName: Expression::stringFieldPath('author.last'), + copiesSold: Expression::intFieldPath('copies'), + ), + ); + + $this->assertSamePipeline(Pipelines::ProjectIncludeComputedFields, $pipeline); + } + + public function testIncludeSpecificFieldsFromEmbeddedDocuments(): void + { + $pipeline = new Pipeline( + Stage::project( + ...['stop.title' => 1], + ), + Stage::project( + stop: object(title: 1), + ), + ); + + $this->assertSamePipeline(Pipelines::ProjectIncludeSpecificFieldsFromEmbeddedDocuments, $pipeline); + } + + public function testIncludeSpecificFieldsInOutputDocuments(): void + { + $pipeline = new Pipeline( + Stage::project( + title: 1, + author: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::ProjectIncludeSpecificFieldsInOutputDocuments, $pipeline); + } + + public function testProjectNewArrayFields(): void + { + $pipeline = new Pipeline( + Stage::project( + myArray: [ + Expression::fieldPath('x'), + Expression::fieldPath('y'), + ], + ), + ); + + $this->assertSamePipeline(Pipelines::ProjectProjectNewArrayFields, $pipeline); + } + + public function testSuppressIdFieldInTheOutputDocuments(): void + { + $pipeline = new Pipeline( + Stage::project( + _id: 0, + title: 1, + author: 1, + ), + ); + + $this->assertSamePipeline(Pipelines::ProjectSuppressIdFieldInTheOutputDocuments, $pipeline); + } +} diff --git a/tests/Builder/Stage/RedactStageTest.php b/tests/Builder/Stage/RedactStageTest.php new file mode 100644 index 000000000..867ca5919 --- /dev/null +++ b/tests/Builder/Stage/RedactStageTest.php @@ -0,0 +1,63 @@ +assertSamePipeline(Pipelines::RedactEvaluateAccessAtEveryDocumentLevel, $pipeline); + } + + public function testExcludeAllFieldsAtAGivenLevel(): void + { + $pipeline = new Pipeline( + Stage::match( + status: 'A', + ), + Stage::redact( + Expression::cond( + if: Expression::eq( + Expression::intFieldPath('level'), + 5, + ), + then: Expression::variable('PRUNE'), + else: Expression::variable('DESCEND'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::RedactExcludeAllFieldsAtAGivenLevel, $pipeline); + } +} diff --git a/tests/Builder/Stage/ReplaceRootStageTest.php b/tests/Builder/Stage/ReplaceRootStageTest.php new file mode 100644 index 000000000..436bde704 --- /dev/null +++ b/tests/Builder/Stage/ReplaceRootStageTest.php @@ -0,0 +1,77 @@ + Query::gte(90)], + ), + Stage::replaceRoot(Expression::objectFieldPath('grades')), + ); + + $this->assertSamePipeline(Pipelines::ReplaceRootWithADocumentNestedInAnArray, $pipeline); + } + + public function testWithANewDocumentCreatedFromROOTAndADefaultDocument(): void + { + $pipeline = new Pipeline( + Stage::replaceRoot( + Expression::mergeObjects( + object(_id: '', name: '', email: '', cell: '', home: ''), + Expression::variable('ROOT'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ReplaceRootWithANewDocumentCreatedFromROOTAndADefaultDocument, $pipeline); + } + + public function testWithANewlyCreatedDocument(): void + { + $pipeline = new Pipeline( + Stage::replaceRoot( + object( + full_name: Expression::concat( + Expression::stringFieldPath('first_name'), + ' ', + Expression::stringFieldPath('last_name'), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ReplaceRootWithANewlyCreatedDocument, $pipeline); + } + + public function testWithAnEmbeddedDocumentField(): void + { + $pipeline = new Pipeline( + Stage::replaceRoot( + Expression::mergeObjects( + object(dogs: 0, cats: 0, birds: 0, fish: 0), + Expression::objectFieldPath('pets'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ReplaceRootWithAnEmbeddedDocumentField, $pipeline); + } +} diff --git a/tests/Builder/Stage/ReplaceWithStageTest.php b/tests/Builder/Stage/ReplaceWithStageTest.php new file mode 100644 index 000000000..ad0edfbc5 --- /dev/null +++ b/tests/Builder/Stage/ReplaceWithStageTest.php @@ -0,0 +1,83 @@ + Query::gte(90)], + ), + Stage::replaceWith(Expression::objectFieldPath('grades')), + ); + + $this->assertSamePipeline(Pipelines::ReplaceWithADocumentNestedInAnArray, $pipeline); + } + + public function testANewDocumentCreatedFromROOTAndADefaultDocument(): void + { + $pipeline = new Pipeline( + Stage::replaceWith( + Expression::mergeObjects( + object(_id: '', name: '', email: '', cell: '', home: ''), + Expression::variable('ROOT'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ReplaceWithANewDocumentCreatedFromROOTAndADefaultDocument, $pipeline); + } + + public function testANewlyCreatedDocument(): void + { + $pipeline = new Pipeline( + Stage::match( + status: 'C', + ), + Stage::replaceWith( + object( + _id: Expression::objectFieldPath('_id'), + item: Expression::fieldPath('item'), + amount: Expression::multiply( + Expression::numberFieldPath('price'), + Expression::numberFieldPath('quantity'), + ), + status: 'Complete', + asofDate: Expression::variable('NOW'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ReplaceWithANewlyCreatedDocument, $pipeline); + } + + public function testAnEmbeddedDocumentField(): void + { + $pipeline = new Pipeline( + Stage::replaceWith( + Expression::mergeObjects( + object(dogs: 0, cats: 0, birds: 0, fish: 0), + Expression::objectFieldPath('pets'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::ReplaceWithAnEmbeddedDocumentField, $pipeline); + } +} diff --git a/tests/Builder/Stage/SampleStageTest.php b/tests/Builder/Stage/SampleStageTest.php new file mode 100644 index 000000000..5ac8efd4a --- /dev/null +++ b/tests/Builder/Stage/SampleStageTest.php @@ -0,0 +1,24 @@ +assertSamePipeline(Pipelines::SampleExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/SearchMetaStageTest.php b/tests/Builder/Stage/SearchMetaStageTest.php new file mode 100644 index 000000000..3b8283424 --- /dev/null +++ b/tests/Builder/Stage/SearchMetaStageTest.php @@ -0,0 +1,33 @@ +assertSamePipeline(Pipelines::SearchMetaExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/SearchStageTest.php b/tests/Builder/Stage/SearchStageTest.php new file mode 100644 index 000000000..656b9b9ee --- /dev/null +++ b/tests/Builder/Stage/SearchStageTest.php @@ -0,0 +1,44 @@ +assertSamePipeline(Pipelines::SearchExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/SetStageTest.php b/tests/Builder/Stage/SetStageTest.php new file mode 100644 index 000000000..64193b8b9 --- /dev/null +++ b/tests/Builder/Stage/SetStageTest.php @@ -0,0 +1,89 @@ +assertSamePipeline(Pipelines::SetAddElementToAnArray, $pipeline); + } + + public function testAddingFieldsToAnEmbeddedDocument(): void + { + $pipeline = new Pipeline( + Stage::set( + ...['specs.fuel_type' => 'unleaded'], + ), + ); + + $this->assertSamePipeline(Pipelines::SetAddingFieldsToAnEmbeddedDocument, $pipeline); + } + + public function testCreatingANewFieldWithExistingFields(): void + { + $pipeline = new Pipeline( + Stage::set( + quizAverage: Expression::avg( + Expression::numberFieldPath('quiz'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetCreatingANewFieldWithExistingFields, $pipeline); + } + + public function testOverwritingAnExistingField(): void + { + $pipeline = new Pipeline( + Stage::set(cats: 20), + ); + + $this->assertSamePipeline(Pipelines::SetOverwritingAnExistingField, $pipeline); + } + + public function testUsingTwoSetStages(): void + { + $pipeline = new Pipeline( + Stage::set( + totalHomework: Expression::sum( + Expression::arrayFieldPath('homework'), + ), + totalQuiz: Expression::sum( + Expression::arrayFieldPath('quiz'), + ), + ), + Stage::set( + totalScore: Expression::add( + Expression::numberFieldPath('totalHomework'), + Expression::numberFieldPath('totalQuiz'), + Expression::numberFieldPath('extraCredit'), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetUsingTwoSetStages, $pipeline); + } +} diff --git a/tests/Builder/Stage/SetWindowFieldsStageTest.php b/tests/Builder/Stage/SetWindowFieldsStageTest.php new file mode 100644 index 000000000..442d959bf --- /dev/null +++ b/tests/Builder/Stage/SetWindowFieldsStageTest.php @@ -0,0 +1,175 @@ +assertSamePipeline(Pipelines::SetWindowFieldsRangeWindowExample, $pipeline); + } + + public function testUseATimeRangeWindowWithANegativeUpperBound(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::stringFieldPath('state'), + sortBy: object(orderDate: Sort::Asc), + output: object( + recentOrders: Accumulator::outputWindow( + Accumulator::push( + Expression::dateFieldPath('orderDate'), + ), + range: ['unbounded', -10], + unit: TimeUnit::Month, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetWindowFieldsUseATimeRangeWindowWithANegativeUpperBound, $pipeline); + } + + public function testUseATimeRangeWindowWithAPositiveUpperBound(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::stringFieldPath('state'), + sortBy: object(orderDate: Sort::Asc), + output: object( + recentOrders: Accumulator::outputWindow( + Accumulator::push( + Expression::dateFieldPath('orderDate'), + ), + range: ['unbounded', 10], + unit: TimeUnit::Month, + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetWindowFieldsUseATimeRangeWindowWithAPositiveUpperBound, $pipeline); + } + + public function testUseDocumentsWindowToObtainCumulativeAndMaximumQuantityForEachYear(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::year( + Expression::dateFieldPath('orderDate'), + ), + sortBy: object(orderDate: Sort::Asc), + output: object( + cumulativeQuantityForYear: Accumulator::outputWindow( + Accumulator::sum( + Expression::numberFieldPath('quantity'), + ), + documents: ['unbounded', 'current'], + ), + maximumQuantityForYear: Accumulator::outputWindow( + Accumulator::max( + Expression::numberFieldPath('quantity'), + ), + documents: ['unbounded', 'unbounded'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetWindowFieldsUseDocumentsWindowToObtainCumulativeAndMaximumQuantityForEachYear, $pipeline); + } + + public function testUseDocumentsWindowToObtainCumulativeQuantityForEachState(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::stringFieldPath('state'), + sortBy: object(orderDate: Sort::Asc), + output: object( + cumulativeQuantityForState: Accumulator::outputWindow( + Accumulator::sum( + Expression::numberFieldPath('quantity'), + ), + documents: ['unbounded', 'current'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetWindowFieldsUseDocumentsWindowToObtainCumulativeQuantityForEachState, $pipeline); + } + + public function testUseDocumentsWindowToObtainCumulativeQuantityForEachYear(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::year( + Expression::dateFieldPath('orderDate'), + ), + sortBy: object(orderDate: Sort::Asc), + output: object( + cumulativeQuantityForYear: Accumulator::outputWindow( + Accumulator::sum( + Expression::numberFieldPath('quantity'), + ), + documents: ['unbounded', 'current'], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetWindowFieldsUseDocumentsWindowToObtainCumulativeQuantityForEachYear, $pipeline); + } + + public function testUseDocumentsWindowToObtainMovingAverageQuantityForEachYear(): void + { + $pipeline = new Pipeline( + Stage::setWindowFields( + partitionBy: Expression::year( + Expression::dateFieldPath('orderDate'), + ), + sortBy: object(orderDate: Sort::Asc), + output: object( + averageQuantity: Accumulator::outputWindow( + Accumulator::avg( + Expression::numberFieldPath('quantity'), + ), + documents: [-1, 0], + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::SetWindowFieldsUseDocumentsWindowToObtainMovingAverageQuantityForEachYear, $pipeline); + } +} diff --git a/tests/Builder/Stage/ShardedDataDistributionStageTest.php b/tests/Builder/Stage/ShardedDataDistributionStageTest.php new file mode 100644 index 000000000..f9c0db9bc --- /dev/null +++ b/tests/Builder/Stage/ShardedDataDistributionStageTest.php @@ -0,0 +1,24 @@ +assertSamePipeline(Pipelines::ShardedDataDistributionExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/SkipStageTest.php b/tests/Builder/Stage/SkipStageTest.php new file mode 100644 index 000000000..4a716bea1 --- /dev/null +++ b/tests/Builder/Stage/SkipStageTest.php @@ -0,0 +1,24 @@ +assertSamePipeline(Pipelines::SkipExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/SortByCountStageTest.php b/tests/Builder/Stage/SortByCountStageTest.php new file mode 100644 index 000000000..d1a0393a9 --- /dev/null +++ b/tests/Builder/Stage/SortByCountStageTest.php @@ -0,0 +1,30 @@ +assertSamePipeline(Pipelines::SortByCountExample, $pipeline); + } +} diff --git a/tests/Builder/Stage/SortStageTest.php b/tests/Builder/Stage/SortStageTest.php new file mode 100644 index 000000000..5590454bc --- /dev/null +++ b/tests/Builder/Stage/SortStageTest.php @@ -0,0 +1,44 @@ +assertSamePipeline(Pipelines::SortAscendingDescendingSort, $pipeline); + } + + public function testTextScoreMetadataSort(): void + { + $pipeline = new Pipeline( + Stage::match( + Query::text('operating'), + ), + Stage::sort( + score: Sort::TextScore, + posts: Sort::Desc, + ), + ); + + $this->assertSamePipeline(Pipelines::SortTextScoreMetadataSort, $pipeline); + } +} diff --git a/tests/Builder/Stage/UnionWithStageTest.php b/tests/Builder/Stage/UnionWithStageTest.php new file mode 100644 index 000000000..fcf2edf90 --- /dev/null +++ b/tests/Builder/Stage/UnionWithStageTest.php @@ -0,0 +1,78 @@ +assertSamePipeline(Pipelines::UnionWithReport1AllSalesByYearAndStoresAndItems, $pipeline); + } + + public function testReport2AggregatedSalesByItems(): void + { + $pipeline = new Pipeline( + Stage::unionWith('sales_2018'), + Stage::unionWith('sales_2019'), + Stage::unionWith('sales_2020'), + Stage::group( + _id: Expression::stringFieldPath('item'), + total: Accumulator::sum( + Expression::numberFieldPath('quantity'), + ), + ), + Stage::sort( + total: Sort::Desc, + ), + ); + + $this->assertSamePipeline(Pipelines::UnionWithReport2AggregatedSalesByItems, $pipeline); + } +} diff --git a/tests/Builder/Stage/UnsetStageTest.php b/tests/Builder/Stage/UnsetStageTest.php new file mode 100644 index 000000000..217172743 --- /dev/null +++ b/tests/Builder/Stage/UnsetStageTest.php @@ -0,0 +1,49 @@ +assertSamePipeline(Pipelines::UnsetRemoveASingleField, $pipeline); + } + + public function testRemoveEmbeddedFields(): void + { + $pipeline = new Pipeline( + Stage::unset( + 'isbn', + 'author.first', + 'copies.warehouse', + ), + ); + + $this->assertSamePipeline(Pipelines::UnsetRemoveEmbeddedFields, $pipeline); + } + + public function testRemoveTopLevelFields(): void + { + $pipeline = new Pipeline( + Stage::unset( + 'isbn', + 'copies', + ), + ); + + $this->assertSamePipeline(Pipelines::UnsetRemoveTopLevelFields, $pipeline); + } +} diff --git a/tests/Builder/Stage/UnwindStageTest.php b/tests/Builder/Stage/UnwindStageTest.php new file mode 100644 index 000000000..26b1d377c --- /dev/null +++ b/tests/Builder/Stage/UnwindStageTest.php @@ -0,0 +1,91 @@ +assertSamePipeline(Pipelines::UnwindGroupByUnwoundValues, $pipeline); + } + + public function testIncludeArrayIndex(): void + { + $pipeline = new Pipeline( + Stage::unwind( + path: Expression::arrayFieldPath('sizes'), + includeArrayIndex: 'arrayIndex', + ), + ); + + $this->assertSamePipeline(Pipelines::UnwindIncludeArrayIndex, $pipeline); + } + + public function testPreserveNullAndEmptyArrays(): void + { + $pipeline = new Pipeline( + Stage::unwind( + path: Expression::arrayFieldPath('sizes'), + preserveNullAndEmptyArrays: true, + ), + ); + + $this->assertSamePipeline(Pipelines::UnwindPreserveNullAndEmptyArrays, $pipeline); + } + + public function testUnwindArray(): void + { + $pipeline = new Pipeline( + Stage::unwind(Expression::arrayFieldPath('sizes')), + ); + + $this->assertSamePipeline(Pipelines::UnwindUnwindArray, $pipeline); + } + + public function testUnwindEmbeddedArrays(): void + { + $pipeline = new Pipeline( + Stage::unwind(Expression::arrayFieldPath('items')), + Stage::unwind(Expression::arrayFieldPath('items.tags')), + Stage::group( + _id: Expression::fieldPath('items.tags'), + totalSalesAmount: Accumulator::sum( + Expression::multiply( + Expression::numberFieldPath('items.price'), + Expression::numberFieldPath('items.quantity'), + ), + ), + ), + ); + + $this->assertSamePipeline(Pipelines::UnwindUnwindEmbeddedArrays, $pipeline); + } +} diff --git a/tests/Builder/Type/CombinedFieldQueryTest.php b/tests/Builder/Type/CombinedFieldQueryTest.php new file mode 100644 index 000000000..1e302f8bd --- /dev/null +++ b/tests/Builder/Type/CombinedFieldQueryTest.php @@ -0,0 +1,113 @@ +assertSame([], $fieldQueries->fieldQueries); + } + + public function testSupportedTypes(): void + { + $fieldQueries = new CombinedFieldQuery([ + new EqOperator(1), + ['$gt' => 1], + (object) ['$lt' => 1], + ]); + + $this->assertCount(3, $fieldQueries->fieldQueries); + } + + public function testFlattenCombinedFieldQueries(): void + { + $fieldQueries = new CombinedFieldQuery([ + new CombinedFieldQuery([ + new CombinedFieldQuery([ + ['$lt' => 1], + new CombinedFieldQuery([]), + ]), + ['$gt' => 1], + ]), + ['$gte' => 1], + ]); + + $this->assertCount(3, $fieldQueries->fieldQueries); + } + + /** @dataProvider provideInvalidFieldQuery */ + public function testRejectInvalidFieldQueries(mixed $invalidQuery, string $message = '-'): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($message); + + new CombinedFieldQuery([$invalidQuery]); + } + + public static function provideInvalidFieldQuery(): Generator + { + yield 'int' => [1, 'Expected filters to be a list of field query operators, array or stdClass, int given']; + yield 'float' => [1.1, 'Expected filters to be a list of field query operators, array or stdClass, float given']; + yield 'string' => ['foo', 'Expected filters to be a list of field query operators, array or stdClass, string given']; + yield 'bool' => [true, 'Expected filters to be a list of field query operators, array or stdClass, bool given']; + yield 'null' => [null, 'Expected filters to be a list of field query operators, array or stdClass, null given']; + yield 'empty array' => [[], 'Operator must contain exactly one key, 0 given']; + yield 'array with two keys' => [['$eq' => 1, '$ne' => 2], 'Operator must contain exactly one key, 2 given']; + yield 'array key without $' => [['eq' => 1], 'Operator must contain exactly one key starting with $, "eq" given']; + yield 'empty object' => [(object) [], 'Operator must contain exactly one key, 0 given']; + yield 'object with two keys' => [(object) ['$eq' => 1, '$ne' => 2], 'Operator must contain exactly one key, 2 given']; + yield 'object key without $' => [(object) ['eq' => 1], 'Operator must contain exactly one key starting with $, "eq" given']; + } + + /** + * @param array $fieldQueries + * + * @dataProvider provideDuplicateOperator + */ + public function testRejectDuplicateOperator(array $fieldQueries): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Duplicate operator "$eq" detected'); + + new CombinedFieldQuery([ + ['$eq' => 1], + new EqOperator(2), + ]); + } + + public function provideDuplicateOperator(): Generator + { + yield 'array and FieldQuery' => [ + [ + ['$eq' => 1], + new EqOperator(2), + ], + ]; + + yield 'object and FieldQuery' => [ + [ + (object) ['$gt' => 1], + new GtOperator(2), + ], + ]; + + yield 'object and array' => [ + [ + (object) ['$ne' => 1], + ['$ne' => 2], + ], + ]; + } +} diff --git a/tests/Builder/Type/OutputWindowTest.php b/tests/Builder/Type/OutputWindowTest.php new file mode 100644 index 000000000..fed42a3eb --- /dev/null +++ b/tests/Builder/Type/OutputWindowTest.php @@ -0,0 +1,108 @@ +createMock(WindowInterface::class), + ); + + $this->assertSame($operator, $outputWindow->operator); + $this->assertSame(Optional::Undefined, $outputWindow->window); + } + + public function testWithDocuments(): void + { + $outputWindow = new OutputWindow( + operator: $operator = $this->createMock(WindowInterface::class), + documents: [1, 5], + ); + + $this->assertSame($operator, $outputWindow->operator); + $this->assertEquals((object) ['documents' => [1, 5]], $outputWindow->window); + } + + public function testWithRange(): void + { + $outputWindow = new OutputWindow( + operator: $operator = $this->createMock(WindowInterface::class), + range: [1.2, 5.8], + ); + + $this->assertSame($operator, $outputWindow->operator); + $this->assertEquals((object) ['range' => [1.2, 5.8]], $outputWindow->window); + } + + public function testWithUnit(): void + { + $outputWindow = new OutputWindow( + operator: $operator = $this->createMock(WindowInterface::class), + unit: TimeUnit::Day, + ); + + $this->assertSame($operator, $outputWindow->operator); + $this->assertEquals((object) ['unit' => TimeUnit::Day], $outputWindow->window); + } + + /** + * @param array $documents + * + * @dataProvider provideInvalidDocuments + */ + public function testRejectInvalidDocuments(array $documents): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected $documents argument to be a list of 2 string or int'); + + new OutputWindow( + operator: $this->createMock(WindowInterface::class), + documents: $documents, + ); + } + + public function provideInvalidDocuments(): Generator + { + yield 'too few' => [[1]]; + yield 'too many' => [[1, 2, 3]]; + yield 'invalid boolean' => [[1, true]]; + yield 'invalid float' => [[1, 4.3]]; + yield 'not a list' => [['foo' => 1, 'bar' => 2]]; + } + + /** + * @param array $range + * + * @dataProvider provideInvalidRange + */ + public function testRejectInvalidRange(array $range): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Expected $range argument to be a list of 2 string or numeric'); + + new OutputWindow( + operator: $this->createMock(WindowInterface::class), + range: $range, + ); + } + + public function provideInvalidRange(): Generator + { + yield 'too few' => [[1]]; + yield 'too many' => [[1, 2, 3]]; + yield 'invalid boolean' => [[1, true]]; + yield 'not a list' => [['foo' => 1, 'bar' => 2]]; + } +} diff --git a/tests/Builder/Type/QueryObjectTest.php b/tests/Builder/Type/QueryObjectTest.php new file mode 100644 index 000000000..fe7a86959 --- /dev/null +++ b/tests/Builder/Type/QueryObjectTest.php @@ -0,0 +1,96 @@ +assertSame([], $queryObject->queries); + } + + public function testShortCutQueryObject(): void + { + $query = $this->createMock(QueryInterface::class); + $queryObject = QueryObject::create([$query]); + + $this->assertSame($query, $queryObject); + } + + /** + * @param array $value + * + * @dataProvider provideQueryObjectValue + */ + public function testCreateQueryObject(array $value, int $expectedCount = 1): void + { + $queryObject = QueryObject::create($value); + + $this->assertCount($expectedCount, $queryObject->queries); + } + + /** + * @param array $value + * + * @dataProvider provideQueryObjectValue + */ + public function testCreateQueryObjectFromArray(array $value, int $expectedCount = 1): void + { + // $value is wrapped in an array as if the user used an array instead of variadic arguments + $queryObject = QueryObject::create([$value]); + + $this->assertCount($expectedCount, $queryObject->queries); + } + + public function provideQueryObjectValue(): Generator + { + yield 'int' => [['foo' => 1]]; + yield 'float' => [['foo' => 1.1]]; + yield 'string' => [['foo' => 'bar']]; + yield 'bool' => [['foo' => true]]; + yield 'null' => [['foo' => null]]; + yield 'decimal128' => [['foo' => new BSON\Decimal128('1.1')]]; + yield 'int64' => [[1 => new BSON\Int64(1)]]; + yield 'objectId' => [['foo' => new BSON\ObjectId()]]; + yield 'binary' => [['foo' => new BSON\Binary('foo')]]; + yield 'regex' => [['foo' => new BSON\Regex('foo')]]; + yield 'datetime' => [['foo' => new BSON\UTCDateTime()]]; + yield 'timestamp' => [['foo' => new BSON\Timestamp(1234, 5678)]]; + yield 'bson document' => [['foo' => BSON\Document::fromPHP(['bar' => 'baz'])]]; + yield 'bson array' => [['foo' => BSON\PackedArray::fromPHP(['bar', 'baz'])]]; + yield 'object' => [['foo' => (object) ['bar' => 'baz']]]; + yield 'list' => [['foo' => ['bar', 'baz']]]; + yield 'operator as array' => [['foo' => ['$eq' => 1]]]; + yield 'operator as object' => [['foo' => (object) ['$eq' => 1]]]; + yield 'field query operator' => [['foo' => new EqOperator(1)]]; + yield 'query operator' => [[new CommentOperator('foo'), 'foo' => 1], 2]; + yield 'numeric field with array' => [[1 => [2, 3]]]; + yield 'numeric field with operator' => [[1 => ['$eq' => 2]]]; + } + + public function testFieldQueryList(): void + { + $queryObject = QueryObject::create( + ['foo' => [new GtOperator(1), new LtOperator(5)]], + ); + + $this->assertArrayHasKey('foo', $queryObject->queries); + $this->assertInstanceOf(CombinedFieldQuery::class, $queryObject->queries['foo']); + $this->assertCount(2, $queryObject->queries['foo']->fieldQueries); + } +} diff --git a/tests/Builder/VariableTest.php b/tests/Builder/VariableTest.php new file mode 100644 index 000000000..f9273bfe2 --- /dev/null +++ b/tests/Builder/VariableTest.php @@ -0,0 +1,67 @@ +assertSame('foo', $variable->name); + $this->assertInstanceOf(Expression\ResolvesToAny::class, $variable); + $this->assertInstanceOf(Expression\Variable::class, $variable); + } + + public function testVariableRejectDollarPrefix(): void + { + $this->expectException(InvalidArgumentException::class); + + new Expression\Variable('$$foo'); + } + + /** @dataProvider provideVariableBuilders */ + public function testSystemVariables($factory): void + { + $variable = $factory(); + $this->assertInstanceOf(Expression\Variable::class, $variable); + $this->assertStringStartsNotWith('$$', $variable->name); + } + + public function provideVariableBuilders(): Generator + { + yield 'now' => [fn () => Variable::now()]; + yield 'clusterTime' => [fn () => Variable::clusterTime()]; + yield 'root' => [fn () => Variable::root()]; + yield 'current' => [fn () => Variable::current()]; + yield 'remove' => [fn () => Variable::remove()]; + yield 'descend' => [fn () => Variable::descend()]; + yield 'prune' => [fn () => Variable::prune()]; + yield 'keep' => [fn () => Variable::keep()]; + yield 'searchMeta' => [fn () => Variable::searchMeta()]; + yield 'userRoles' => [fn () => Variable::userRoles()]; + } + + public function testCurrent(): void + { + $variable = Variable::current(); + $this->assertInstanceOf(Expression\Variable::class, $variable); + $this->assertSame('CURRENT', $variable->name); + + $variable = Variable::current('foo'); + $this->assertInstanceOf(Expression\Variable::class, $variable); + $this->assertSame('CURRENT.foo', $variable->name); + } + + public function testCustomVariable(): void + { + $this->assertInstanceOf(Expression\Variable::class, Variable::variable('foo')); + } +} diff --git a/tests/PedantryTest.php b/tests/PedantryTest.php index 29df17a19..0faf67c54 100644 --- a/tests/PedantryTest.php +++ b/tests/PedantryTest.php @@ -2,6 +2,7 @@ namespace MongoDB\Tests; +use MongoDB; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ReflectionClass; @@ -10,6 +11,7 @@ use function array_filter; use function array_map; +use function in_array; use function realpath; use function str_contains; use function str_replace; @@ -25,6 +27,11 @@ */ class PedantryTest extends TestCase { + private const SKIPPED_CLASSES = [ + // Generated + MongoDB\Builder\Stage\FluentFactoryTrait::class, + ]; + /** @dataProvider provideProjectClassNames */ public function testMethodsAreOrderedAlphabeticallyByVisibility($className): void { @@ -74,6 +81,10 @@ public function provideProjectClassNames() } $className = 'MongoDB\\' . str_replace(DIRECTORY_SEPARATOR, '\\', substr($file->getRealPath(), strlen($srcDir) + 1, -4)); + if (in_array($className, self::SKIPPED_CLASSES)) { + continue; + } + $classNames[$className][] = $className; }