diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 65c55562..238c386c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -88,9 +88,9 @@ jobs: symfony-version: '7.0.*' coverage-mode: 'xdebug' - name: "Run tests with phpunit/phpunit" - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | vendor/bin/phpunit --testsuite=Code --coverage-clover coverage.xml - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/src/batch-doctrine-dbal/src/DoctrineDBALJobExecutionStorage.php b/src/batch-doctrine-dbal/src/DoctrineDBALJobExecutionStorage.php index 21c8f278..b5bceb8e 100644 --- a/src/batch-doctrine-dbal/src/DoctrineDBALJobExecutionStorage.php +++ b/src/batch-doctrine-dbal/src/DoctrineDBALJobExecutionStorage.php @@ -175,6 +175,38 @@ public function query(Query $query): iterable $queryTypes['statuses'] = Connection::PARAM_INT_ARRAY; } + if ($query->startTime()) { + $qb->andWhere($qb->expr()->isNotNull('start_time')); + } + $startDateFrom = $query->startTime()?->getFrom(); + if ($startDateFrom) { + $qb->andWhere($qb->expr()->gte('start_time', ':startDateFrom')); + $queryParameters['startDateFrom'] = $startDateFrom; + $queryTypes['startDateFrom'] = Types::DATETIME_IMMUTABLE; + } + $startDateTo = $query->startTime()?->getTo(); + if ($startDateTo) { + $qb->andWhere($qb->expr()->lte('start_time', ':startDateTo')); + $queryParameters['startDateTo'] = $startDateTo; + $queryTypes['startDateTo'] = Types::DATETIME_IMMUTABLE; + } + + if ($query->endTime()) { + $qb->andWhere($qb->expr()->isNotNull('start_time')); + } + $endDateFrom = $query->endTime()?->getFrom(); + if ($endDateFrom) { + $qb->andWhere($qb->expr()->gte('end_time', ':endDateFrom')); + $queryParameters['endDateFrom'] = $endDateFrom; + $queryTypes['endDateFrom'] = Types::DATETIME_IMMUTABLE; + } + $endDateTo = $query->endTime()?->getTo(); + if ($endDateTo) { + $qb->andWhere($qb->expr()->lte('end_time', ':endDateTo')); + $queryParameters['endDateTo'] = $endDateTo; + $queryTypes['endDateTo'] = Types::DATETIME_IMMUTABLE; + } + switch ($query->sort()) { case Query::SORT_BY_START_ASC: $qb->orderBy('start_time', 'asc'); diff --git a/src/batch-doctrine-dbal/tests/DoctrineDBALJobExecutionStorageTest.php b/src/batch-doctrine-dbal/tests/DoctrineDBALJobExecutionStorageTest.php index 027ad8db..7ba5daca 100644 --- a/src/batch-doctrine-dbal/tests/DoctrineDBALJobExecutionStorageTest.php +++ b/src/batch-doctrine-dbal/tests/DoctrineDBALJobExecutionStorageTest.php @@ -357,6 +357,53 @@ public function queries(): Generator ['import', '789'], ], ]; + yield 'Filter start time lower boundary' => [ + (new QueryBuilder()) + ->startTime(new \DateTimeImmutable('2019-07-01T13:00:01+0200'), null), + [ + ['import', '456'], + ], + ]; + yield 'Filter start time upper boundary' => [ + (new QueryBuilder()) + ->startTime(null, new \DateTimeImmutable('2019-06-30T22:00:00+0200')), + [ + ['import', '789'], + ], + ]; + yield 'Filter start time boundaries' => [ + (new QueryBuilder()) + ->startTime( + new \DateTimeImmutable('2019-07-01T13:00:01+0200'), + new \DateTimeImmutable('2019-07-01T17:29:29+0200'), + ), + [ + // none + ], + ]; + yield 'Filter end time lower boundary' => [ + (new QueryBuilder()) + ->endTime(new \DateTimeImmutable('2019-07-01T13:30:01+0200'), null), + [ + ['import', '456'], + ], + ]; + yield 'Filter end time upper boundary' => [ + (new QueryBuilder()) + ->endTime(null, new \DateTimeImmutable('2019-07-01T18:29:59+0200')), + [ + ['export', '123'], + ], + ]; + yield 'Filter end time boundaries' => [ + (new QueryBuilder()) + ->endTime( + new \DateTimeImmutable('2019-07-01T13:30:01+0200'), + new \DateTimeImmutable('2019-07-01T18:29:59+0200'), + ), + [ + ], + ]; } public static function assertExecutionIds(array $ids, iterable $executions): void diff --git a/src/batch/src/Storage/FilesystemJobExecutionStorage.php b/src/batch/src/Storage/FilesystemJobExecutionStorage.php index 1a7646a7..51581b59 100644 --- a/src/batch/src/Storage/FilesystemJobExecutionStorage.php +++ b/src/batch/src/Storage/FilesystemJobExecutionStorage.php @@ -113,6 +113,26 @@ public function query(Query $query): iterable continue; } + $startTime = $execution->getStartTime(); + $startDateFrom = $query->startTime()?->getFrom(); + if ($startDateFrom !== null && ($startTime === null || $startTime < $startDateFrom)) { + continue; + } + $startDateTo = $query->startTime()?->getTo(); + if ($startDateTo !== null && ($startTime === null || $startTime > $startDateTo)) { + continue; + } + + $endTime = $execution->getEndTime(); + $endDateFrom = $query->endTime()?->getFrom(); + if ($endDateFrom !== null && ($endTime === null || $endTime < $endDateFrom)) { + continue; + } + $endDateTo = $query->endTime()?->getTo(); + if ($endDateTo !== null && ($endTime === null || $endTime > $endDateTo)) { + continue; + } + $candidates[] = $execution; } diff --git a/src/batch/src/Storage/Query.php b/src/batch/src/Storage/Query.php index 2ac17b0a..f022c3aa 100644 --- a/src/batch/src/Storage/Query.php +++ b/src/batch/src/Storage/Query.php @@ -33,9 +33,11 @@ public function __construct( * @var int[] */ private array $statuses, + private ?TimeFilter $startTime, + private ?TimeFilter $endTime, private ?string $sort, private int $limit, - private int $offset = 0, + private int $offset, ) { } @@ -63,6 +65,16 @@ public function statuses(): array return $this->statuses; } + public function startTime(): ?TimeFilter + { + return $this->startTime; + } + + public function endTime(): ?TimeFilter + { + return $this->endTime; + } + public function sort(): ?string { return $this->sort; diff --git a/src/batch/src/Storage/QueryBuilder.php b/src/batch/src/Storage/QueryBuilder.php index c2602b7a..35e70c75 100644 --- a/src/batch/src/Storage/QueryBuilder.php +++ b/src/batch/src/Storage/QueryBuilder.php @@ -4,6 +4,7 @@ namespace Yokai\Batch\Storage; +use DateTimeInterface; use Yokai\Batch\BatchStatus; use Yokai\Batch\Exception\UnexpectedValueException; @@ -16,6 +17,8 @@ * ->ids(['123', '456']) * ->jobs(['export', 'import']) * ->statuses([BatchStatus::RUNNING, BatchStatus::COMPLETED]) + * ->startTime(new \DateTimeImmutable('2023-07-07 15:18'), new \DateTime('2023-07-07 16:30')) + * ->endTime(new \DateTimeImmutable('2023-07-07 15:18'), new \DateTime('2023-07-07 16:30')) * ->sort(Query::SORT_BY_END_DESC) * ->limit(6, 12) * ->getQuery(); @@ -26,6 +29,8 @@ * $builder->ids(['123', '456']); * $builder->jobs(['export', 'import']); * $builder->statuses([BatchStatus::RUNNING, BatchStatus::COMPLETED]); + * $builder->startTime(new \DateTimeImmutable('2023-07-07 15:18'), new \DateTime('2023-07-07 16:30')); + * $builder->endTime(new \DateTimeImmutable('2023-07-07 15:18'), new \DateTime('2023-07-07 16:30')); * $builder->sort(Query::SORT_BY_END_DESC); * $builder->limit(6, 12); * $builder->getQuery(); @@ -63,6 +68,10 @@ final class QueryBuilder */ private array $statuses = []; + private ?TimeFilter $startTime = null; + + private ?TimeFilter $endTime = null; + private ?string $sortBy = null; private int $limit = 10; @@ -126,6 +135,44 @@ public function statuses(array $statuses): self return $this; } + /** + * Filter executions that started in a frame. + * Both frame boundaries are optional. + * Calling this method with both null boundaries result in removing the filter. + * + * @param DateTimeInterface|null $from Beginning of the time frame + * @param DateTimeInterface|null $to End of the time frame + */ + public function startTime(?DateTimeInterface $from, ?DateTimeInterface $to): self + { + if ($from === null && $to === null) { + $this->startTime = null; + } else { + $this->startTime = new TimeFilter($from, $to); + } + + return $this; + } + + /** + * Filter executions that ended in a frame. + * Both frame boundaries are optional. + * Calling this method with both null boundaries result in removing the filter. + * + * @param DateTimeInterface|null $from Beginning of the time frame + * @param DateTimeInterface|null $to End of the time frame + */ + public function endTime(?DateTimeInterface $from, ?DateTimeInterface $to): self + { + if ($from === null && $to === null) { + $this->endTime = null; + } else { + $this->endTime = new TimeFilter($from, $to); + } + + return $this; + } + /** * Sort executions. * @@ -165,6 +212,15 @@ public function limit(int $limit, int $offset): self */ public function getQuery(): Query { - return new Query($this->jobNames, $this->ids, $this->statuses, $this->sortBy, $this->limit, $this->offset); + return new Query( + $this->jobNames, + $this->ids, + $this->statuses, + $this->startTime, + $this->endTime, + $this->sortBy, + $this->limit, + $this->offset + ); } } diff --git a/src/batch/src/Storage/TimeFilter.php b/src/batch/src/Storage/TimeFilter.php new file mode 100644 index 00000000..ca617ea0 --- /dev/null +++ b/src/batch/src/Storage/TimeFilter.php @@ -0,0 +1,33 @@ + $to) { + throw new UnexpectedValueException('TimeFilter expect "from" boundary to be lower than "to" boundary.'); + } + } + + public function getFrom(): ?DateTimeInterface + { + return $this->from; + } + + public function getTo(): ?DateTimeInterface + { + return $this->to; + } +} diff --git a/src/batch/tests/Storage/FilesystemJobExecutionStorageTest.php b/src/batch/tests/Storage/FilesystemJobExecutionStorageTest.php index 3d34c8a2..bb945a9b 100644 --- a/src/batch/tests/Storage/FilesystemJobExecutionStorageTest.php +++ b/src/batch/tests/Storage/FilesystemJobExecutionStorageTest.php @@ -232,6 +232,62 @@ public function query(): \Generator ['list', '20210910'], ], ]; + yield 'Filter start time lower boundary' => [ + (new QueryBuilder()) + ->startTime(new \DateTimeImmutable('2021-09-20T10:35:48+0200'), null), + [ + ['export', '20210920'], + ['export', '20210922'], + ['list', '20210920'], + ], + ]; + yield 'Filter start time upper boundary' => [ + (new QueryBuilder()) + ->startTime(null, new \DateTimeImmutable('2021-09-20T10:35:50+0200')), + [ + ['export', '20210920'], + ['list', '20210910'], + ['list', '20210915'], + ], + ]; + yield 'Filter start time boundaries' => [ + (new QueryBuilder()) + ->startTime( + new \DateTimeImmutable('2021-09-20T10:35:48+0200'), + new \DateTimeImmutable('2021-09-20T10:35:50+0200'), + ), + [ + ['export', '20210920'], + ], + ]; + yield 'Filter end time lower boundary' => [ + (new QueryBuilder()) + ->endTime(new \DateTimeImmutable('2021-09-20T10:35:48+0200'), null), + [ + ['export', '20210920'], + ['export', '20210922'], + ['list', '20210920'], + ], + ]; + yield 'Filter end time upper boundary' => [ + (new QueryBuilder()) + ->endTime(null, new \DateTimeImmutable('2021-09-20T10:35:50+0200')), + [ + ['export', '20210920'], + ['list', '20210910'], + ['list', '20210915'], + ], + ]; + yield 'Filter end time boundaries' => [ + (new QueryBuilder()) + ->endTime( + new \DateTimeImmutable('2021-09-20T10:35:48+0200'), + new \DateTimeImmutable('2021-09-20T10:35:50+0200'), + ), + [ + ['export', '20210920'], + ], + ]; } public function testRetrieveFilePathNotFound(): void diff --git a/src/batch/tests/Storage/QueryBuilderTest.php b/src/batch/tests/Storage/QueryBuilderTest.php index 75af5bdd..daefe288 100644 --- a/src/batch/tests/Storage/QueryBuilderTest.php +++ b/src/batch/tests/Storage/QueryBuilderTest.php @@ -10,6 +10,7 @@ use Yokai\Batch\Exception\UnexpectedValueException; use Yokai\Batch\Storage\Query; use Yokai\Batch\Storage\QueryBuilder; +use Yokai\Batch\Storage\TimeFilter; class QueryBuilderTest extends TestCase { @@ -24,6 +25,10 @@ public function testValid(callable $factory, Query $expected): void self::assertSame($expected->jobs(), $actual->jobs()); self::assertSame($expected->ids(), $actual->ids()); self::assertSame($expected->statuses(), $actual->statuses()); + self::assertSame($expected->startTime()?->getFrom(), $actual->startTime()?->getFrom()); + self::assertSame($expected->startTime()?->getTo(), $actual->startTime()?->getTo()); + self::assertSame($expected->endTime()?->getFrom(), $actual->endTime()?->getFrom()); + self::assertSame($expected->endTime()?->getTo(), $actual->endTime()?->getTo()); self::assertSame($expected->sort(), $actual->sort()); self::assertSame($expected->limit(), $actual->limit()); self::assertSame($expected->offset(), $actual->offset()); @@ -35,44 +40,133 @@ public function valid(): \Generator $jobNames = []; $ids = []; $statuses = []; + $startTime = null; + $endTime = null; $sortBy = null; $limit = 10; $offset = 0; yield 'Query job names' => [ fn() => (new QueryBuilder())->jobs(['job1', 'job2']), - new Query(['job1', 'job2'], $ids, $statuses, $sortBy, $limit, $offset), + new Query(['job1', 'job2'], $ids, $statuses, $startTime, $endTime, $sortBy, $limit, $offset), ]; yield 'Query job ids' => [ fn() => (new QueryBuilder())->ids(['id1', 'id2', 'id3']), - new Query($jobNames, ['id1', 'id2', 'id3'], $statuses, $sortBy, $limit, $offset), + new Query($jobNames, ['id1', 'id2', 'id3'], $statuses, $startTime, $endTime, $sortBy, $limit, $offset), ]; yield 'Query job statuses' => [ fn() => (new QueryBuilder())->statuses([BatchStatus::ABANDONED, BatchStatus::STOPPED]), - new Query($jobNames, $ids, [BatchStatus::ABANDONED, BatchStatus::STOPPED], $sortBy, $limit, $offset), + new Query( + jobs: $jobNames, + ids: $ids, + statuses: [BatchStatus::ABANDONED, BatchStatus::STOPPED], + startTime: $startTime, + endTime: $endTime, + sort: $sortBy, + limit: $limit, + offset: $offset + ), ]; yield 'Query with sort' => [ fn() => (new QueryBuilder())->sort(Query::SORT_BY_START_DESC), - new Query($jobNames, $ids, $statuses, Query::SORT_BY_START_DESC, $limit, $offset), + new Query( + jobs: $jobNames, + ids: $ids, + statuses: $statuses, + startTime: $startTime, + endTime: $endTime, + sort: Query::SORT_BY_START_DESC, + limit: $limit, + offset: $offset + ), ]; yield 'Query with limit' => [ fn() => (new QueryBuilder())->limit(30, 60), - new Query($jobNames, $ids, $statuses, $sortBy, 30, 60), + new Query( + jobs: $jobNames, + ids: $ids, + statuses: $statuses, + startTime: $startTime, + endTime: $endTime, + sort: $sortBy, + limit: 30, + offset: 60 + ), + ]; + $startTimeFrom = new \DateTimeImmutable('2023-07-07 15:18'); + $startTimeTo = new \DateTime('2023-07-07 16:30'); + yield 'Query with start time boundary' => [ + fn() => (new QueryBuilder())->startTime($startTimeFrom, $startTimeTo), + new Query( + jobs: $jobNames, + ids: $ids, + statuses: $statuses, + startTime: new TimeFilter($startTimeFrom, $startTimeTo), + endTime: null, + sort: $sortBy, + limit: $limit, + offset: $offset + ), + ]; + yield 'Query with start time boundary reset' => [ + fn() => (new QueryBuilder())->startTime($startTimeFrom, $startTimeTo)->startTime(null, null), + new Query( + jobs: $jobNames, + ids: $ids, + statuses: $statuses, + startTime: null, + endTime: null, + sort: $sortBy, + limit: $limit, + offset: $offset + ), + ]; + $endTimeFrom = new \DateTimeImmutable('2023-07-07 15:18'); + $endTimeTo = new \DateTime('2023-07-07 16:30'); + yield 'Query with end time boundary' => [ + fn() => (new QueryBuilder())->endTime($endTimeFrom, $endTimeTo), + new Query( + jobs: $jobNames, + ids: $ids, + statuses: $statuses, + startTime: null, + endTime: new TimeFilter($endTimeFrom, $endTimeTo), + sort: $sortBy, + limit: $limit, + offset: $offset + ), + ]; + yield 'Query with end time boundary reset' => [ + fn() => (new QueryBuilder())->endTime($endTimeFrom, $endTimeTo)->endTime(null, null), + new Query( + jobs: $jobNames, + ids: $ids, + statuses: $statuses, + startTime: null, + endTime: null, + sort: $sortBy, + limit: $limit, + offset: $offset + ), ]; yield 'Query complex' => [ fn() => (new QueryBuilder()) ->ids(['123', '456']) ->jobs(['export', 'import']) ->statuses([BatchStatus::RUNNING, BatchStatus::COMPLETED]) + ->startTime($startTimeFrom, $startTimeTo) + ->endTime($endTimeFrom, $endTimeTo) ->sort(Query::SORT_BY_END_DESC) ->limit(6, 12), new Query( - ['export', 'import'], - ['123', '456'], - [BatchStatus::RUNNING, BatchStatus::COMPLETED], - Query::SORT_BY_END_DESC, - 6, - 12 + jobs: ['export', 'import'], + ids: ['123', '456'], + statuses: [BatchStatus::RUNNING, BatchStatus::COMPLETED], + startTime: new TimeFilter($startTimeFrom, $startTimeTo), + endTime: new TimeFilter($endTimeFrom, $endTimeTo), + sort: Query::SORT_BY_END_DESC, + limit: 6, + offset: 12 ), ]; } @@ -130,5 +224,19 @@ public function invalid(): \Generator fn() => (new QueryBuilder())->limit(1, -1), UnexpectedValueException::min(0, -1), ]; + yield 'QueryBuilder::startTime with inversed boundaries' => [ + fn() => (new QueryBuilder())->startTime( + new \DateTimeImmutable('2024-01-01T00:00:00+0200'), + new \DateTimeImmutable('2023-12-31T23:59:59+0200'), + ), + new UnexpectedValueException('TimeFilter expect "from" boundary to be lower than "to" boundary.'), + ]; + yield 'QueryBuilder::endTime with inversed boundaries' => [ + fn() => (new QueryBuilder())->endTime( + new \DateTimeImmutable('2024-01-01T00:00:00+0200'), + new \DateTimeImmutable('2023-12-31T23:59:59+0200'), + ), + new UnexpectedValueException('TimeFilter expect "from" boundary to be lower than "to" boundary.'), + ]; } }