Skip to content
This repository was archived by the owner on Feb 28, 2025. It is now read-only.

PHPLIB-1363 PHPLIB-1355 Add enum for $type query and $meta expression #38

Closed
wants to merge 4 commits into from
Closed
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
26 changes: 26 additions & 0 deletions generator/config/expression/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,29 @@ 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'
5 changes: 4 additions & 1 deletion psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="5.20.0@3f284e96c9d9be6fe6b15c79416e1d1903dcfef4">
<files psalm-version="5.21.0@04ba9358e3f7d14a9dc3edd4e814a9d51d8c637f">
<file src="src/Builder/Encoder/AbstractExpressionEncoder.php">
<MixedAssignment>
<code>$val</code>
Expand Down Expand Up @@ -52,6 +52,9 @@
<ArgumentTypeCoercion>
<code>$query</code>
</ArgumentTypeCoercion>
<PossiblyInvalidArgument>
<code>$type</code>
</PossiblyInvalidArgument>
</file>
<file src="src/Builder/Query/ElemMatchOperator.php">
<MixedArgumentTypeCoercion>
Expand Down
9 changes: 9 additions & 0 deletions src/Builder/Encoder/OperatorEncoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace MongoDB\Builder\Encoder;

use BackedEnum;
use LogicException;
use MongoDB\Builder\Stage\GroupStage;
use MongoDB\Builder\Type\Encode;
Expand Down Expand Up @@ -65,6 +66,10 @@ public function encode(mixed $value): stdClass
*/
private function encodeAsArray(OperatorInterface $value): stdClass
{
if ($value instanceof BackedEnum) {
return $this->wrap($value, [$value->value]);
}

$result = [];
/** @var mixed $val */
foreach (get_object_vars($value) as $val) {
Expand Down Expand Up @@ -142,6 +147,10 @@ private function encodeAsDollarObject(OperatorInterface $value): stdClass
*/
private function encodeAsSingle(OperatorInterface $value): stdClass
{
if ($value instanceof BackedEnum) {
return $this->wrap($value, $value->value);
}

foreach (get_object_vars($value) as $val) {
$result = $this->recursiveEncode($val);

Expand Down
43 changes: 43 additions & 0 deletions src/Builder/Meta.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace MongoDB\Builder;

use MongoDB\Builder\Type\Encode;
use MongoDB\Builder\Type\ExpressionInterface;
use MongoDB\Builder\Type\OperatorInterface;

/**
* Returns the metadata associated with a document, e.g. "textScore" when performing text search.
*
* @see https://www.mongodb.com/docs/manual/reference/operator/aggregation/meta/
*/
enum Meta: string implements OperatorInterface, ExpressionInterface
Copy link
Member Author

Choose a reason for hiding this comment

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

@alcaeus suggested to name it SortMeta. #56 (comment)

Copy link
Member

Choose a reason for hiding this comment

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

$meta: Behavior suggests the operator can be used in context beyond sorting, such as projections.

Copy link
Member Author

Choose a reason for hiding this comment

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

The ExpressionInterface allows to use it anywhere an expression is expected.

{
public const ENCODE = Encode::Single;

/**
* Returns the score associated with the corresponding $text query for each
* matching document. The text score signifies how well the document matched
* the search term or terms.
*/
case TextScore = 'textScore';

/**
* Returns an index key for the document if a non-text index is used. The
* { $meta: "indexKey" } expression is for debugging purposes only, and
* not for application logic, and is preferred over cursor.returnKey().
*/
case IndexKey = 'indexKey';
Copy link
Member

Choose a reason for hiding this comment

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

Do the cases here preclude users from using this with additional modes? $meta: Definition mentions searchScore and searchHighlights for Atlas.


/**
* Display search terms in their original context.
*/
case SearchHighlights = 'searchHighlights';

public function getOperator(): string
{
return '$meta';
}
}
22 changes: 22 additions & 0 deletions src/Builder/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use MongoDB\BSON\Regex;
use MongoDB\BSON\Type;
use MongoDB\Builder\Query\RegexOperator;
use MongoDB\Builder\Query\TypeOperator;
use MongoDB\Builder\Type\CombinedFieldQuery;
use MongoDB\Builder\Type\FieldQueryInterface;
use MongoDB\Builder\Type\QueryInterface;
Expand All @@ -25,6 +26,27 @@ final class Query
{
use Query\FactoryTrait {
regex as private generatedRegex;
type as private generatedType;
}

/**
* Selects documents if a field is of the specified type.
*
* @see https://www.mongodb.com/docs/manual/reference/operator/query/type/
*
* @param int|non-empty-string|QueryType ...$type
*
* @no-named-arguments
*/
public static function type(string|int|QueryType ...$type): TypeOperator
{
foreach ($type as &$value) {
if ($value instanceof QueryType) {
$value = $value->value;
}
}

return self::generatedType(...$type);
}

/**
Expand Down
46 changes: 46 additions & 0 deletions src/Builder/QueryType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace MongoDB\Builder;

use MongoDB\Builder\Query\TypeOperator;
use MongoDB\Builder\Type\Encode;
use MongoDB\Builder\Type\FieldQueryInterface;
use MongoDB\Builder\Type\OperatorInterface;

/**
* Shortcut for $type field query operator
*
* @see https://www.mongodb.com/docs/manual/reference/operator/query/type/#available-types
* @see TypeOperator
*/
enum QueryType: string implements OperatorInterface, FieldQueryInterface
{
public const ENCODE = Encode::Array;
Copy link
Member

Choose a reason for hiding this comment

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

Is this the first time a BackedEnum is being used for an encoding type? If so, perhaps it'd be prudent to have BuilderEncoder::encode() assert that that $value::ENCODE is Encode::Array (and you can revise that as needed should another use case come up).

That would address my question about how a BackedEnum might be handled for other switch cases.

Copy link
Member Author

@GromNaN GromNaN Jan 22, 2024

Choose a reason for hiding this comment

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

We need to rework the whole encoder class. The "ENCODE" constant will not stay, it may become a method of the Operator interface.

Copy link
Member

Choose a reason for hiding this comment

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

If this isn't already ticketed please do so and drop a reference to the issue.


case Array = 'array';
case Binary = 'binData';
case Bool = 'bool';
case Decimal64 = 'double';
case Decimal128 = 'decimal';
case Int32 = 'int';
case Int64 = 'long';
case Javascript = 'javascript';
case Object = 'object';
case ObjectId = 'objectId';
case MaxKey = 'maxKey';
case MinKey = 'minKey';
case Null = 'null';
/** Alias for Int32, Int64, Decimal64, Decimal128 */
case Number = 'number';
case Regex = 'regex';
case String = 'string';
case Timestamp = 'timestamp';
case UTCDateTime = 'date';

public function getOperator(): string
{
return '$type';
}
}
77 changes: 77 additions & 0 deletions tests/Builder/Expression/MetaOperatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace MongoDB\Tests\Builder\Expression;

use MongoDB\Builder\Accumulator;
use MongoDB\Builder\Expression;
use MongoDB\Builder\Meta;
use MongoDB\Builder\Pipeline;
use MongoDB\Builder\Query;
use MongoDB\Builder\Stage;
use MongoDB\Tests\Builder\PipelineTestCase;

/**
* Test $meta expression
*/
class MetaOperatorTest extends PipelineTestCase
{
public function testIndexKey(): void
{
$pipeline = new Pipeline(
Stage::match(
type: 'apparel',
),
Stage::addFields(
idxKey: Expression::meta('indexKey'),
),
);

$this->assertSamePipeline(Pipelines::MetaIndexKey, $pipeline);
}

public function testIndexKeyWithEnum(): void
{
$pipeline = new Pipeline(
Stage::match(
type: 'apparel',
),
Stage::addFields(
idxKey: Meta::IndexKey,
),
);

$this->assertSamePipeline(Pipelines::MetaIndexKey, $pipeline);
}

public function testTextScore(): void
{
$pipeline = new Pipeline(
Stage::match(
Query::text('cake'),
),
Stage::group(
_id: Expression::meta('textScore'),
count: Accumulator::sum(1),
),
);

$this->assertSamePipeline(Pipelines::MetaTextScore, $pipeline);
}

public function testTextScoreWithEnum(): void
{
$pipeline = new Pipeline(
Stage::match(
Query::text('cake'),
),
Stage::group(
_id: Meta::TextScore,
count: Accumulator::sum(1),
),
);

$this->assertSamePipeline(Pipelines::MetaTextScore, $pipeline);
}
}
51 changes: 51 additions & 0 deletions tests/Builder/Expression/Pipelines.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions tests/Builder/Query/TypeOperatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use MongoDB\Builder\Pipeline;
use MongoDB\Builder\Query;
use MongoDB\Builder\QueryType;
use MongoDB\Builder\Stage;
use MongoDB\Tests\Builder\PipelineTestCase;

Expand All @@ -32,16 +33,16 @@ public function testQueryingByDataType(): void
zipCode: Query::type(2),
),
Stage::match(
zipCode: Query::type('string'),
zipCode: QueryType::String,
Copy link
Member

Choose a reason for hiding this comment

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

Huh, this is interesting. I had initially considered using an enum case as the argument for Query::type, but this is interesting as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

I can customize the Query::type factory to accept enum values and convert them to string if necessary.

Copy link
Member

Choose a reason for hiding this comment

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

Not sure if that is necessary. This allows people to do whatever they want with the $type operator, but provides a nice shorthand for all known types.

Copy link
Member

Choose a reason for hiding this comment

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

I assume this only works because QueryType is both an enum and an OperatorInterface (and FieldQueryInterface in that it's associated with a field path). I assume this convenience only works for a single type check, though. Trying to match zipCode with [QueryType::String, ...] in order to match multiple types would produce invalid syntax, correct?

And that's where Query::type(...) comes in.

),
Stage::match(
zipCode: Query::type(1),
),
Stage::match(
zipCode: Query::type('double'),
zipCode: Query::type(QueryType::Decimal64),
),
Stage::match(
zipCode: Query::type('number'),
zipCode: QueryType::Number,
),
);

Expand Down Expand Up @@ -69,7 +70,7 @@ public function testQueryingByMultipleDataType(): void
zipCode: Query::type(2, 1),
),
Stage::match(
zipCode: Query::type('string', 'double'),
zipCode: Query::type(QueryType::String, 'double'),
),
);

Expand Down