Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for timeseries #2597

Closed
wants to merge 9 commits into from
11 changes: 6 additions & 5 deletions lib/Doctrine/ODM/MongoDB/Mapping/Annotations/Validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;

/**
* @Annotation
Expand All @@ -21,18 +22,18 @@ class Validation implements Annotation
/**
* @var string|null
* @Enum({
* \Doctrine\ODM\MongoDB\Mapping\ClassMetadata::SCHEMA_VALIDATION_ACTION_ERROR,
* \Doctrine\ODM\MongoDB\Mapping\ClassMetadata::SCHEMA_VALIDATION_ACTION_WARN,
* ClassMetadata::SCHEMA_VALIDATION_ACTION_ERROR,
* ClassMetadata::SCHEMA_VALIDATION_ACTION_WARN,
* })
*/
public $action;

/**
* @var string|null
* @Enum({
* \Doctrine\ODM\MongoDB\Mapping\ClassMetadata::SCHEMA_VALIDATION_LEVEL_OFF,
* \Doctrine\ODM\MongoDB\Mapping\ClassMetadata::SCHEMA_VALIDATION_LEVEL_STRICT,
* \Doctrine\ODM\MongoDB\Mapping\ClassMetadata::SCHEMA_VALIDATION_LEVEL_MODERATE,
* ClassMetadata::SCHEMA_VALIDATION_LEVEL_OFF,
* ClassMetadata::SCHEMA_VALIDATION_LEVEL_STRICT,
* ClassMetadata::SCHEMA_VALIDATION_LEVEL_MODERATE,
* })
*/
public $level;
Expand Down
55 changes: 50 additions & 5 deletions lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,20 @@
*/
public $collectionMax;

/**
* READ-ONLY: If the collection should be a timeseries
*
* @var array{timeField:string,metaField?:string,granularity?:string}|null
*/
public $collectionTimeseries;

/**
* READ-ONLY: If the collection is timeseries, sets automatic removal for the collection.
*
* @var int|null
*/
public $collectionExpireAfterSeconds;

/**
* READ-ONLY Describes how MongoDB clients route read operations to the members of a replica set.
*
Expand Down Expand Up @@ -653,6 +667,13 @@
*/
public $isMappedSuperclass = false;

/**
* READ-ONLY: Whether this class describes the mapping of Timeseries.
*
* @var bool
*/
public $isTimeSeriesDocument = false;

/**
* READ-ONLY: Whether this class describes the mapping of a embedded document.
*
Expand Down Expand Up @@ -1387,7 +1408,7 @@
* Sets the collection this Document is mapped to.
*
* @param array|string $name
* @psalm-param array{name: string, capped?: bool, size?: int, max?: int}|string $name
* @psalm-param array{name: string, capped?: bool, size?: int, max?: int}|array{name: string, timeseries?: array{timeField:string,metaField?:string,granularity?:string}, expireAfterSeconds?: int}|string $name
*
* @throws InvalidArgumentException
*/
Expand All @@ -1398,10 +1419,19 @@
throw new InvalidArgumentException('A name key is required when passing an array to setCollection()');
}

$this->collectionCapped = $name['capped'] ?? false;
$this->collectionSize = $name['size'] ?? 0;
$this->collectionMax = $name['max'] ?? 0;
$this->collection = $name['name'];
if (array_key_exists('timeseries', $name)) {
$this->collectionTimeseries = [
'timeField' => $name['timeseries']['timeField'],
'metaField' => $name['timeseries']['metaField'] ?? null,
'granularity' => $name['timeseries']['granularity'] ?? null,

Check failure on line 1426 in lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (8.2)

InvalidPropertyAssignmentValue

lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php:1426:55: InvalidPropertyAssignmentValue: $this->collectionTimeseries with declared type 'array{granularity?: string, metaField?: string, timeField: string}|null' cannot be assigned type 'array{granularity: null|string, metaField: null|string, timeField: string}' (see https://psalm.dev/145)
];
$this->collectionExpireAfterSeconds = $name['expireAfterSeconds'] ?? null;
} else {
$this->collectionCapped = $name['capped'] ?? false;
$this->collectionSize = $name['size'] ?? 0;
$this->collectionMax = $name['max'] ?? 0;
$this->collection = $name['name'];
}
} else {
$this->collection = $name;
}
Expand Down Expand Up @@ -1476,6 +1506,17 @@
$this->collectionMax = $max;
}

/** @return array|null */
public function getCollectionTimeseries(): ?array
{
return $this->collectionTimeseries;
}

Check failure on line 1513 in lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (8.2)

Method Doctrine\ODM\MongoDB\Mapping\ClassMetadata::getCollectionTimeseries() return type has no value type specified in iterable type array.

public function getCollectionExpireAfterSeconds(): ?int
{
return $this->collectionExpireAfterSeconds;
}

/**
* Returns TRUE if this Document is mapped to a collection FALSE otherwise.
*/
Expand Down Expand Up @@ -2405,7 +2446,7 @@
* That means any metadata properties that are not set or empty or simply have
* their default value are NOT serialized.
*
* Parts that are also NOT serialized because they can not be properly unserialized:

Check failure on line 2449 in lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (8.2)

Parameter #2 $enumType of class Doctrine\Persistence\Reflection\EnumReflectionProperty constructor expects class-string<BackedEnum>, class-string<UnitEnum> given.
* - reflClass (ReflectionClass)
* - reflFields (ReflectionProperty array)
*
Expand Down Expand Up @@ -2451,6 +2492,10 @@
$serialized[] = 'subClasses';
}

if ($this->isTimeSeriesDocument) {
$serialized[] = 'isTimeSeriesDocument';
}

if ($this->isMappedSuperclass) {
$serialized[] = 'isMappedSuperclass';
}
Expand Down Expand Up @@ -2653,7 +2698,7 @@
$type = $this->reflClass->getProperty($mapping['fieldName'])->getType();

if (! $type instanceof ReflectionNamedType) {
return $mapping;

Check failure on line 2701 in lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (8.2)

Method Doctrine\ODM\MongoDB\Mapping\ClassMetadata::validateAndCompleteTypedFieldMapping() should return array{type?: string, fieldName?: string, name?: string, strategy?: string, association?: int, id?: bool, isOwningSide?: bool, collectionClass?: class-string, ...} but returns array{type?: string, fieldName?: string, name?: string, strategy?: string, association?: int, id?: bool, isOwningSide?: bool, collectionClass?: class-string, ...}.
}

if (! isset($mapping['collectionClass']) && class_exists($type->getName())) {
Expand Down
4 changes: 4 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use ReflectionClass;
use ReflectionMethod;

use function array_key_exists;
use function array_merge;
use function array_replace;
use function assert;
Expand Down Expand Up @@ -179,6 +180,9 @@

if (isset($documentAnnot->collection)) {
$metadata->setCollection($documentAnnot->collection);
if (is_array($documentAnnot->collection) && array_key_exists('timeseries', $documentAnnot->collection)) {
$metadata->isTimeSeriesDocument = true;
}
}

if (isset($documentAnnot->view)) {
Expand Down Expand Up @@ -336,7 +340,7 @@
}

$options = array_merge($options, $index->options);
$class->addIndex($keys, $options);

Check failure on line 343 in lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (8.2)

InvalidArgument

lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php:343:33: InvalidArgument: Argument 2 of Doctrine\ODM\MongoDB\Mapping\ClassMetadata::addIndex expects array{background?: bool, bits?: int, default_language?: string, expireAfterSeconds?: int, language_override?: string, max?: float, min?: float, name?: string, partialFilterExpression?: array<array-key, mixed>, sparse?: bool, storageEngine?: array<array-key, mixed>, textIndexVersion?: int, unique?: bool, weights?: list{string, int}}, but array{background?: mixed, expireAfterSeconds?: mixed, name?: mixed, partialFilterExpression?: mixed|non-empty-array<string, mixed>, sparse?: mixed, unique?: mixed, ...<array-key, mixed>} provided (see https://psalm.dev/004)
}

/**
Expand Down
13 changes: 13 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ public function loadMetadataForClass($className, \Doctrine\Persistence\Mapping\C
$config['size'] = (int) $xmlRoot['capped-collection-size'];
}

$metadata->setCollection($config);
} elseif (isset($xmlRoot['timeseries-collection'])) {
$config = ['name' => (string) $xmlRoot['collection']];
$config['timeseries'] = [
'timeField' => (string) $xmlRoot['timeseries-collection-timefield'],
'metaField' => (string) $xmlRoot['timeseries-collection-meta'],
'granularity' => (string) $xmlRoot['timeseries-collection-granularity'],
];

if (isset($xmlRoot['expireAfterSeconds'])) {
$config['expireAfterSeconds'] = (int) $xmlRoot['expireAfterSeconds'];
}

$metadata->setCollection($config);
} else {
$metadata->setCollection((string) $xmlRoot['collection']);
Expand Down
4 changes: 3 additions & 1 deletion lib/Doctrine/ODM/MongoDB/SchemaManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ public function updateValidators(?int $maxTimeMs = null, ?WriteConcern $writeCon
public function updateDocumentValidator(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null): void
{
$class = $this->dm->getClassMetadata($documentName);
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView() || $class->isFile) {
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView() || $class->isFile || $class->isTimeSeriesDocument) {

Choose a reason for hiding this comment

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

Checkout out updateValidators, also needs the condition to be extended

throw new InvalidArgumentException('Cannot update validators for files, views, mapped super classes, embedded documents or aggregation result documents.');

Choose a reason for hiding this comment

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

Message needs to be changed

}

Expand Down Expand Up @@ -447,6 +447,8 @@ public function createDocumentCollection(string $documentName, ?int $maxTimeMs =
}

$options = [
'timeseries' => $class->getCollectionTimeseries(),
'expireAfterSeconds' => $class->getCollectionExpireAfterSeconds(),
'capped' => $class->getCollectionCapped(),
'size' => $class->getCollectionSize(),
'max' => $class->getCollectionMax(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Tests\Functional;

use DateTime;
use DateTimeInterface;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Doctrine\ODM\MongoDB\Tests\BaseTestCase;

class TimeseriesCollectionsTest extends BaseTestCase
{
public function testCreateTimeseriesCollectionsCapped(): void
{
$sm = $this->dm->getSchemaManager();
$sm->dropDocumentCollection(TimeseriesCollectionTestDocument::class);
$sm->createDocumentCollection(TimeseriesCollectionTestDocument::class);

$coll = $this->dm->getDocumentCollection(TimeseriesCollectionTestDocument::class);
$document = new TimeseriesCollectionTestDocument();
$document->createdAt = new DateTime('2023-12-09 00:00:00');
$document->metaData = ['foo' => 'bar'];
$document->value = 1337;
$this->dm->persist($document);
$this->dm->flush();

$data = $coll->find()->toArray();
self::assertCount(1, $data);
}
}

/**
* @ODM\Document(collection={
* "name"="TimeseriesCollectionTest",
* "capped"=true,
* "size"=1000,
* "max"=1,
* "timeseries"={
* "timeField"="createdAt",
* "metaField"="metaData",
* "granularity"="seconds",
* },
* "expireAfterSeconds"=60
* })
*/
class TimeseriesCollectionTestDocument
{
/** @ODM\Id */
public ?string $id;

/** @ODM\Field(type="date") */
public ?DateTimeInterface $createdAt;

/**
* @ODM\Field(type="hash")
*
* @var array|null
*/
public ?array $metaData;

Check failure on line 60 in tests/Doctrine/ODM/MongoDB/Tests/Functional/TimeseriesCollectionsTest.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (8.2)

Property Doctrine\ODM\MongoDB\Tests\Functional\TimeseriesCollectionTestDocument::$metaData type has no value type specified in iterable type array.

/** @ODM\Field(type="integer") */
public ?int $value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class DoctrineGlobal_Article
protected $author;

/**
* @ODM\ReferenceMany(targetDocument=\DoctrineGlobal_User::class)
* @ODM\ReferenceMany(targetDocument=DoctrineGlobal_User::class)
*
* @var Collection<int, DoctrineGlobal_User>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,8 @@ private function createMockedDocumentManager(): DocumentManager

class DocumentNotFoundListener
{
private Closure $closure;

public function __construct(Closure $closure)
public function __construct(private Closure $closure)
{
$this->closure = $closure;
}

public function documentNotFound(DocumentNotFoundEventArgs $eventArgs): void
Expand Down
2 changes: 2 additions & 0 deletions tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,8 @@ public function testCreateDocumentCollectionWithValidator(array $expectedWriteOp
'validator' => $expectedValidator,
'validationAction' => ClassMetadata::SCHEMA_VALIDATION_ACTION_WARN,
'validationLevel' => ClassMetadata::SCHEMA_VALIDATION_LEVEL_MODERATE,
'timeseries' => null,
'expireAfterSeconds' => null,
];
$cm = $this->dm->getClassMetadata(SchemaValidated::class);
$database = $this->documentDatabases[$this->getDatabaseName($cm)];
Expand Down
Loading