Skip to content
This repository was archived by the owner on Feb 28, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ psalm-baseline.xml export-ignore
/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
3 changes: 2 additions & 1 deletion generator/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
"require": {
"php": ">=8.1",
"ext-mongodb": "^1.16.0",
"mongodb/mongodb": "^1.17.0@dev",
"mongodb/builder": "@dev",
"mongodb/mongodb": "^1.17.0@dev",
"nette/php-generator": "^4",
"nikic/php-parser": "^4.17",
Copy link
Member Author

Choose a reason for hiding this comment

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

Required to modify existing files with the generator.

"symfony/console": "^6.3|^7.0",
"symfony/finder": "^6.3|^7.0",
"symfony/yaml": "^6.3|^7.0"
Expand Down
54 changes: 50 additions & 4 deletions generator/config/accumulator/accumulator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ arguments:
-
name: init
type:
- string
- 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.
-
Expand All @@ -24,7 +24,7 @@ arguments:
-
name: accumulate
type:
- string
- 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.
-
Expand All @@ -36,13 +36,13 @@ arguments:
-
name: merge
type:
- string
- 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:
- string
- javascript
optional: true
description: |
Function used to update the result of the accumulation.
Expand All @@ -52,3 +52,49 @@ arguments:
- 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'
32 changes: 32 additions & 0 deletions generator/config/accumulator/addToSet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,35 @@ 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'
6 changes: 6 additions & 0 deletions generator/config/definitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use MongoDB\CodeGenerator\OperatorClassGenerator;
use MongoDB\CodeGenerator\OperatorFactoryGenerator;
use MongoDB\CodeGenerator\OperatorTestGenerator;

return [
// Aggregation Pipeline Stages
Expand All @@ -16,6 +17,7 @@
'generators' => [
OperatorClassGenerator::class,
OperatorFactoryGenerator::class,
OperatorTestGenerator::class,
],
],

Expand All @@ -27,6 +29,7 @@
'generators' => [
OperatorClassGenerator::class,
OperatorFactoryGenerator::class,
OperatorTestGenerator::class,
],
],

Expand All @@ -38,6 +41,7 @@
'generators' => [
OperatorClassGenerator::class,
OperatorFactoryGenerator::class,
OperatorTestGenerator::class,
],
],

Expand All @@ -49,6 +53,7 @@
'generators' => [
OperatorClassGenerator::class,
OperatorFactoryGenerator::class,
OperatorTestGenerator::class,
],
],

Expand All @@ -60,6 +65,7 @@
'generators' => [
OperatorClassGenerator::class,
OperatorFactoryGenerator::class,
OperatorTestGenerator::class,
],
],
];
24 changes: 24 additions & 0 deletions generator/config/query/regex.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,27 @@ 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:
# Should be nested in $regex
$regularExpression:
pattern: '789$'
options: ''
Comment on lines +23 to +26
Copy link
Member Author

@GromNaN GromNaN Oct 19, 2023

Choose a reason for hiding this comment

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

$regex is a special key. I can't use it for a regular object.

$json = <<<'JSON'
{
    "foo": {
        "$regex": {
            "$regularExpression": {
                "pattern": "789$",
                "options": "i"
            }
        }
    }
}
JSON;

$document = fromJSON($json);

// Fatal error: Uncaught MongoDB\Driver\Exception\UnexpectedValueException: Unexpected nested object value for "$regex" key 

Copy link
Member

Choose a reason for hiding this comment

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

That exception originates from _bson_json_read_start_map()

Also discussed in the Extended JSON spec:

If you're using $regex as a query operator, the "Parsers" section above says:

A parser that accepts Legacy Extended JSON MUST be configurable such that a JSON text of a MongoDB query filter containing the regex query operator can be parsed.

And then it proceeds to give two examples. The first example resembles what you're trying to do here, where the $regex value is the extended JSON representation of a regex object (using the $regularExpression key). I'm not sure why that's prohibited in libbson, but it may be worth asking the C team about.

That aside, is anything preventing you from using the following?

{ $regex: 'pattern', $options: '<options>' }

Copy link
Member Author

@GromNaN GromNaN Oct 23, 2023

Choose a reason for hiding this comment

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

That aside, is anything preventing you from using the following?
{ $regex: 'pattern', $options: '<options>' }

This will result in new MongoDB\BSON\Regex('pattern', '<options'>).

The codec wraps the regex in the $regex operator for consistency with other operators.

The expected query that I need to encode in Yaml/Json is:

(object) ['sku' => (object) ['$regex' => new MongoDB\BSON\Regex('789$', '')]]

Copy link
Member Author

@GromNaN GromNaN Oct 23, 2023

Choose a reason for hiding this comment

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

$query = (object) ['sku' => (object) ['$regex' => new MongoDB\BSON\Regex('789$', '')]];

$json = MongoDB\BSON\toJSON(MongoDB\BSON\fromPHP($x));

// { "sku" : { "$regex" : { "$regex" : "789$", "$options" : "" } } }

MongoDB\BSON\fromJSON($json);

// Warning: Uncaught MongoDB\Driver\Exception\UnexpectedValueException: Unexpected nested object value for "$regex" key in php shell code:1

Copy link
Member Author

@GromNaN GromNaN Nov 3, 2023

Choose a reason for hiding this comment

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

Issue solved by comparing JSON: #13

-
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:
# Should be nested in $regex
$regularExpression:
pattern: '^ABC'
options: 'i'
31 changes: 28 additions & 3 deletions generator/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
"$comment": "The link to the operator's documentation on MongoDB's website.",
"type": "string",
"format": "uri",
"qt-uri-protocols": [
"https"
]
"pattern": "^https://"
},
"type": {
"type": "array",
Expand Down Expand Up @@ -76,6 +74,13 @@
"items": {
"$ref": "#/definitions/Argument"
}
},
"tests": {
"$comment": "An optional list of examples for the operator.",
"type": "array",
"items": {
"$ref": "#/definitions/Test"
}
}
},
"required": [
Expand Down Expand Up @@ -168,6 +173,26 @@
"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"
}
}
}
}
}
}
25 changes: 25 additions & 0 deletions generator/config/stage/addFields.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,28 @@ arguments:
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:
$sum: '$homework'
totalQuiz:
$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'
20 changes: 18 additions & 2 deletions generator/src/AbstractGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
use function current;
use function dirname;
use function explode;
use function file_get_contents;
use function file_put_contents;
use function implode;
use function is_dir;
use function is_file;
use function ltrim;
use function mkdir;
use function sprintf;
Expand Down Expand Up @@ -48,7 +50,7 @@ final protected function splitNamespaceAndClassName(string $fqcn): array
return [implode('\\', $parts), $className];
}

final protected function writeFile(PhpNamespace $namespace): void
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)));
Expand All @@ -62,12 +64,26 @@ final protected function writeFile(PhpNamespace $namespace): void

$file = new PhpFile();
$file->setStrictTypes();
$file->setComment('THIS FILE IS AUTO-GENERATED. ANY CHANGES WILL BE LOST!');
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.
*
Expand Down
12 changes: 10 additions & 2 deletions generator/src/Definition/OperatorDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@
use MongoDB\Builder\Type\Encode;
use UnexpectedValueException;

use function array_map;
use function array_merge;
use function array_values;
use function assert;
use function count;
use function sprintf;

final class OperatorDefinition
{
public Encode $encode;
public readonly Encode $encode;

/** @var list<ArgumentDefinition> */
public array $arguments;
public readonly array $arguments;

/** @var list<TestDefinition> */
public readonly array $tests;

public function __construct(
public string $name,
Expand All @@ -27,6 +32,7 @@ public function __construct(
public array $type,
public string|null $description = null,
array $arguments = [],
array $tests = [],
) {
$this->encode = match ($encode) {
'single' => Encode::Single,
Expand Down Expand Up @@ -55,5 +61,7 @@ public function __construct(
}

$this->arguments = array_merge($requiredArgs, $optionalArgs);

$this->tests = array_map(static fn (array $test): TestDefinition => new TestDefinition(...$test), array_values($tests));
}
}
Loading