Use canonical UUID strings for PostgreSQL queries

This commit is contained in:
Bernard Ngandu
2025-10-20 14:56:03 +02:00
parent c334452426
commit 5ac2fcb1fb
26 changed files with 109 additions and 66 deletions
@@ -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;
@@ -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);
@@ -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;
}