entityManager = $entityManager; } public function getOrphanedSendingTasksCount(): int { $builder = $this->entityManager->createQueryBuilder() ->select('count(st.id)'); return (int)$this->buildOrphanedSendingTasksQuery($builder)->getSingleScalarResult(); } public function getOrphanedScheduledTasksSubscribersCount(): int { $stTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName(); $stsTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName(); /** @var string $count */ $count = $this->entityManager->getConnection()->executeQuery(" SELECT COUNT(*) FROM $stsTable sts WHERE sts.task_id IN ( -- SELECT ... FROM (subquery) forces the subquery to materialize first. -- We're using this twice here to get the reduced data set before JOIN. SELECT oprhaned_task_ids.task_id FROM ( SELECT task_ids.task_id FROM ( SELECT DISTINCT task_id FROM $stsTable ) AS task_ids LEFT JOIN $stTable st ON st.id = task_ids.task_id WHERE st.id IS NULL ) AS oprhaned_task_ids ); ")->fetchOne(); return intval($count); } public function getSendingQueuesWithoutNewsletterCount(): int { $sqTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName(); $newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName(); /** @var string $count */ $count = $this->entityManager->getConnection()->executeQuery(" SELECT count(*) FROM $sqTable sq LEFT JOIN $newsletterTable n ON n.`id` = sq.`newsletter_id` WHERE n.`id` IS NULL ")->fetchOne(); return intval($count); } public function getOrphanedSubscriptionsCount(): int { $subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $segmentTable = $this->entityManager->getClassMetadata(SegmentEntity::class)->getTableName(); $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName(); /** @var string $count */ $count = $this->entityManager->getConnection()->executeQuery(" SELECT count(distinct ss.`id`) FROM $subscriberSegmentTable ss LEFT JOIN $segmentTable seg ON seg.`id` = ss.`segment_id` LEFT JOIN $subscriberTable sub ON sub.`id` = ss.`subscriber_id` WHERE seg.`id` IS NULL OR sub.`id` IS NULL ")->fetchOne(); return intval($count); } public function getOrphanedNewsletterLinksCount(): int { $newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName(); $sendingQueueTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName(); $newsletterLinkTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName(); /** @var string $count */ $count = $this->entityManager->getConnection()->executeQuery(" SELECT count(distinct nl.`id`) FROM $newsletterLinkTable nl LEFT JOIN $newsletterTable n ON n.`id` = nl.`newsletter_id` LEFT JOIN $sendingQueueTable sq ON sq.`id` = nl.`queue_id` WHERE n.`id` IS NULL OR sq.`id` IS NULL ")->fetchOne(); return intval($count); } public function getOrphanedNewsletterPostsCount(): int { $newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName(); $newsletterPostTable = $this->entityManager->getClassMetadata(NewsletterPostEntity::class)->getTableName(); /** @var string $count */ $count = $this->entityManager->getConnection()->executeQuery(" SELECT count(distinct np.`id`) FROM $newsletterPostTable np LEFT JOIN $newsletterTable n ON n.`id` = np.`newsletter_id` WHERE n.`id` IS NULL ")->fetchOne(); return intval($count); } public function cleanupOrphanedSendingTasks(): int { /** @var array $ids */ $ids = $this->buildOrphanedSendingTasksQuery( $this->entityManager->createQueryBuilder() ->select('st.id') )->getResult(); if (!$ids) { return 0; } $ids = array_column($ids, 'id'); // delete the orphaned tasks $qb = $this->entityManager->createQueryBuilder(); $countDeletedTasks = $qb->delete(ScheduledTaskEntity::class, 'st') ->where($qb->expr()->in('st.id', ':ids')) ->setParameter('ids', $ids) ->getQuery() ->execute(); // delete the scheduled tasks subscribers $stsTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName(); $this->entityManager->getConnection()->executeStatement( "DELETE sts_top FROM $stsTable sts_top JOIN ( SELECT sts.`task_id`, sts.`subscriber_id` FROM $stsTable sts WHERE `task_id` IN (:ids) LIMIT :limit ) as to_delete ON sts_top.`task_id` = to_delete.`task_id` AND sts_top.`subscriber_id` = to_delete.`subscriber_id`", ['limit' => self::DELETE_ROWS_LIMIT, 'ids' => $ids], ['limit' => ParameterType::INTEGER, 'ids' => ArrayParameterType::INTEGER] ); $qb = $this->entityManager->createQueryBuilder(); $qb->delete(ScheduledTaskSubscriberEntity::class, 'sts') ->where($qb->expr()->in('sts.task', ':ids')) ->setParameter('ids', $ids) ->getQuery() ->execute(); return $countDeletedTasks; } public function cleanupOrphanedScheduledTaskSubscribers(): int { $stTable = $this->entityManager->getClassMetadata(ScheduledTaskEntity::class)->getTableName(); $stsTable = $this->entityManager->getClassMetadata(ScheduledTaskSubscriberEntity::class)->getTableName(); $deletedCount = 0; $missingTaskIds = $this->entityManager->getConnection()->executeQuery(" SELECT task_ids.task_id -- SELECT ... FROM (subquery) forces the subquery to materialize first. -- We're using this here to get the reduced data set before JOIN. FROM ( SELECT DISTINCT task_id FROM $stsTable ) AS task_ids LEFT JOIN $stTable st ON st.id = task_ids.task_id WHERE st.id IS NULL;")->fetchFirstColumn(); if (!$missingTaskIds) { return 0; } do { $deletedCount += (int)$this->entityManager->getConnection()->executeStatement( "DELETE sts_top FROM $stsTable sts_top JOIN ( SELECT sts.`task_id`, sts.`subscriber_id` FROM $stsTable sts WHERE sts.`task_id` IN (:taskIds) LIMIT :limit ) as to_delete ON sts_top.`task_id` = to_delete.`task_id` AND sts_top.`subscriber_id` = to_delete.`subscriber_id`", ['taskIds' => $missingTaskIds, 'limit' => self::DELETE_ROWS_LIMIT], ['taskIds' => ArrayParameterType::INTEGER, 'limit' => ParameterType::INTEGER] ); } while ($this->getOrphanedScheduledTasksSubscribersCount() > 0); return $deletedCount; } public function cleanupSendingQueuesWithoutNewsletter(): int { $sqTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName(); $newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName(); $deletedQueuesCount = (int)$this->entityManager->getConnection()->executeStatement(" DELETE sq FROM $sqTable sq LEFT JOIN $newsletterTable n ON n.`id` = sq.`newsletter_id` WHERE n.`id` IS NULL "); $this->cleanupOrphanedSendingTasks(); return $deletedQueuesCount; } public function cleanupOrphanedSubscriptions(): int { $subscriberTable = $this->entityManager->getClassMetadata(SubscriberEntity::class)->getTableName(); $segmentTable = $this->entityManager->getClassMetadata(SegmentEntity::class)->getTableName(); $subscriberSegmentTable = $this->entityManager->getClassMetadata(SubscriberSegmentEntity::class)->getTableName(); return (int)$this->entityManager->getConnection()->executeStatement(" DELETE ss FROM $subscriberSegmentTable ss LEFT JOIN $segmentTable seg ON seg.`id` = ss.`segment_id` LEFT JOIN $subscriberTable sub ON sub.`id` = ss.`subscriber_id` WHERE seg.`id` IS NULL OR sub.`id` IS NULL "); } public function cleanupOrphanedNewsletterLinks(): int { $newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName(); $sendingQueueTable = $this->entityManager->getClassMetadata(SendingQueueEntity::class)->getTableName(); $newsletterLinkTable = $this->entityManager->getClassMetadata(NewsletterLinkEntity::class)->getTableName(); return (int)$this->entityManager->getConnection()->executeStatement(" DELETE nl FROM $newsletterLinkTable nl LEFT JOIN $newsletterTable n ON n.`id` = nl.`newsletter_id` LEFT JOIN $sendingQueueTable sq ON sq.`id` = nl.`queue_id` WHERE n.`id` IS NULL OR sq.`id` IS NULL "); } public function cleanupOrphanedNewsletterPosts(): int { $newsletterTable = $this->entityManager->getClassMetadata(NewsletterEntity::class)->getTableName(); $newsletterPostTable = $this->entityManager->getClassMetadata(NewsletterPostEntity::class)->getTableName(); return (int)$this->entityManager->getConnection()->executeStatement(" DELETE np FROM $newsletterPostTable np LEFT JOIN $newsletterTable n ON n.`id` = np.`newsletter_id` WHERE n.`id` IS NULL "); } private function buildOrphanedSendingTasksQuery(QueryBuilder $queryBuilder): Query { return $queryBuilder ->from(ScheduledTaskEntity::class, 'st') ->leftJoin('st.sendingQueue', 'sq') ->where('sq.id IS NULL') ->andWhere('st.type = :type') ->setParameter('type', SendingQueue::TASK_TYPE) ->getQuery(); } }