Skip to content

Commit 18d7c18

Browse files
committed
Improve type of min/max bounds for range queries
1 parent 214999b commit 18d7c18

File tree

7 files changed

+183
-22
lines changed

7 files changed

+183
-22
lines changed

lib/Doctrine/ODM/MongoDB/Mapping/Annotations/Encrypt.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
namespace Doctrine\ODM\MongoDB\Mapping\Annotations;
66

77
use Attribute;
8+
use DateTimeInterface;
89
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
9-
use MongoDB\BSON\Type;
10+
use MongoDB\BSON\Decimal128;
11+
use MongoDB\BSON\Int64;
12+
use MongoDB\BSON\UTCDateTime;
1013

1114
/**
1215
* Defines an encrypted field mapping.
@@ -19,6 +22,9 @@
1922
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
2023
final class Encrypt implements Annotation
2124
{
25+
public int|float|Int64|Decimal128|UTCDateTime|null $min;
26+
public int|float|Int64|Decimal128|UTCDateTime|null $max;
27+
2228
/**
2329
* @param EncryptQuery|null $queryType Set the query type for the field, null if not queryable.
2430
* @param int<1, 4>|null $sparsity
@@ -28,12 +34,14 @@ final class Encrypt implements Annotation
2834
*/
2935
public function __construct(
3036
public ?EncryptQuery $queryType = null,
31-
public string|int|Type|null $min = null,
32-
public string|int|Type|null $max = null,
37+
int|float|Int64|Decimal128|UTCDateTime|DateTimeInterface|null $min = null,
38+
int|float|Int64|Decimal128|UTCDateTime|DateTimeInterface|null $max = null,
3339
public ?int $sparsity = null,
3440
public ?int $prevision = null,
3541
public ?int $trimFactor = null,
3642
public ?int $contention = null,
3743
) {
44+
$this->min = $min instanceof DateTimeInterface ? new UTCDateTime($min) : $min;
45+
$this->max = $max instanceof DateTimeInterface ? new UTCDateTime($max) : $max;
3846
}
3947
}

lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,20 @@
44

55
namespace Doctrine\ODM\MongoDB\Mapping\Driver;
66

7+
use DateTimeImmutable;
78
use Doctrine\ODM\MongoDB\Mapping\Annotations\TimeSeries;
89
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
910
use Doctrine\ODM\MongoDB\Mapping\MappingException;
1011
use Doctrine\ODM\MongoDB\Mapping\TimeSeries\Granularity;
12+
use Doctrine\ODM\MongoDB\Types\Type;
1113
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
1214
use Doctrine\Persistence\Mapping\Driver\FileDriver;
1315
use DOMDocument;
1416
use InvalidArgumentException;
1517
use LibXMLError;
18+
use MongoDB\BSON\Decimal128;
1619
use MongoDB\BSON\Document;
20+
use MongoDB\BSON\UTCDateTime;
1721
use MongoDB\Driver\Exception\UnexpectedValueException;
1822
use SimpleXMLElement;
1923

@@ -313,24 +317,17 @@ public function loadMetadataForClass($className, \Doctrine\Persistence\Mapping\C
313317
if (isset($field->encrypt)) {
314318
$mapping['encrypt'] = [];
315319
foreach ($field->encrypt->attributes() as $encryptKey => $encryptValue) {
316-
switch ($encryptKey) {
317-
case 'queryType':
318-
$mapping['encrypt'][$encryptKey] = (string) $encryptValue;
319-
break;
320-
case 'min':
321-
case 'max':
322-
$mapping['encrypt'][$encryptKey] = match ($mapping['type']) {
323-
'int' => (int) $encryptValue,
324-
'string' => (string) $encryptValue,
325-
};
326-
break;
327-
case 'sparsity':
328-
case 'prevision':
329-
case 'trimFactor':
330-
case 'contention':
331-
$mapping['encrypt'][$encryptKey] = (int) $encryptValue;
332-
break;
333-
}
320+
$mapping['encrypt'][$encryptKey] = match ($encryptKey) {
321+
'queryType' => (string) $encryptValue,
322+
'min', 'max' => match ($mapping['type']) {
323+
Type::INT => (int) $encryptValue,
324+
Type::FLOAT => (float) $encryptValue,
325+
Type::DECIMAL128 => new Decimal128((string) $encryptValue),
326+
Type::DATE, Type::DATE_IMMUTABLE => new UTCDateTime(new DateTimeImmutable((string) $encryptValue)),
327+
default => null, // Invalid
328+
},
329+
'sparsity', 'prevision', 'trimFactor', 'contention' => (int) $encryptValue,
330+
};
334331
}
335332
}
336333

lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,4 +314,14 @@ public static function rootDocumentCannotBeEncrypted(string $className): self
314314
$className,
315315
));
316316
}
317+
318+
public static function invalidEncryptedQueryRangeType(string $className, string $fieldName, string $type): self
319+
{
320+
return new self(sprintf(
321+
'The field type "%s" for field "%s::%s" is not supported for "range" query on encrypted field.',
322+
$type,
323+
$className,
324+
$fieldName,
325+
));
326+
}
317327
}

tests/Doctrine/ODM/MongoDB/Tests/Mapping/Driver/AbstractDriverTestCase.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44

55
namespace Doctrine\ODM\MongoDB\Tests\Mapping\Driver;
66

7+
use DateTimeImmutable;
78
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
89
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
910
use Documents\Account;
1011
use Documents\Address;
1112
use Documents\Encryption\ClientCard;
1213
use Documents\Encryption\PatientRecord;
14+
use Documents\Encryption\RangeTypes;
1315
use Documents\Group;
1416
use Documents\Phonenumber;
1517
use Documents\Profile;
18+
use MongoDB\BSON\Decimal128;
19+
use MongoDB\BSON\UTCDateTime;
1620
use MongoDB\Driver\ClientEncryption;
1721
use PHPUnit\Framework\TestCase;
1822
use TestDocuments\EmbeddedDocument;
@@ -557,4 +561,34 @@ public function testEncryptEmbeddedDocumentMapping(): void
557561
self::assertArrayNotHasKey('encrypt', $classMetadata->fieldMappings['type']);
558562
self::assertArrayNotHasKey('encrypt', $classMetadata->fieldMappings['number']);
559563
}
564+
565+
public function testEncryptQueryRangeTypes(): void
566+
{
567+
$classMetadata = new ClassMetadata(RangeTypes::class);
568+
$this->driver->loadMetadataForClass(RangeTypes::class, $classMetadata);
569+
570+
self::assertEquals([
571+
'queryType' => ClientEncryption::QUERY_TYPE_RANGE,
572+
'min' => 5,
573+
'max' => 10,
574+
], $classMetadata->fieldMappings['intField']['encrypt']);
575+
576+
self::assertEquals([
577+
'queryType' => ClientEncryption::QUERY_TYPE_RANGE,
578+
'min' => 5.5,
579+
'max' => 10.5,
580+
], $classMetadata->fieldMappings['floatField']['encrypt']);
581+
582+
self::assertEquals([
583+
'queryType' => ClientEncryption::QUERY_TYPE_RANGE,
584+
'min' => new Decimal128('0.1'),
585+
'max' => new Decimal128('0.2'),
586+
], $classMetadata->fieldMappings['decimalField']['encrypt']);
587+
588+
self::assertEquals([
589+
'queryType' => ClientEncryption::QUERY_TYPE_RANGE,
590+
'min' => new UTCDateTime(new DateTimeImmutable('2000-01-01 00:00:00')),
591+
'max' => new UTCDateTime(new DateTimeImmutable('2100-01-01 00:00:00')),
592+
], $classMetadata->fieldMappings['dateField']['encrypt']);
593+
}
560594
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
6+
http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">
7+
8+
<document name="Documents\Encryption\RangeTypes">
9+
<id/>
10+
<field name="intField" type="int">
11+
<encrypt queryType="range" min="5" max="10"/>
12+
</field>
13+
<field name="floatField" type="float">
14+
<encrypt queryType="range" min="5.5" max="10.5"/>
15+
</field>
16+
<field name="decimalField" type="decimal128">
17+
<encrypt queryType="range" min="0.1" max="0.2"/>
18+
</field>
19+
<field name="dateField" type="date_immutable">
20+
<encrypt queryType="range" min="2000-01-01 00:00:00" max="2100-01-01 00:00:00"/>
21+
</field>
22+
<field name="intField" type="int">
23+
<encrypt queryType="range" min="5" max="10"/>
24+
</field>
25+
</document>
26+
27+
</doctrine-mongo-mapping>

tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptionFieldMapTest.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
namespace Doctrine\ODM\MongoDB\Tests\Tools;
66

7+
use DateTimeImmutable;
78
use Doctrine\ODM\MongoDB\Mapping\MappingException;
89
use Doctrine\ODM\MongoDB\Tests\BaseTestCase;
910
use Doctrine\ODM\MongoDB\Utility\EncryptionFieldMap;
1011
use Documents\Encryption\Client;
1112
use Documents\Encryption\InvalidRootEncrypt;
1213
use Documents\Encryption\Patient;
14+
use Documents\Encryption\RangeTypes;
15+
use MongoDB\BSON\Decimal128;
16+
use MongoDB\BSON\UTCDateTime;
1317

1418
class EncryptionFieldMapTest extends BaseTestCase
1519
{
@@ -38,7 +42,7 @@ public function testEncryptionFieldMap(): void
3842
],
3943
];
4044

41-
self::assertSame($expected, $encryptedFieldsMap);
45+
self::assertEquals($expected, $encryptedFieldsMap);
4246
}
4347

4448
public function testEncryptEmbeddedDocument(): void
@@ -62,6 +66,45 @@ public function testEncryptEmbeddedDocument(): void
6266
self::assertSame($expected, $encryptedFieldsMap);
6367
}
6468

69+
public function testVariousRangeTypes(): void
70+
{
71+
$factory = new EncryptionFieldMap($this->dm->getMetadataFactory());
72+
$encryptedFieldsMap = $factory->getEncryptionFieldMap(RangeTypes::class);
73+
74+
$expected = [
75+
[
76+
'path' => 'intField',
77+
'bsonType' => 'int',
78+
'keyId' => null,
79+
'queries' => ['queryType' => 'range', 'min' => 5, 'max' => 10],
80+
],
81+
[
82+
'path' => 'floatField',
83+
'bsonType' => 'float',
84+
'keyId' => null,
85+
'queries' => ['queryType' => 'range', 'min' => 5.5, 'max' => 10.5],
86+
],
87+
[
88+
'path' => 'decimalField',
89+
'bsonType' => 'decimal128',
90+
'keyId' => null,
91+
'queries' => ['queryType' => 'range', 'min' => new Decimal128('0.1'), 'max' => new Decimal128('0.2')],
92+
],
93+
[
94+
'path' => 'dateField',
95+
'bsonType' => 'date_immutable',
96+
'keyId' => null,
97+
'queries' => [
98+
'queryType' => 'range',
99+
'min' => new UTCDateTime(new DateTimeImmutable('2000-01-01 00:00:00')),
100+
'max' => new UTCDateTime(new DateTimeImmutable('2100-01-01 00:00:00')),
101+
],
102+
],
103+
];
104+
105+
self::assertEquals($expected, $encryptedFieldsMap);
106+
}
107+
65108
public function testRootDocumentsCannotBeEncrypted(): void
66109
{
67110
$this->expectException(MappingException::class);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Documents\Encryption;
6+
7+
use DateTimeImmutable;
8+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
9+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Encrypt;
10+
use Doctrine\ODM\MongoDB\Mapping\Annotations\EncryptQuery;
11+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Field;
12+
use Doctrine\ODM\MongoDB\Mapping\Annotations\Id;
13+
use Doctrine\ODM\MongoDB\Types\Type;
14+
use MongoDB\BSON\Decimal128;
15+
16+
/**
17+
* Test all supported types for range encrypted queries.
18+
*
19+
* @see https://www.mongodb.com/docs/manual/core/queryable-encryption/reference/supported-operations/#supported-and-unsupported-bson-types
20+
*/
21+
#[Document]
22+
class RangeTypes
23+
{
24+
#[Id]
25+
public string $id;
26+
27+
#[Field(type: Type::INT)]
28+
#[Encrypt(EncryptQuery::Range, min: 5, max: 10)]
29+
public int $intField;
30+
31+
#[Field(type: Type::FLOAT)]
32+
#[Encrypt(EncryptQuery::Range, min: 5.5, max: 10.5)]
33+
public float $floatField;
34+
35+
#[Field(type: Type::DECIMAL128)]
36+
#[Encrypt(EncryptQuery::Range, min: new Decimal128('0.1'), max: new Decimal128('0.2'))]
37+
public Decimal128 $decimalField;
38+
39+
#[Field(type: Type::DATE_IMMUTABLE)]
40+
#[Encrypt(EncryptQuery::Range, min: new DateTimeImmutable('2000-01-01 00:00:00'), max: new DateTimeImmutable('2100-01-01 00:00:00'))]
41+
public DateTimeImmutable $dateField;
42+
}

0 commit comments

Comments
 (0)