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
@@ -30,7 +30,7 @@ final readonly class ArticleForExport
public static function create(array $item): self public static function create(array $item): self
{ {
return new self( return new self(
ArticleId::fromBinary($item['article_id']), ArticleId::fromString(DataMapping::string($item, 'article_id')),
DataMapping::string($item, 'article_title'), DataMapping::string($item, 'article_title'),
DataMapping::string($item, 'article_link'), DataMapping::string($item, 'article_link'),
DataMapping::string($item, 'article_categories'), DataMapping::string($item, 'article_categories'),
@@ -26,10 +26,10 @@ final readonly class SourceStatistics
public static function create(array $item): self public static function create(array $item): self
{ {
return new self( return new self(
SourceId::fromBinary($item['source_id']), SourceId::fromString(DataMapping::string($item, 'source_id')),
DataMapping::string($item, 'source_name'), DataMapping::string($item, 'source_name'),
DataMapping::integer($item, 'articles_count'), DataMapping::integer($item, 'articles_count'),
DataMapping::integer($item, 'article_metadata_available'), DataMapping::integer($item, 'articles_metadata_available'),
DataMapping::nullableDatetime($item, 'source_crawled_at') DataMapping::nullableDatetime($item, 'source_crawled_at')
); );
} }
@@ -41,7 +41,8 @@ final readonly class GetArticlesForExportDbalHandler implements GetArticlesForEx
) )
->from('article', 'a') ->from('article', 'a')
->innerJoin('a', 'source', 's', 'a.source_id = s.id') ->innerJoin('a', 'source', 's', 'a.source_id = s.id')
->orderBy('a.published_at', 'DESC'); ->orderBy('a.published_at', 'DESC')
->addOrderBy('a.id', 'DESC');
if ($query->source !== null) { if ($query->source !== null) {
$qb->andWhere('s.name = :source') $qb->andWhere('s.name = :source')
@@ -44,7 +44,7 @@ final readonly class ArticleDetails
public static function create(array $item): self public static function create(array $item): self
{ {
return new self( return new self(
ArticleId::fromBinary($item['article_id']), ArticleId::fromString(DataMapping::string($item, 'article_id')),
DataMapping::string($item, 'article_title'), DataMapping::string($item, 'article_title'),
Link::from(DataMapping::string($item, 'article_link')), Link::from(DataMapping::string($item, 'article_link')),
explode(',', DataMapping::string($item, 'article_categories')), explode(',', DataMapping::string($item, 'article_categories')),
@@ -33,7 +33,7 @@ final readonly class ArticleOverview
public static function create(array $item): self public static function create(array $item): self
{ {
return new self( return new self(
ArticleId::fromBinary($item['article_id']), ArticleId::fromString(DataMapping::string($item, 'article_id')),
DataMapping::string($item, 'article_title'), DataMapping::string($item, 'article_title'),
Link::from(DataMapping::string($item, 'article_link')), Link::from(DataMapping::string($item, 'article_link')),
explode(',', DataMapping::string($item, 'article_categories')), explode(',', DataMapping::string($item, 'article_categories')),
@@ -28,7 +28,7 @@ final readonly class Bookmark
public static function create(array $item): self public static function create(array $item): self
{ {
return new self( return new self(
BookmarkId::fromBinary($item['bookmark_id']), BookmarkId::fromString(DataMapping::string($item, 'bookmark_id')),
DataMapping::string($item, 'bookmark_name'), DataMapping::string($item, 'bookmark_name'),
DataMapping::datetime($item, 'bookmark_created_at'), DataMapping::datetime($item, 'bookmark_created_at'),
DataMapping::nullableString($item, 'bookmark_description'), DataMapping::nullableString($item, 'bookmark_description'),
@@ -27,7 +27,7 @@ final readonly class Comment
public static function create(array $item): self public static function create(array $item): self
{ {
return new self( return new self(
CommentId::fromBinary($item['comment_id']), CommentId::fromString(DataMapping::string($item, 'comment_id')),
UserReference::create($item), UserReference::create($item),
DataMapping::enum($item, 'comment_sentiment', Sentiment::class), DataMapping::enum($item, 'comment_sentiment', Sentiment::class),
DataMapping::string($item, 'comment_content'), DataMapping::string($item, 'comment_content'),
@@ -39,7 +39,7 @@ final readonly class SourceDetails
public static function create(array $item, PublicationGraph $publicationGraph, CategoryShares $categoryShares): self public static function create(array $item, PublicationGraph $publicationGraph, CategoryShares $categoryShares): self
{ {
return new self( return new self(
SourceId::fromBinary($item['source_id']), SourceId::fromString(DataMapping::string($item, 'source_id')),
DataMapping::string($item, 'source_name'), DataMapping::string($item, 'source_name'),
DataMapping::string($item, 'source_url'), DataMapping::string($item, 'source_url'),
new Credibility( new Credibility(
@@ -27,7 +27,7 @@ final readonly class SourceOverview
public static function create(array $item): self public static function create(array $item): self
{ {
return new self( return new self(
SourceId::fromBinary($item['source_id']), SourceId::fromString(DataMapping::string($item, 'source_id')),
DataMapping::string($item, 'source_name'), DataMapping::string($item, 'source_name'),
DataMapping::string($item, 'source_url'), DataMapping::string($item, 'source_url'),
DataMapping::nullableString($item, 'source_display_name'), DataMapping::nullableString($item, 'source_display_name'),
@@ -26,7 +26,7 @@ final readonly class SourceReference
public static function create(array $item): self public static function create(array $item): self
{ {
return new self( return new self(
SourceId::fromBinary($item['source_id']), SourceId::fromString(DataMapping::string($item, 'source_id')),
DataMapping::string($item, 'source_name'), DataMapping::string($item, 'source_name'),
DataMapping::nullableString($item, 'source_display_name'), DataMapping::nullableString($item, 'source_display_name'),
DataMapping::nullableString($item, 'source_image'), DataMapping::nullableString($item, 'source_image'),
@@ -23,7 +23,7 @@ final readonly class UserReference
public static function create(array $item): self public static function create(array $item): self
{ {
return new self( return new self(
UserId::fromBinary($item['user_id']), UserId::fromString(DataMapping::string($item, 'user_id')),
DataMapping::string($item, 'user_name') DataMapping::string($item, 'user_name')
); );
} }
@@ -39,8 +39,7 @@ final readonly class GetArticleCommentListDbalHandler implements GetArticleComme
->from('comment', 'c') ->from('comment', 'c')
->innerJoin('c', 'user', 'u', 'c.user_id = u.id') ->innerJoin('c', 'user', 'u', 'c.user_id = u.id')
->where('c.article_id = :articleId') ->where('c.article_id = :articleId')
->orderBy('c.created_at', 'DESC') ->setParameter('articleId', $query->articleId->toString());
->setParameter('articleId', $query->articleId->toRfc4122());
$qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('c.id', 'c.created_at')); $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('c.id', 'c.created_at'));
@@ -41,8 +41,8 @@ final readonly class GetArticleDetailsDbalHandler implements GetArticleDetailsHa
$qb->innerJoin('a', 'source', 's', 'a.source_id = s.id') $qb->innerJoin('a', 'source', 's', 'a.source_id = s.id')
->from('article', 'a') ->from('article', 'a')
->where('a.id = :articleId') ->where('a.id = :articleId')
->setParameter('articleId', $query->id->toRfc4122()) ->setParameter('articleId', $query->id->toString())
->setParameter('userId', $query->userId->toRfc4122()) ->setParameter('userId', $query->userId->toString())
; ;
try { try {
@@ -43,11 +43,16 @@ final readonly class GetArticleOverviewListDbalHandler implements GetArticleOver
$qb->from('article', 'a') $qb->from('article', 'a')
->innerJoin('a', 'source', 's', 'a.source_id = s.id') ->innerJoin('a', 'source', 's', 'a.source_id = s.id')
//->orderBy('a.published_at', $query->filters->sortDirection->value) //->orderBy('a.published_at', $query->filters->sortDirection->value)
->setParameter('userId', $query->userId->toRfc4122()) ->setParameter('userId', $query->userId->toString())
; ;
$qb = $this->applyArticleFilters($qb, $query->filters); $qb = $this->applyArticleFilters($qb, $query->filters);
$qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('a.id', 'a.published_at')); $qb = $this->applyCursorPagination(
$qb,
$query->page,
new PaginatorKeyset('a.id', 'a.published_at'),
$query->filters->sortDirection
);
try { try {
$data = $qb->executeQuery()->fetchAllAssociative(); $data = $qb->executeQuery()->fetchAllAssociative();
@@ -37,8 +37,7 @@ final readonly class GetBookmarkListDbalHandler implements GetBookmarkListHandle
->leftJoin('b', 'bookmark_article', 'ba', 'ba.bookmark_id = b.id') ->leftJoin('b', 'bookmark_article', 'ba', 'ba.bookmark_id = b.id')
->where('b.user_id = :userId') ->where('b.user_id = :userId')
->groupBy('b.id') ->groupBy('b.id')
->orderBy('b.id', 'DESC') ->setParameter('userId', $query->userId->toString())
->setParameter('userId', $query->userId->toRfc4122())
; ;
$qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('b.id')); $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('b.id'));
@@ -43,12 +43,17 @@ final readonly class GetBookmarkedArticleListDbalHandler implements GetBookmarke
->innerJoin('ba', 'bookmark', 'b', 'b.id = ba.bookmark_id AND b.user_id = :userId') ->innerJoin('ba', 'bookmark', 'b', 'b.id = ba.bookmark_id AND b.user_id = :userId')
->innerJoin('a', 'source', 's', 'a.source_id = s.id') ->innerJoin('a', 'source', 's', 'a.source_id = s.id')
->where('b.id = :bookmarkId') ->where('b.id = :bookmarkId')
->setParameter('bookmarkId', $query->bookmarkId->toRfc4122()) ->setParameter('bookmarkId', $query->bookmarkId->toString())
->setParameter('userId', $query->userId->toRfc4122()) ->setParameter('userId', $query->userId->toString())
; ;
$qb = $this->applyArticleFilters($qb, $query->filters); $qb = $this->applyArticleFilters($qb, $query->filters);
$qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('a.id', 'a.published_at')); $qb = $this->applyCursorPagination(
$qb,
$query->page,
new PaginatorKeyset('a.id', 'a.published_at'),
$query->filters->sortDirection
);
try { try {
$data = $qb->executeQuery()->fetchAllAssociative(); $data = $qb->executeQuery()->fetchAllAssociative();
@@ -43,13 +43,17 @@ final readonly class GetSourceArticleOverviewListDbalHandler implements GetSourc
$qb->from('article', 'a') $qb->from('article', 'a')
->innerJoin('a', 'source', 's', 'a.source_id = s.id') ->innerJoin('a', 'source', 's', 'a.source_id = s.id')
->where('s.id = :sourceId') ->where('s.id = :sourceId')
->orderBy('a.published_at', $query->filters->sortDirection->value) ->setParameter('userId', $query->userId->toString())
->setParameter('userId', $query->userId->toRfc4122()) ->setParameter('sourceId', $query->sourceId->toString())
->setParameter('sourceId', $query->sourceId->toRfc4122())
; ;
$qb = $this->applyArticleFilters($qb, $query->filters); $qb = $this->applyArticleFilters($qb, $query->filters);
$qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('a.id', 'a.published_at')); $qb = $this->applyCursorPagination(
$qb,
$query->page,
new PaginatorKeyset('a.id', 'a.published_at'),
$query->filters->sortDirection
);
try { try {
$data = $qb->executeQuery()->fetchAllAssociative(); $data = $qb->executeQuery()->fetchAllAssociative();
@@ -49,8 +49,8 @@ final readonly class GetSourceDetailsDbalHandler implements GetSourceDetailsHand
$qb->from('source', 's') $qb->from('source', 's')
->leftJoin('s', 'article', 'a', 'a.source_id = s.id') ->leftJoin('s', 'article', 'a', 'a.source_id = s.id')
->where('s.id = :sourceId') ->where('s.id = :sourceId')
->setParameter('sourceId', $query->sourceId->toRfc4122()) ->setParameter('sourceId', $query->sourceId->toString())
->setParameter('userId', $query->userId->toRfc4122()); ->setParameter('userId', $query->userId->toString());
// Aggregate columns are selected; include non-aggregated columns in GROUP BY for PostgreSQL // Aggregate columns are selected; include non-aggregated columns in GROUP BY for PostgreSQL
$qb->groupBy('s.id, s.name, s.description, s.url, s.updated_at, s.display_name, s.bias, s.reliability, s.transparency'); $qb->groupBy('s.id, s.name, s.description, s.url, s.updated_at, s.display_name, s.bias, s.reliability, s.transparency');
@@ -84,7 +84,7 @@ final readonly class GetSourceDetailsDbalHandler implements GetSourceDetailsHand
->andWhere('a.published_at BETWEEN to_timestamp(:start) AND to_timestamp(:end)') ->andWhere('a.published_at BETWEEN to_timestamp(:start) AND to_timestamp(:end)')
->groupBy('day') ->groupBy('day')
->orderBy('day', 'ASC') ->orderBy('day', 'ASC')
->setParameter('sourceId', $query->sourceId->toRfc4122()) ->setParameter('sourceId', $query->sourceId->toString())
->setParameter('start', $dateRange->start, ParameterType::INTEGER) ->setParameter('start', $dateRange->start, ParameterType::INTEGER)
->setParameter('end', $dateRange->end, ParameterType::INTEGER) ->setParameter('end', $dateRange->end, ParameterType::INTEGER)
->enableResultCache(new QueryCacheProfile(SourceCacheAttributes::CACHE_TTL, $cacheKey)); ->enableResultCache(new QueryCacheProfile(SourceCacheAttributes::CACHE_TTL, $cacheKey));
@@ -126,7 +126,7 @@ final readonly class GetSourceDetailsDbalHandler implements GetSourceDetailsHand
->from('article', 'a') ->from('article', 'a')
->innerJoin('a', 'source', 's', 'a.source_id = s.id') ->innerJoin('a', 'source', 's', 'a.source_id = s.id')
->where('s.id = :sourceId') ->where('s.id = :sourceId')
->setParameter('sourceId', $query->sourceId->toRfc4122()) ->setParameter('sourceId', $query->sourceId->toString())
->enableResultCache(new QueryCacheProfile(SourceCacheAttributes::CACHE_TTL, $cacheKey)); ->enableResultCache(new QueryCacheProfile(SourceCacheAttributes::CACHE_TTL, $cacheKey));
try { try {
@@ -36,7 +36,7 @@ final readonly class GetSourceOverviewListDbalHandler implements GetSourceOvervi
$qb = $this->addFollowedSourceExistsQuery($qb); $qb = $this->addFollowedSourceExistsQuery($qb);
$qb->from('source', 's') $qb->from('source', 's')
->setParameter('userId', $query->userId->toRfc4122()) ->setParameter('userId', $query->userId->toString())
; ;
$qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('s.id', 's.created_at')); $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('s.id', 's.created_at'));
@@ -47,7 +47,7 @@ final readonly class GetSourceOverviewListDbalHandler implements GetSourceOvervi
throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e);
} }
$pagination = $this->createPaginationInfo($data, $query->page, new PaginatorKeyset('source_id')); $pagination = $this->createPaginationInfo($data, $query->page, new PaginatorKeyset('source_id', 'source_created_at'));
return SourceOverviewList::create($data, $pagination); return SourceOverviewList::create($data, $pagination);
} }
} }
@@ -22,6 +22,7 @@ trait SourceQuery
"CONCAT('https://devscast.org/images/sources/', s.name, '.png') as source_image", "CONCAT('https://devscast.org/images/sources/', s.name, '.png') as source_image",
's.url as source_url', 's.url as source_url',
's.name as source_name', 's.name as source_name',
's.created_at as source_created_at',
); );
} }
@@ -42,8 +42,8 @@ final class FollowedSourceOrmRepository extends ServiceEntityRepository implemen
return $this->createQueryBuilder('fs') return $this->createQueryBuilder('fs')
->andWhere('IDENTITY(fs.follower) = :userId') ->andWhere('IDENTITY(fs.follower) = :userId')
->andWhere('IDENTITY(fs.source) = :sourceId') ->andWhere('IDENTITY(fs.source) = :sourceId')
->setParameter('sourceId', $sourceId->toRfc4122()) ->setParameter('sourceId', $sourceId->toString())
->setParameter('userId', $userId->toRfc4122()) ->setParameter('userId', $userId->toString())
->getQuery() ->getQuery()
->getOneOrNullResult(); ->getOneOrNullResult();
} }
@@ -27,7 +27,7 @@ final readonly class UserProfile
public static function create(array $item): self public static function create(array $item): self
{ {
return new self( return new self(
UserId::fromBinary($item['user_id']), UserId::fromString(DataMapping::string($item, 'user_id')),
DataMapping::string($item, 'user_name'), DataMapping::string($item, 'user_name'),
EmailAddress::from(DataMapping::string($item, 'user_email')), EmailAddress::from(DataMapping::string($item, 'user_email')),
DataMapping::dateTime($item, 'user_created_at'), DataMapping::dateTime($item, 'user_created_at'),
@@ -34,7 +34,7 @@ final readonly class GetUserProfileDbalHandler implements GetUserProfileHandler
) )
->from('user', 'u') ->from('user', 'u')
->where('u.id = :userId') ->where('u.id = :userId')
->setParameter('userId', $query->userId->toRfc4122()); ->setParameter('userId', $query->userId->toString());
/** @var array<string, mixed>|false $data */ /** @var array<string, mixed>|false $data */
$data = $qb->executeQuery()->fetchAssociative(); $data = $qb->executeQuery()->fetchAssociative();
@@ -16,7 +16,7 @@ final readonly class PaginationCursor
{ {
public function __construct( public function __construct(
public UuidV7 $id, 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 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) { if ($keyset->date !== null) {
$date = DataMapping::dateTime($item, $keyset->date)->format('Y-m-d H:i:s'); $payload['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)
);
} }
return base64_encode( return base64_encode(json_encode($payload, JSON_THROW_ON_ERROR));
json_encode([
'id' => $id,
], JSON_THROW_ON_ERROR)
);
} }
@@ -58,15 +49,25 @@ final readonly class PaginationCursor
} }
try { 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'); throw new \InvalidArgumentException('Invalid cursor format');
} }
$date = null;
if (isset($data['date'])) {
$date = new \DateTimeImmutable($data['date']);
}
return new self( return new self(
id: UuidV7::fromString($data['id']), id: UuidV7::fromString($data['id']),
date: new \DateTimeImmutable($data['date']) date: $date,
); );
} catch (\Throwable) { } catch (\Throwable) {
return null; 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\PaginationCursor;
use Basango\SharedKernel\Domain\Model\Pagination\PaginationInfo; use Basango\SharedKernel\Domain\Model\Pagination\PaginationInfo;
use Basango\SharedKernel\Domain\Model\Pagination\PaginatorKeyset; use Basango\SharedKernel\Domain\Model\Pagination\PaginatorKeyset;
use Basango\SharedKernel\Domain\Model\ValueObject\SortDirection;
use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Query\QueryBuilder;
/** /**
@@ -17,35 +18,62 @@ use Doctrine\DBAL\Query\QueryBuilder;
*/ */
trait PaginationQuery 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); $paginationInfo = PaginationInfo::from($page);
if ($data === []) { if ($data === []) {
return $paginationInfo; return $paginationInfo;
} }
$paginationInfo->cursor = PaginationCursor::encode(array_pop($data), $keyset); $hasNext = count($data) > $page->limit;
$paginationInfo->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; 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); $cursor = PaginationCursor::decode($page->cursor);
if (! $cursor instanceof PaginationCursor) { if (! $cursor instanceof PaginationCursor) {
return $this->applyOffsetPagination($qb, $page); return $qb->setMaxResults($page->limit + 1);
} }
if ($keyset->date === null) { if ($keyset->date === null) {
$qb $qb
->andWhere(sprintf('%s <= :cursorLastId', $keyset->id)) ->andWhere(sprintf('%s %s :cursorLastId', $keyset->id, $comparisonOperator))
->setParameter('cursorLastId', $cursor->id->toRfc4122()); ->setParameter('cursorLastId', $cursor->id->toString());
} else { } else {
if (! $cursor->date instanceof \DateTimeImmutable) {
return $qb->setMaxResults($page->limit + 1);
}
$qb $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('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); return $qb->setMaxResults($page->limit + 1);
@@ -194,7 +194,7 @@ final readonly class ImportEngine
if ($val !== null) { if ($val !== null) {
// Convert BINARY(16) UUIDs to canonical RFC4122 // Convert BINARY(16) UUIDs to canonical RFC4122
if ($col === 'id' || str_ends_with((string) $col, '_id')) { if ($col === 'id' || str_ends_with((string) $col, '_id')) {
$params[$i++] = Uuid::fromBinary($val)->toRfc4122(); $params[$i++] = Uuid::fromBinary($val)->toString();
continue; continue;
} }