Use canonical UUID strings for PostgreSQL queries
This commit is contained in:
@@ -16,7 +16,7 @@ final readonly class PaginationCursor
|
||||
{
|
||||
public function __construct(
|
||||
public UuidV7 $id,
|
||||
public \DateTimeImmutable $date,
|
||||
public ?\DateTimeImmutable $date = null,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -26,24 +26,15 @@ final readonly class PaginationCursor
|
||||
*/
|
||||
public static function encode(array $item, PaginatorKeyset $keyset): string
|
||||
{
|
||||
$id = DataMapping::uuid($item, $keyset->id)->toString();
|
||||
$payload = [
|
||||
'id' => DataMapping::string($item, $keyset->id),
|
||||
];
|
||||
|
||||
if ($keyset->date !== null) {
|
||||
$date = DataMapping::dateTime($item, $keyset->date)->format('Y-m-d H:i:s');
|
||||
|
||||
return base64_encode(
|
||||
json_encode([
|
||||
'date' => $date,
|
||||
'id' => $id,
|
||||
], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
$payload['date'] = DataMapping::dateTime($item, $keyset->date)->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
return base64_encode(
|
||||
json_encode([
|
||||
'id' => $id,
|
||||
], JSON_THROW_ON_ERROR)
|
||||
);
|
||||
return base64_encode(json_encode($payload, JSON_THROW_ON_ERROR));
|
||||
|
||||
}
|
||||
|
||||
@@ -58,15 +49,25 @@ final readonly class PaginationCursor
|
||||
}
|
||||
|
||||
try {
|
||||
$data = json_decode(base64_decode($cursor), true, 512, JSON_THROW_ON_ERROR);
|
||||
$decoded = base64_decode($cursor, true);
|
||||
if ($decoded === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! is_array($data) || ! isset($data['date'], $data['id'])) {
|
||||
$data = json_decode($decoded, true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if (! is_array($data) || ! isset($data['id'])) {
|
||||
throw new \InvalidArgumentException('Invalid cursor format');
|
||||
}
|
||||
|
||||
$date = null;
|
||||
if (isset($data['date'])) {
|
||||
$date = new \DateTimeImmutable($data['date']);
|
||||
}
|
||||
|
||||
return new self(
|
||||
id: UuidV7::fromString($data['id']),
|
||||
date: new \DateTimeImmutable($data['date'])
|
||||
date: $date,
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
|
||||
+38
-10
@@ -8,6 +8,7 @@ use Basango\SharedKernel\Domain\Model\Pagination\Page;
|
||||
use Basango\SharedKernel\Domain\Model\Pagination\PaginationCursor;
|
||||
use Basango\SharedKernel\Domain\Model\Pagination\PaginationInfo;
|
||||
use Basango\SharedKernel\Domain\Model\Pagination\PaginatorKeyset;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\SortDirection;
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
|
||||
/**
|
||||
@@ -17,35 +18,62 @@ use Doctrine\DBAL\Query\QueryBuilder;
|
||||
*/
|
||||
trait PaginationQuery
|
||||
{
|
||||
public function createPaginationInfo(array $data, Page $page, PaginatorKeyset $keyset): PaginationInfo
|
||||
public function createPaginationInfo(array &$data, Page $page, PaginatorKeyset $keyset): PaginationInfo
|
||||
{
|
||||
$paginationInfo = PaginationInfo::from($page);
|
||||
if ($data === []) {
|
||||
return $paginationInfo;
|
||||
}
|
||||
|
||||
$paginationInfo->cursor = PaginationCursor::encode(array_pop($data), $keyset);
|
||||
$paginationInfo->hasNext = count($data) > $page->limit;
|
||||
$hasNext = count($data) > $page->limit;
|
||||
if ($hasNext) {
|
||||
array_pop($data);
|
||||
}
|
||||
|
||||
$cursorSource = end($data);
|
||||
|
||||
if (is_array($cursorSource)) {
|
||||
$paginationInfo->cursor = PaginationCursor::encode($cursorSource, $keyset);
|
||||
}
|
||||
|
||||
$paginationInfo->hasNext = $hasNext;
|
||||
reset($data);
|
||||
|
||||
return $paginationInfo;
|
||||
}
|
||||
|
||||
public function applyCursorPagination(QueryBuilder $qb, Page $page, PaginatorKeyset $keyset): QueryBuilder
|
||||
{
|
||||
public function applyCursorPagination(
|
||||
QueryBuilder $qb,
|
||||
Page $page,
|
||||
PaginatorKeyset $keyset,
|
||||
SortDirection $direction = SortDirection::DESC
|
||||
): QueryBuilder {
|
||||
$orderDirection = strtoupper($direction->value);
|
||||
$comparisonOperator = $direction === SortDirection::ASC ? '>' : '<';
|
||||
|
||||
if ($keyset->date !== null) {
|
||||
$qb->addOrderBy($keyset->date, $orderDirection);
|
||||
}
|
||||
$qb->addOrderBy($keyset->id, $orderDirection);
|
||||
|
||||
$cursor = PaginationCursor::decode($page->cursor);
|
||||
if (! $cursor instanceof PaginationCursor) {
|
||||
return $this->applyOffsetPagination($qb, $page);
|
||||
return $qb->setMaxResults($page->limit + 1);
|
||||
}
|
||||
|
||||
if ($keyset->date === null) {
|
||||
$qb
|
||||
->andWhere(sprintf('%s <= :cursorLastId', $keyset->id))
|
||||
->setParameter('cursorLastId', $cursor->id->toRfc4122());
|
||||
->andWhere(sprintf('%s %s :cursorLastId', $keyset->id, $comparisonOperator))
|
||||
->setParameter('cursorLastId', $cursor->id->toString());
|
||||
} else {
|
||||
if (! $cursor->date instanceof \DateTimeImmutable) {
|
||||
return $qb->setMaxResults($page->limit + 1);
|
||||
}
|
||||
|
||||
$qb
|
||||
->andWhere(sprintf('(%s, %s) <= (:cursorLastDate, :cursorLastId)', $keyset->date, $keyset->id))
|
||||
->andWhere(sprintf('(%s, %s) %s (:cursorLastDate, :cursorLastId)', $keyset->date, $keyset->id, $comparisonOperator))
|
||||
->setParameter('cursorLastDate', $cursor->date->format('Y-m-d H:i:s'))
|
||||
->setParameter('cursorLastId', $cursor->id->toRfc4122());
|
||||
->setParameter('cursorLastId', $cursor->id->toString());
|
||||
}
|
||||
|
||||
return $qb->setMaxResults($page->limit + 1);
|
||||
|
||||
+1
-1
@@ -194,7 +194,7 @@ final readonly class ImportEngine
|
||||
if ($val !== null) {
|
||||
// Convert BINARY(16) UUIDs to canonical RFC4122
|
||||
if ($col === 'id' || str_ends_with((string) $col, '_id')) {
|
||||
$params[$i++] = Uuid::fromBinary($val)->toRfc4122();
|
||||
$params[$i++] = Uuid::fromBinary($val)->toString();
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user