Merge pull request #2 from bernard-ng/codex/optimize-backend-queries-for-postgres
Use canonical UUID strings for PostgreSQL queries
This commit is contained in:
@@ -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')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -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')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -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'));
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -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 {
|
||||||
|
|||||||
+7
-2
@@ -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();
|
||||||
|
|||||||
+1
-2
@@ -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'));
|
||||||
|
|||||||
+8
-3
@@ -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();
|
||||||
|
|||||||
+8
-4
@@ -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();
|
||||||
|
|||||||
+4
-4
@@ -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 {
|
||||||
|
|||||||
+2
-2
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -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',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -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'),
|
||||||
|
|||||||
+1
-1
@@ -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;
|
||||||
|
|||||||
+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\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);
|
||||||
|
|||||||
+22
-2
@@ -192,9 +192,8 @@ final readonly class ImportEngine
|
|||||||
$val = $row[$col] ?? null;
|
$val = $row[$col] ?? null;
|
||||||
|
|
||||||
if ($val !== null) {
|
if ($val !== null) {
|
||||||
// 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++] = $this->normalizeUuidValue($val);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,4 +297,25 @@ final readonly class ImportEngine
|
|||||||
|
|
||||||
return $converted !== false ? $converted : $value;
|
return $converted !== false ? $converted : $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizeUuidValue(mixed $value): string
|
||||||
|
{
|
||||||
|
if ($value instanceof Uuid) {
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value)) {
|
||||||
|
if (strlen($value) === 16) {
|
||||||
|
$uuid = Uuid::fromBinary($value);
|
||||||
|
|
||||||
|
return method_exists($uuid, 'toString')
|
||||||
|
? $uuid->toString()
|
||||||
|
: $uuid->toRfc4122();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user