From ecb975152c750b62b042b9486db1edf23f770f31 Mon Sep 17 00:00:00 2001 From: Jonas Raoni Soares da Silva Date: Tue, 23 May 2023 17:57:42 +0300 Subject: [PATCH] pkp/pkp-lib#8929 Added automated code while keeping the customizations --- .../v3_4_0/PreflightCheckMigration.php | 1233 ++++++++++------- 1 file changed, 701 insertions(+), 532 deletions(-) diff --git a/classes/migration/upgrade/v3_4_0/PreflightCheckMigration.php b/classes/migration/upgrade/v3_4_0/PreflightCheckMigration.php index d8916fbcd45..f5ff3c783f9 100755 --- a/classes/migration/upgrade/v3_4_0/PreflightCheckMigration.php +++ b/classes/migration/upgrade/v3_4_0/PreflightCheckMigration.php @@ -20,6 +20,7 @@ use Exception; use Illuminate\Database\MySqlConnection; use Illuminate\Database\PostgresConnection; +use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; @@ -46,6 +47,9 @@ public function up(): void $this->checkSubmissionChecklist(); $this->checkContactSetting(); $this->clearOrphanedEntities(); + // Extra checks done after the entities are clean to avoid flagging problems over data that's supposed to be gone + $this->checkSubmissionLocale(); + $this->checkAuthorsMissingUserGroup(); } catch (Throwable $e) { if ($fallbackVersion = $this->setFallbackVersion()) { $this->_installer->log("A pre-flight check failed. The software was successfully upgraded to {$fallbackVersion} but could not be upgraded further (to " . $this->_installer->newVersion->getVersionString() . '). Check and correct the error, then try again.'); @@ -358,623 +362,788 @@ protected function checkForeignKeySupport(): void } } + /** + * Checks if the submission.locale field is filled, due to a bug, a fix will be attempted (retrieve the submission locale from its related publication entity) + * @see https://github.com/pkp/pkp-lib/issues/7190 + */ + protected function checkSubmissionLocale(): void + { + // Fix missing submission.locale by retrieving the value from the publication + $rows = DB::table('submissions AS s')->join('publications p', 'p.publication_id', '=', 's.current_publication_id') + ->whereNull('s.locale') + ->whereNotNull('p.locale') + ->get(['s.submission_id', 'p.locale']); + foreach ($rows as $row) { + $this->_installer->log("Updating locale of submission {$row->submission_id} to {$row->locale}."); + DB::table('submissions')->where('submission_id', '=', $row->submission_id)->update(['locale' => $row->locale]); + } + + if ($count = DB::table('submissions AS s')->whereNull('locale')->count()) { + throw new Exception("There are {$count} submission records with null in the locale column. Please correct these before upgrading."); + } + } + + /** + * Checks if there are authors missing an user_group relationship + */ + protected function checkAuthorsMissingUserGroup(): void + { + // Flag orphaned authors entries by user_group_id + $result = DB::table('authors AS a')->leftJoin('user_groups AS ug', 'ug.user_group_id', '=', 'a.user_group_id')->leftJoin('publications AS p', 'p.publication_id', '=', 'a.publication_id')->whereNull('ug.user_group_id')->select('a.author_id AS author_id', 'a.publication_id AS publication_id', 'a.user_group_id AS user_group_id', 'p.submission_id AS submission_id')->get(); + foreach ($result as $row) { + $this->_installer->log("Found an orphaned author entry with author_id {$row->author_id} for publication_id {$row->publication_id} with submission_id {$row->submission_id} and user_group_id {$row->user_group_id}."); + } + if ($result->count()) { + throw new Exception('There are author records without matching user_group entries. Please correct these before upgrading.'); + } + } + /** * Clears orphaned entities before introducing foreign keys + * - The cleanup is sorted based on the number of dependents (entities with a higher number of dependents are deleted first) + * - Some recovery might be attempted before cleaning/dropping data + * - Rows with required, but invalid foreign keys (null/bad values) will be deleted + * - Rows with nullable/optional foreign keys will be inspected on a case-by-case basis (if possible they will be nulled, otherwise removed) + * - Some consideration is given to bidirectional/direct dependencies and exceptional cases (e.g. submission.current_publication_id, which is nullable, but required) * @see https://github.com/pkp/pkp-lib/issues/6093 * * @throws Exception */ protected function clearOrphanedEntities(): void { - // Clean orphaned context data - $orphanedIds = DB::table($this->getContextSettingsTable() . ' AS cs')->leftJoin($this->getContextTable() . ' AS c', 'cs.' . $this->getContextKeyField(), '=', 'c.' . $this->getContextKeyField())->whereNull('c.' . $this->getContextKeyField())->distinct()->pluck('cs.' . $this->getContextKeyField()); - foreach ($orphanedIds as $contextId) { - DB::table($this->getContextSettingsTable())->where($this->getContextKeyField(), '=', $contextId)->delete(); - } - - // Attempts to recover the submission.current_publication_id before discarding it - $rows = DB::table('submissions AS s') - ->leftJoin('publications AS p', 'p.publication_id', '=', 's.current_publication_id') - ->join( - 'publications AS last', - fn (JoinClause $q) => $q->where( - fn (Builder $q) => $q->from('publications AS p2') - ->whereColumn('p2.submission_id', '=', 's.submission_id') - ->orderByDesc('p2.publication_id') - ->limit(1) - ->select('p2.publication_id'), - '=', - DB::raw('last.publication_id') + submissions: { + // Depends directly on ~2 entities: context_id->journals.journal_id current_publication_id->publications.publication_id + // Dependent entities: ~21 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('submissions', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + + // Attempts to recover the field submissions.current_publication_id before discarding the entry + $rows = DB::table('submissions AS s') + ->leftJoin('publications AS p', 'p.publication_id', '=', 's.current_publication_id') + ->join( + 'publications AS last', + fn (JoinClause $q) => $q->where( + fn (Builder $q) => $q->from('publications AS p2') + ->whereColumn('p2.submission_id', '=', 's.submission_id') + ->orderByDesc('p2.publication_id') + ->limit(1) + ->select('p2.publication_id'), + '=', + DB::raw('last.publication_id') + ) ) - ) - ->whereNull('p.publication_id') - ->pluck('s.submission_id', 'last.publication_id'); - foreach ($rows as $publicationId => $submissionId) { - DB::table('submissions')->where('submission_id', '=', $submissionId)->update(['current_publication_id' => $publicationId]); - } + ->whereNull('p.publication_id') + ->pluck('s.submission_id', 'last.publication_id'); + foreach ($rows as $publicationId => $submissionId) { + $this->_installer->log("The current publication ID ({$publicationId}) for the submission ID {$submissionId} is invalid, the publication ID {$publicationId} will replace it"); + DB::table('submissions')->where('submission_id', '=', $submissionId)->update(['current_publication_id' => $publicationId]); + } - // Clean orphaned submission entries by current_publication_id - DB::table('submissions s')->leftJoin('publications p', 'p.publication_id', '=', 's.current_publication_id')->whereNull('s.current_publication_id')->orWhereNull('p.publication_id')->delete(); + // The current_publication_id is nullable, but it's in fact required by the software, so we delete orphan entries instead of nulling them + $this->deleteRequiredReference('submissions', 'current_publication_id', 'publications', 'publication_id'); + } - // Clean orphan access keys by user ID - $orphanedIds = DB::table('access_keys AS n')->leftJoin('users AS u', 'n.user_id', '=', 'u.user_id')->whereNull('u.user_id')->distinct()->pluck('n.user_id'); - foreach ($orphanedIds as $userId) { - $this->_installer->log("Removing orphaned access keys for user ID {$userId}"); - DB::table('access_keys')->where('user_id', '=', $userId)->delete(); + submission_files: { + // Depends directly on ~5 entities: file_id->files.file_id genre_id->genres.genre_id source_submission_file_id->submission_files.submission_file_id submission_id->submissions.submission_id uploader_user_id->users.user_id + // Dependent entities: ~10 + $this->deleteRequiredReference('submission_files', 'submission_id', 'submissions', 'submission_id'); + $this->deleteRequiredReference('submission_files', 'file_id', 'files', 'file_id'); + $this->cleanOptionalReference('submission_files', 'uploader_user_id', 'users', 'user_id'); + $this->cleanOptionalReference('submission_files', 'source_submission_file_id', 'submission_files', 'submission_file_id'); + $this->cleanOptionalReference('submission_files', 'genre_id', 'genres', 'genre_id'); } - // pkp/pkp-lib#6903 Prepare to add foreign key relationships - DB::table('notifications')->where('user_id', '=', 0)->update(['user_id' => null]); - // Clean orphan notifications by user ID - $orphanedIds = DB::table('notifications AS n')->leftJoin('users AS u', 'n.user_id', '=', 'u.user_id')->whereNull('u.user_id')->whereNotNull('n.user_id')->select(['n.user_id AS user_id'])->distinct()->get(); - foreach ($orphanedIds as $row) { - DB::table('notifications')->where('user_id', '=', $row->user_id)->delete(); + publications: { + // Depends directly on ~4 entities: primary_contact_id->authors.author_id doi_id->dois.doi_id(not found in previous version) section_id->sections.section_id submission_id->submissions.submission_id + // Dependent entities: ~6 + $this->deleteRequiredReference('publications', 'submission_id', 'submissions', 'submission_id'); + $this->cleanOptionalReference('publications', 'primary_contact_id', 'authors', 'author_id'); + // Remaining 2 dependencies moved to the other migration } - // Clean orphan notifications by context ID - $orphanedIds = DB::table('notifications AS n')->leftJoin($this->getContextTable() . ' AS c', 'n.context_id', '=', 'c.' . $this->getContextKeyField())->whereNotNull('n.context_id')->whereNull('c.' . $this->getContextKeyField())->distinct()->pluck('n.context_id'); - foreach ($orphanedIds as $contextId) { - DB::table('notifications')->where('context_id', '=', $contextId)->delete(); + + user_groups: { + // Depends directly on ~1 entities: context_id->journals.journal_id + // Dependent entities: ~6 + // Custom field (not found in at least one of the softwares) + $this->cleanOptionalReference('user_groups', 'context_id', $this->getContextTable(), $this->getContextKeyField()); } - // Clean orphaned assoc_type/assoc_id data in announcement_types - $orphanedIds = DB::table('announcement_types AS at')->leftJoin($this->getContextTable() . ' AS c', 'at.assoc_id', '=', 'c.' . $this->getContextKeyField())->whereNull('c.' . $this->getContextKeyField())->orWhere('at.assoc_type', '<>', Application::get()->getContextAssocType())->distinct()->pluck('at.type_id'); - foreach ($orphanedIds as $typeId) { - $this->_installer->log("Removing orphaned announcement type ID {$typeId} with no matching context ID."); - DB::table('announcement_types')->where('type_id', '=', $typeId)->delete(); + publication_galleys: { + // Depends directly on ~3 entities: doi_id->dois.doi_id(not found in previous version) publication_id->publications.publication_id submission_file_id->submission_files.submission_file_id + // Dependent entities: ~5 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('publication_galleys', 'publication_id', 'publications', 'publication_id'); + // Custom field (not found in at least one of the softwares) + $this->deleteOptionalReference('publication_galleys', 'submission_file_id', 'submission_files', 'submission_file_id'); + // Deprecated/moved field (not found on previous software version) + // $this->deleteOptionalReference('publication_galleys', 'doi_id', 'dois', 'doi_id'); } - // Clean orphaned announcement_type_setting entries - $orphanedIds = DB::table('announcement_type_settings AS ats')->leftJoin('announcement_types AS at', 'ats.type_id', '=', 'at.type_id')->whereNull('at.type_id')->distinct()->pluck('ats.type_id'); - foreach ($orphanedIds as $typeId) { - $this->_installer->log("Removing orphaned settings for missing announcement type ID {$typeId}"); - DB::table('announcement_type_settings')->where('type_id', '=', $typeId)->delete(); + categories: { + // Depends directly on ~2 entities: context_id->journals.journal_id parent_id->categories.category_id + // Dependent entities: ~3 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('categories', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + $this->deleteOptionalReference('categories', 'parent_id', 'categories', 'category_id'); } - if ($count = DB::table('announcements AS a')->leftJoin('announcement_types AS at', 'a.type_id', '=', 'at.type_id')->whereNull('at.type_id')->whereNotNull('a.type_id')->update(['a.type_id' => null])) { - $this->_installer->log("Reset {$count} announcements with orphaned (non-null) announcement types to no announcement type."); + issue_galleys: { + // Depends directly on ~2 entities: file_id->issue_files.file_id issue_id->issues.issue_id + // Dependent entities: ~3 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('issue_galleys', 'issue_id', 'issues', 'issue_id'); + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('issue_galleys', 'file_id', 'issue_files', 'file_id'); } - // Clean orphaned announcement_setting entries - $orphanedIds = DB::table('announcement_settings AS a_s')->leftJoin('announcements AS a', 'a_s.announcement_id', '=', 'a.announcement_id')->whereNull('a.announcement_id')->distinct()->pluck('a_s.announcement_id'); - foreach ($orphanedIds as $announcementId) { - $this->_installer->log("Removing orphaned settings for missing announcement ID {$announcementId}"); - DB::table('announcement_settings')->where('announcement_id', '=', $announcementId)->delete(); + review_rounds: { + // Depends directly on ~1 entities: submission_id->submissions.submission_id + // Dependent entities: ~3 + $this->deleteRequiredReference('review_rounds', 'submission_id', 'submissions', 'submission_id'); } - // Fix missing submission.locale by retrieving the value from the publication, see https://github.com/pkp/pkp-lib/issues/7190 - $rows = DB::table('submissions AS s')->join('publications p', 'p.publication_id', '=', 's.current_publication_id') - ->whereNull('s.locale') - ->whereNotNull('p.locale') - ->get(['s.submission_id', 'p.locale']); - foreach ($rows as $row) { - $this->_installer->log("Updating locale of submission {$row->submission_id} to {$row->locale}."); - DB::table('submissions')->where('submission_id', '=', $row->submission_id)->update(['locale' => $row->locale]); + sections: { + // Depends directly on ~2 entities: journal_id->journals.journal_id review_form_id->review_forms.review_form_id + // Dependent entities: ~3 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('sections', $this->getContextKeyField(), $this->getContextTable(), $this->getContextKeyField()); + // Custom field (not found in at least one of the softwares) + $this->deleteOptionalReference('sections', 'review_form_id', 'review_forms', 'review_form_id'); } - if ($count = DB::table('submissions AS s')->whereNull('locale')->count()) { - throw new Exception('There are submission records with null in the locale column. Please correct these before upgrading.'); - } - - // Clean orphaned category data - $orphanedIds = DB::table('categories AS ca')->leftJoin($this->getContextTable() . ' AS c', 'ca.context_id', '=', 'c.' . $this->getContextKeyField())->whereNull('c.' . $this->getContextKeyField())->distinct()->pluck('ca.category_id'); - foreach ($orphanedIds as $categoryId) { - $this->_installer->log("Removing orphaned category ID {$categoryId} with no matching context ID."); - DB::table('categories')->where('category_id', '=', $categoryId)->delete(); - } - - // Clean orphaned category data - $orphanedIds = DB::table('categories AS c')->leftJoin('categories AS pc', 'pc.category_id', '=', 'c.parent_id')->whereNull('pc.category_id')->pluck('c.category_id'); - foreach ($orphanedIds as $categoryId) { - $this->_installer->log("Removing orphaned category ID {$categoryId} with no matching parent category ID."); - DB::table('categories')->where('category_id', '=', $categoryId)->delete(); - } - - // Clean orphaned category_setting entries - $orphanedIds = DB::table('category_settings AS cs')->leftJoin('categories AS c', 'cs.category_id', '=', 'c.category_id')->whereNull('c.category_id')->distinct()->pluck('cs.category_id'); - foreach ($orphanedIds as $categoryId) { - $this->_installer->log("Removing orphaned settings for missing category ID {$categoryId}"); - DB::table('category_settings')->where('category_id', '=', $categoryId)->delete(); - } - - // Clean orphaned publication_categories entries - $orphanedIds = DB::table('publication_categories AS pc')->leftJoin('categories AS c', 'pc.category_id', '=', 'c.category_id')->whereNull('c.category_id')->distinct()->pluck('pc.category_id'); - foreach ($orphanedIds as $categoryId) { - $this->_installer->log("Removing orphaned category/publication associations for missing category ID {$categoryId}"); - DB::table('publication_categories')->where('category_id', '=', $categoryId)->delete(); - } - $orphanedIds = DB::table('publication_categories AS pc')->leftJoin('publications AS p', 'pc.publication_id', '=', 'p.publication_id')->whereNull('p.publication_id')->distinct()->pluck('pc.publication_id'); - foreach ($orphanedIds as $publicationId) { - $this->_installer->log("Removing orphaned category/publication associations for missing publication ID {$publicationId}"); - DB::table('publication_categories')->where('publication_id', '=', $publicationId)->delete(); - } - - // Clean orphaned genre data - $orphanedIds = DB::table('genres AS g')->leftJoin($this->getContextTable() . ' AS c', 'g.context_id', '=', 'c.' . $this->getContextKeyField())->whereNull('c.' . $this->getContextKeyField())->distinct()->pluck('g.genre_id'); - foreach ($orphanedIds as $genreId) { - $this->_installer->log("Removing orphaned genre ID {$genreId} with no matching context ID."); - DB::table('genres')->where('genre_id', '=', $genreId)->delete(); - } - // Clean orphaned genre_setting entries - $orphanedIds = DB::table('genre_settings AS gs')->leftJoin('genres AS g', 'gs.genre_id', '=', 'g.genre_id')->whereNull('g.genre_id')->distinct()->pluck('gs.genre_id'); - foreach ($orphanedIds as $genreId) { - $this->_installer->log("Removing orphaned settings for missing genre ID {$genreId}"); - DB::table('genre_settings')->where('genre_id', '=', $genreId)->delete(); - } - // Clean orphan notification_settings - $orphanedIds = DB::table('notification_settings AS ns')->leftJoin('notifications AS n', 'ns.notification_id', '=', 'n.notification_id')->whereNull('n.notification_id')->distinct()->pluck('ns.notification_id'); - foreach ($orphanedIds as $notificationId) { - $this->_installer->log("Removing orphaned settings for missing notification ID {$notificationId}"); - DB::table('notification_settings')->where('notification_id', '=', $notificationId)->delete(); - } - // Clean orphan notification_subscription_settings by user ID - $orphanedIds = DB::table('notification_subscription_settings AS nss')->leftJoin('users AS u', 'nss.user_id', '=', 'u.user_id')->whereNull('u.user_id')->distinct()->pluck('nss.user_id'); - foreach ($orphanedIds as $userId) { - DB::table('notification_subscription_settings')->where('user_id', '=', $userId)->delete(); - } - // Clean orphan notification_subscription_settings by context ID - $orphanedIds = DB::table('notification_subscription_settings AS nss')->leftJoin($this->getContextTable() . ' AS c', 'nss.context', '=', 'c.' . $this->getContextKeyField())->whereNull('c.' . $this->getContextKeyField())->whereNotNull('nss.context')->distinct()->pluck('nss.context'); - foreach ($orphanedIds as $contextId) { - DB::table('notification_subscription_settings')->where('context', '=', $contextId)->delete(); - } - // Clean orphan submission_file_settings - $orphanedIds = DB::table('submission_file_settings AS sfs')->leftJoin('submission_files AS sf', 'sfs.submission_file_id', '=', 'sf.submission_file_id')->whereNull('sf.submission_file_id')->distinct()->pluck('sfs.submission_file_id'); - foreach ($orphanedIds as $submissionFileId) { - $this->_installer->log("Removing orphaned settings for missing submission file ID {$submissionFileId}"); - DB::table('submission_file_settings')->where('submission_file_id', '=', $submissionFileId)->delete(); - } - // Clean orphan query_participants - non existing user - $orphanedIds = DB::table('query_participants AS qp')->leftJoin('users AS u', 'qp.user_id', '=', 'u.user_id')->whereNull('u.user_id')->distinct()->pluck('qp.user_id'); - foreach ($orphanedIds as $userId) { - $this->_installer->log("Removing orphaned query_participants for missing user ID {$userId}"); - DB::table('query_participants')->where('user_id', '=', $userId)->delete(); - } - // Clean orphan query_participants - non existing query - $orphanedIds = DB::table('query_participants AS qp')->leftJoin('queries AS q', 'qp.query_id', '=', 'q.query_id')->whereNull('q.query_id')->distinct()->pluck('qp.query_id'); - foreach ($orphanedIds as $queryId) { - $this->_installer->log("Removing orphaned query_participants for missing query ID {$queryId}"); - DB::table('query_participants')->where('query_id', '=', $queryId)->delete(); - } - // Clean orphaned controlled_vocab_entry entries - $orphanedIds = DB::table('controlled_vocab_entries AS cve')->leftJoin('controlled_vocabs AS cv', 'cve.controlled_vocab_id', '=', 'cv.controlled_vocab_id')->whereNull('cv.controlled_vocab_id')->distinct()->pluck('cve.controlled_vocab_id'); - foreach ($orphanedIds as $controlledVocabId) { - $this->_installer->log("Removing orphaned controlled_vocab_entries for missing controlled_vocab_id {$controlledVocabId}"); - DB::table('controlled_vocab_entries')->where('controlled_vocab_id', '=', $controlledVocabId)->delete(); - } - // Clean orphaned controlled_vocab_entry_settings entries - $orphanedIds = DB::table('controlled_vocab_entry_settings AS cves')->leftJoin('controlled_vocab_entries AS cve', 'cves.controlled_vocab_entry_id', '=', 'cve.controlled_vocab_entry_id')->whereNull('cve.controlled_vocab_entry_id')->distinct()->pluck('cves.controlled_vocab_entry_id'); - foreach ($orphanedIds as $controlledVocabEntryId) { - $this->_installer->log("Removing orphaned controlled_vocab_entry_settings for missing controlled_vocab_entry_id {$controlledVocabEntryId}"); - DB::table('controlled_vocab_entry_settings')->where('controlled_vocab_entry_id', '=', $controlledVocabEntryId)->delete(); - } - // Clean orphaned user_interests entries by user ID - $orphanedIds = DB::table('user_interests AS ui')->leftJoin('users AS u', 'ui.user_id', '=', 'u.user_id')->whereNull('u.user_id')->distinct()->pluck('ui.user_id'); - foreach ($orphanedIds as $userId) { - $this->_installer->log("Removing orphaned user_interests for missing user_id {$userId}"); - DB::table('user_interests')->where('user_id', '=', $userId)->delete(); - } - // Clean orphaned user_interests entries by controlled_vocab_entry_id - $orphanedIds = DB::table('user_interests AS ui')->leftJoin('controlled_vocab_entries AS cve', 'ui.controlled_vocab_entry_id', '=', 'cve.controlled_vocab_entry_id')->whereNull('ui.controlled_vocab_entry_id')->distinct()->pluck('ui.controlled_vocab_entry_id'); - foreach ($orphanedIds as $controlledVocabEntryId) { - $this->_installer->log("Removing orphaned user_interests for missing controlled_vocab_entry_id {$controlledVocabEntryId}"); - DB::table('user_interests')->where('controlled_vocab_entry_id', '=', $controlledVocabEntryId)->delete(); - } - // Clean orphaned user_setting entries - $orphanedIds = DB::table('user_settings AS us')->leftJoin('users AS u', 'us.user_id', '=', 'u.user_id')->whereNull('u.user_id')->distinct()->pluck('us.user_id'); - foreach ($orphanedIds as $userId) { - DB::table('user_settings')->where('user_id', '=', $userId)->delete(); - } - // Clean orphaned sessions entries by user_id - $orphanedIds = DB::table('sessions AS s')->leftJoin('users AS u', 's.user_id', '=', 'u.user_id')->whereNull('u.user_id')->whereNotNull('s.user_id')->distinct()->pluck('s.user_id'); - foreach ($orphanedIds as $userId) { - DB::table('sessions')->where('user_id', '=', $userId)->delete(); - } - // Clean orphaned email_template entries by context_id - $orphanedIds = DB::table('email_templates AS et')->leftJoin($this->getContextTable() . ' AS c', 'et.context_id', '=', 'c.' . $this->getContextKeyField())->whereNull('c.' . $this->getContextKeyField())->distinct()->pluck('et.context_id'); - foreach ($orphanedIds as $contextId) { - $this->_installer->log("Removing orphaned email_templates for missing context_id {$contextId}"); - DB::table('email_templates')->where('context_id', '=', $contextId)->delete(); - } - // Clean orphaned email_templates_settings entries by email_id - $orphanedIds = DB::table('email_templates_settings AS ets')->leftJoin('email_templates AS et', 'et.email_id', '=', 'ets.email_id')->whereNull('et.email_id')->distinct()->pluck('ets.email_id'); - foreach ($orphanedIds as $emailId) { - $this->_installer->log("Removing orphaned email_templates_settings for missing email_id {$emailId}"); - DB::table('email_templates_settings')->where('email_id', '=', $emailId)->delete(); - } - // Clean orphaned library_files entries by context_id - $orphanedIds = DB::table('library_files AS lf')->leftJoin($this->getContextTable() . ' AS c', 'lf.context_id', '=', 'c.' . $this->getContextKeyField())->whereNull('c.' . $this->getContextKeyField())->distinct()->pluck('lf.context_id'); - foreach ($orphanedIds as $contextId) { - $this->_installer->log("Removing orphaned library_files for missing context_id {$contextId}"); - DB::table('library_files')->where('context_id', '=', $contextId)->delete(); - } - // Clean orphaned library_files entries by submission_id - $orphanedIds = DB::table('library_files AS lf')->leftJoin('submissions AS s', 's.submission_id', '=', 'lf.submission_id')->where('lf.submission_id', '<>', 0)->whereNull('s.submission_id')->distinct()->pluck('lf.submission_id'); - foreach ($orphanedIds as $submissionId) { - $this->_installer->log("Removing orphaned library_files for missing submission_id {$submissionId}"); - DB::table('library_files')->where('submission_id', '=', $submissionId)->delete(); - } - // Clean orphaned library_file_setting entries - $orphanedIds = DB::table('library_file_settings AS lfs')->leftJoin('library_files AS lf', 'lfs.file_id', '=', 'lf.file_id')->whereNull('lf.file_id')->distinct()->pluck('lfs.file_id'); - foreach ($orphanedIds as $fileId) { - $this->_installer->log("Removing orphaned settings for missing library_file {$fileId}"); - DB::table('library_file_settings')->where('file_id', '=', $fileId)->delete(); - } - // Clean orphaned event_log entries by user_id - $orphanedIds = DB::table('event_log AS el')->leftJoin('users AS u', 'el.user_id', '=', 'u.user_id')->whereNull('u.user_id')->distinct()->pluck('el.user_id'); - foreach ($orphanedIds as $userId) { - DB::table('event_log')->where('user_id', '=', $userId)->delete(); - } - // Clean orphaned event_log_settings entries - $orphanedIds = DB::table('event_log_settings AS els')->leftJoin('event_log AS el', 'els.log_id', '=', 'el.log_id')->whereNull('el.log_id')->distinct()->pluck('els.log_id'); - foreach ($orphanedIds as $logId) { - DB::table('event_log_settings')->where('log_id', '=', $logId)->delete(); - } - // Clean orphaned email_log_users entries by user_id - $orphanedIds = DB::table('email_log_users AS elu')->leftJoin('users AS u', 'elu.user_id', '=', 'u.user_id')->whereNull('u.user_id')->distinct()->pluck('elu.user_id'); - foreach ($orphanedIds as $userId) { - DB::table('email_log_users')->where('user_id', '=', $userId)->delete(); - } - // Clean orphaned email_log_users entries by email_log_id - $orphanedIds = DB::table('email_log_users AS elu')->leftJoin('email_log AS el', 'el.log_id', '=', 'elu.email_log_id')->whereNull('el.log_id')->distinct()->pluck('elu.email_log_id'); - foreach ($orphanedIds as $logId) { - DB::table('email_log_users')->where('email_log_id', '=', $logId)->delete(); - } - // Clean orphaned citations entries by publication_id - $orphanedIds = DB::table('citations AS c')->leftJoin('publications AS p', 'c.publication_id', '=', 'p.publication_id')->whereNull('p.publication_id')->distinct()->pluck('c.publication_id'); - foreach ($orphanedIds as $publicationId) { - DB::table('citations')->where('publication_id', '=', $publicationId)->delete(); - } - // Clean orphaned citation_settings entries - $orphanedIds = DB::table('citation_settings AS cs')->leftJoin('citations AS c', 'cs.citation_id', '=', 'c.citation_id')->whereNull('c.citation_id')->distinct()->pluck('cs.citation_id'); - foreach ($orphanedIds as $citationId) { - DB::table('citation_settings')->where('citation_id', '=', $citationId)->delete(); - } - // Clean orphaned filters entries by filter_group_id - $orphanedIds = DB::table('filters AS f')->leftJoin('filter_groups AS fg', 'f.filter_group_id', '=', 'fg.filter_group_id')->whereNull('fg.filter_group_id')->distinct()->pluck('f.filter_group_id'); - foreach ($orphanedIds as $filterGroupId) { - $this->_installer->log("Removing orphaned filters for missing filter_group {$filterGroupId}"); - DB::table('filters')->where('filter_group_id', '=', $filterGroupId)->delete(); - } - // Clean orphaned filter_settings entries - $orphanedIds = DB::table('filter_settings AS fs')->leftJoin('filters AS f', 'fs.filter_id', '=', 'f.filter_id')->whereNull('f.filter_id')->distinct()->pluck('fs.filter_id'); - foreach ($orphanedIds as $filterId) { - DB::table('filter_settings')->where('filter_id', '=', $filterId)->delete(); - } - // Clean orphaned temporary_files entries by user_id - $orphanedIds = DB::table('temporary_files AS tf')->leftJoin('users AS u', 'tf.user_id', '=', 'u.user_id')->whereNull('u.user_id')->distinct()->pluck('tf.user_id'); - foreach ($orphanedIds as $userId) { - DB::table('temporary_files')->where('user_id', '=', $userId)->delete(); - } - // Clean orphaned notes entries by user_id - $orphanedIds = DB::table('notes AS n')->leftJoin('users AS u', 'n.user_id', '=', 'u.user_id')->whereNull('u.user_id')->distinct()->pluck('n.user_id'); - foreach ($orphanedIds as $userId) { - DB::table('notes')->where('user_id', '=', $userId)->delete(); - } - // Clean orphaned navigation_menu_item_settings entries - $orphanedIds = DB::table('navigation_menu_item_settings AS nmis')->leftJoin('navigation_menu_items AS nmi', 'nmis.navigation_menu_item_id', '=', 'nmi.navigation_menu_item_id')->whereNull('nmi.navigation_menu_item_id')->distinct()->pluck('nmis.navigation_menu_item_id'); - foreach ($orphanedIds as $navigationMenuItemId) { - DB::table('navigation_menu_item_settings')->where('navigation_menu_item_id', '=', $navigationMenuItemId)->delete(); - } - // Clean orphaned navigation_menu_item_assignments by navigation_menu_item_id - $orphanedIds = DB::table('navigation_menu_item_assignments AS nmia')->leftJoin('navigation_menu_items AS nmi', 'nmia.navigation_menu_item_id', '=', 'nmi.navigation_menu_item_id')->whereNull('nmi.navigation_menu_item_id')->distinct()->pluck('nmia.navigation_menu_item_id'); - foreach ($orphanedIds as $navigationMenuItemId) { - DB::table('navigation_menu_item_assignments')->where('navigation_menu_item_id', '=', $navigationMenuItemId)->delete(); - } - // Clean orphaned navigation_menu_item_assignments by navigation_menu_id - $orphanedIds = DB::table('navigation_menu_item_assignments AS nmia')->leftJoin('navigation_menus AS nm', 'nmia.navigation_menu_id', '=', 'nm.navigation_menu_id')->whereNull('nm.navigation_menu_id')->distinct()->pluck('nmia.navigation_menu_id'); - foreach ($orphanedIds as $navigationMenuId) { - DB::table('navigation_menu_item_assignments')->where('navigation_menu_id', '=', $navigationMenuId)->delete(); - } - // Clean orphaned navigation_menu_item_assignment_settings entries - $orphanedIds = DB::table('navigation_menu_item_assignment_settings AS nmias')->leftJoin('navigation_menu_item_assignments AS nmia', 'nmias.navigation_menu_item_assignment_id', '=', 'nmia.navigation_menu_item_assignment_id')->whereNull('nmia.navigation_menu_item_assignment_id')->distinct()->pluck('nmias.navigation_menu_item_assignment_id'); - foreach ($orphanedIds as $navigationMenuItemAssignmentId) { - DB::table('navigation_menu_item_assignment_settings')->where('navigation_menu_item_assignment_id', '=', $navigationMenuItemAssignmentId)->delete(); - } - // Clean orphaned completed_payments by context_id - $orphanedIds = DB::table('completed_payments AS n')->leftJoin($this->getContextTable() . ' AS c', 'n.context_id', '=', 'c.' . $this->getContextKeyField())->whereNull('c.' . $this->getContextKeyField())->distinct()->pluck('n.context_id'); - foreach ($orphanedIds as $contextId) { - $this->_installer->log("Removing completed payments from orphan context ID {$contextId}"); - DB::table('completed_payments')->where('context_id', '=', $contextId)->delete(); - } - // Clean orphaned completed_payments by user_id - $orphanedIds = DB::table('completed_payments AS n')->leftJoin('users AS u', 'n.user_id', '=', 'u.user_id')->whereNotNull('n.user_id')->whereNull('u.user_id')->distinct()->pluck('n.user_id'); - foreach ($orphanedIds as $userId) { - $this->_installer->log("Removing completed payments from orphan user ID {$userId}"); - DB::table('completed_payments')->where('user_id', '=', $userId)->delete(); - } - - if (Schema::hasTable('review_assignments')) { - // Clean orphaned review_assignments entries by review_form_id - $orphanedIds = DB::table('review_assignments AS r')->leftJoin('review_forms AS rf', 'rf.review_form_id', '=', 'r.review_form_id')->whereNotNull('r.review_form_id')->whereNull('rf.review_form_id')->distinct()->pluck('r.review_form_id'); - foreach ($orphanedIds as $reviewFormId) { - DB::table('review_assignments')->where('review_form_id', '=', $reviewFormId)->delete(); - } - // Clean orphaned review_assignments entries by review_round_id - $orphanedIds = DB::table('review_assignments AS r')->leftJoin('review_rounds AS rr', 'rr.review_round_id', '=', 'r.review_round_id')->whereNull('rr.review_round_id')->distinct()->pluck('r.review_round_id'); - foreach ($orphanedIds as $reviewRoundId) { - DB::table('review_assignments')->where('review_round_id', '=', $reviewRoundId)->delete(); - } - // Clean orphaned review_assignments entries by reviewer_id - $orphanedIds = DB::table('review_assignments AS r')->leftJoin('users AS u', 'u.user_id', '=', 'r.reviewer_id')->whereNull('u.user_id')->distinct()->pluck('r.reviewer_id'); - foreach ($orphanedIds as $reviewerId) { - DB::table('review_assignments')->where('reviewer_id', '=', $reviewerId)->delete(); - } - // Clean orphaned review_assignments entries by submission_id - $orphanedIds = DB::table('review_assignments AS r')->leftJoin('submissions AS s', 's.submission_id', '=', 'r.submission_id')->whereNull('s.submission_id')->distinct()->pluck('r.submission_id'); - foreach ($orphanedIds as $submissionId) { - DB::table('review_assignments')->where('submission_id', '=', $submissionId)->delete(); + announcement_types: { + // Depends directly on ~1 entities: context_id->journals.journal_id + // Dependent entities: ~2 + // Deprecated/moved field (not found on previous software version) + // $this->deleteRequiredReference('announcement_types', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + // Clean orphaned assoc_type/assoc_id data in announcement_types + $orphanedIds = DB::table('announcement_types AS at') + ->leftJoin($this->getContextTable() . ' AS c', 'at.assoc_id', '=', 'c.' . $this->getContextKeyField()) + ->whereNull('c.' . $this->getContextKeyField()) + ->orWhere('at.assoc_type', '<>', Application::get()->getContextAssocType()) + ->distinct() + ->pluck('at.type_id'); + foreach ($orphanedIds as $typeId) { + $this->_installer->log("Removing orphaned announcement type ID {$typeId} with no matching context ID."); + DB::table('announcement_types')->where('type_id', '=', $typeId)->delete(); } } - if (Schema::hasTable('review_form_settings')) { - // Clean orphaned review_form_settings entries - $orphanedIds = DB::table('review_form_settings AS rfs')->leftJoin('review_forms AS rf', 'rf.review_form_id', '=', 'rfs.review_form_id')->whereNull('rf.review_form_id')->distinct()->pluck('rfs.review_form_id'); - foreach ($orphanedIds as $reviewFormId) { - DB::table('review_form_settings')->where('review_form_id', '=', $reviewFormId)->delete(); - } + authors: { + // Depends directly on ~2 entities: publication_id->publications.publication_id user_group_id->user_groups.user_group_id + // Dependent entities: ~2 + $this->deleteRequiredReference('authors', 'publication_id', 'publications', 'publication_id'); + $this->deleteOptionalReference('authors', 'user_group_id', 'user_groups', 'user_group_id'); } - if (Schema::hasTable('review_form_elements')) { - // Clean orphaned review_form_elements entries - $orphanedIds = DB::table('review_form_elements AS rfe')->leftJoin('review_forms AS rf', 'rf.review_form_id', '=', 'rfe.review_form_id')->whereNull('rf.review_form_id')->distinct()->pluck('rfe.review_form_id'); - foreach ($orphanedIds as $reviewFormId) { - DB::table('review_form_elements')->where('review_form_id', '=', $reviewFormId)->delete(); - } - // Clean orphaned review_form_element_settings entries - $orphanedIds = DB::table('review_form_element_settings AS rfes')->leftJoin('review_form_elements AS rfe', 'rfes.review_form_element_id', '=', 'rfe.review_form_element_id')->whereNull('rfe.review_form_element_id')->distinct()->pluck('rfes.review_form_element_id'); - foreach ($orphanedIds as $reviewFormElementId) { - DB::table('review_form_element_settings')->where('review_form_element_id', '=', $reviewFormElementId)->delete(); - } - // Clean orphaned review_form_responses entries by review_form_element_id - $orphanedIds = DB::table('review_form_responses AS rfr')->leftJoin('review_form_elements AS rfe', 'rfe.review_form_element_id', '=', 'rfr.review_form_element_id')->whereNull('rfe.review_form_element_id')->distinct()->pluck('rfr.review_form_element_id'); - foreach ($orphanedIds as $reviewFormElementId) { - $this->_installer->log("Removing orphaned review_form_responses for missing review_form_element_id {$reviewFormElementId}"); - DB::table('review_form_responses')->where('review_form_element_id', '=', $reviewFormElementId)->delete(); + controlled_vocab_entries: { + // Depends directly on ~1 entities: controlled_vocab_id->controlled_vocabs.controlled_vocab_id + // Dependent entities: ~2 + $this->deleteRequiredReference('controlled_vocab_entries', 'controlled_vocab_id', 'controlled_vocabs', 'controlled_vocab_id'); + } + + filters: { + // Depends directly on ~3 entities: context_id->journals.journal_id filter_group_id->filter_groups.filter_group_id parent_filter_id->filters.filter_id + // Dependent entities: ~2 + $this->deleteRequiredReference('filters', 'filter_group_id', 'filter_groups', 'filter_group_id'); + $this->deleteOptionalReference('filters', 'parent_filter_id', 'filters', 'filter_id'); + // Custom field (not found in at least one of the softwares) + $this->deleteOptionalReference('filters', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + } + + genres: { + // Depends directly on ~1 entities: context_id->journals.journal_id + // Dependent entities: ~2 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('genres', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + } + + navigation_menu_item_assignments: { + // Depends directly on ~3 entities: navigation_menu_id->navigation_menus.navigation_menu_id navigation_menu_item_id->navigation_menu_items.navigation_menu_item_id parent_id->navigation_menu_item_assignments.navigation_menu_item_assignment_id + // Dependent entities: ~2 + $this->deleteRequiredReference('navigation_menu_item_assignments', 'navigation_menu_item_id', 'navigation_menu_items', 'navigation_menu_item_id'); + $this->deleteRequiredReference('navigation_menu_item_assignments', 'navigation_menu_id', 'navigation_menus', 'navigation_menu_id'); + $this->deleteOptionalReference('navigation_menu_item_assignments', 'parent_id', 'navigation_menu_item_assignments', 'navigation_menu_item_assignment_id'); + } + + navigation_menu_items: { + // Depends directly on ~1 entities: context_id->journals.journal_id + // Dependent entities: ~2 + // Custom field (not found in at least one of the softwares) + $this->deleteOptionalReference('navigation_menu_items', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + } + + review_assignments: { + // Depends directly on ~4 entities: reviewer_id->users.user_id review_form_id->review_forms.review_form_id review_round_id->review_rounds.review_round_id submission_id->submissions.submission_id + // Dependent entities: ~2 + if (Schema::hasTable('review_assignments')) { + $this->deleteRequiredReference('review_assignments', 'submission_id', 'submissions', 'submission_id'); + $this->deleteRequiredReference('review_assignments', 'review_round_id', 'review_rounds', 'review_round_id'); + $this->deleteRequiredReference('review_assignments', 'reviewer_id', 'users', 'user_id'); + $this->deleteOptionalReference('review_assignments', 'review_form_id', 'review_forms', 'review_form_id'); } - // Clean orphaned review_form_responses entries by review_id - $orphanedIds = DB::table('review_form_responses AS rfr')->leftJoin('review_assignments AS ra', 'rfr.review_id', '=', 'ra.review_id')->whereNull('ra.review_id')->distinct()->pluck('rfr.review_id'); - foreach ($orphanedIds as $reviewId) { - $this->_installer->log("Removing orphaned review_form_responses for missing review_id {$reviewId}"); - DB::table('review_form_responses')->where('review_id', '=', $reviewId)->delete(); + } + + review_form_elements: { + // Depends directly on ~1 entities: review_form_id->review_forms.review_form_id + // Dependent entities: ~2 + if (Schema::hasTable('review_form_elements')) { + $this->deleteRequiredReference('review_form_elements', 'review_form_id', 'review_forms', 'review_form_id'); } } - // Clean orphaned review_form_responses entries by review_id - $orphanedIds = DB::table('review_files AS rf')->leftJoin('review_assignments AS ra', 'rf.review_id', '=', 'ra.review_id')->whereNull('ra.review_id')->distinct()->pluck('rf.review_id'); - foreach ($orphanedIds as $reviewId) { - $this->_installer->log("Removing orphaned review_files for missing review_id {$reviewId}"); - DB::table('review_files')->where('review_id', '=', $reviewId)->delete(); + + subscription_types: { + // Depends directly on ~1 entities: journal_id->journals.journal_id + // Dependent entities: ~2 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('subscription_types', $this->getContextKeyField(), $this->getContextTable(), $this->getContextKeyField()); } - // Clean orphaned submissions by context_id - $rows = DB::table('submissions AS s')->leftJoin($this->getContextTable() . ' AS c', 's.context_id', '=', 'c.' . $this->getContextKeyField())->whereNull('c.' . $this->getContextKeyField())->select(['s.submission_id', 's.context_id'])->get(); - foreach ($rows as $row) { - $this->_installer->log("Removing orphaned submission ID {$row->submission_id} with nonexistent context ID {$row->context_id}."); - DB::table('submissions')->where('submission_id', '=', $row->submission_id)->delete(); - } - // Clean orphaned submission_settings entries - $orphanedIds = DB::table('submission_settings AS ss')->leftJoin('submissions AS s', 'ss.submission_id', '=', 's.submission_id')->whereNull('s.submission_id')->distinct()->pluck('ss.submission_id'); - foreach ($orphanedIds as $submissionId) { - DB::table('submission_settings')->where('submission_id', '=', $submissionId)->delete(); - } - // Clean orphaned review_rounds entries by submission_id - $orphanedIds = DB::table('review_rounds AS rr')->leftJoin('submissions AS s', 's.submission_id', '=', 'rr.submission_id')->whereNull('s.submission_id')->distinct()->pluck('rr.review_round_id'); - foreach ($orphanedIds as $reviewRoundId) { - DB::table('review_rounds')->where('review_round_id', '=', $reviewRoundId)->delete(); - } - // Clean orphaned publications entries by submission_id - $orphanedIds = DB::table('publications AS p')->leftJoin('submissions AS s', 's.submission_id', '=', 'p.submission_id')->whereNull('s.submission_id')->distinct()->pluck('p.publication_id'); - foreach ($orphanedIds as $publicationId) { - DB::table('publications')->where('publication_id', '=', $publicationId)->delete(); - } - // Clean orphaned publications entries by primary_contact_id - if(DB::connection() instanceof MySqlConnection) { - DB::statement('UPDATE publications p LEFT JOIN users u ON (p.primary_contact_id = u.user_id) SET p.primary_contact_id = NULL WHERE u.user_id IS NULL'); - } elseif (DB::connection() instanceof PostgresConnection) { - DB::statement('UPDATE publications SET primary_contact_id = NULL WHERE publication_id IN (SELECT publication_id FROM publications p LEFT JOIN users u ON (p.primary_contact_id = u.user_id) WHERE u.user_id IS NULL AND p.primary_contact_id IS NOT NULL)'); - } else { - throw new Exception('Unknown database connection type!'); - } - // Clean orphaned publication_settings entries - $orphanedIds = DB::table('publication_settings AS ps')->leftJoin('publications AS p', 'ps.publication_id', '=', 'p.publication_id')->whereNull('p.publication_id')->distinct()->pluck('ps.publication_id'); - foreach ($orphanedIds as $publicationId) { - DB::table('publication_settings')->where('publication_id', '=', $publicationId)->delete(); - } - // Clean orphaned authors entries by publication_id - $orphanedIds = DB::table('authors AS a')->leftJoin('publications AS p', 'a.publication_id', '=', 'p.publication_id')->whereNull('p.publication_id')->distinct()->pluck('a.publication_id'); - foreach ($orphanedIds as $publicationId) { - DB::table('authors')->where('publication_id', '=', $publicationId)->delete(); + + announcements: { + // Depends directly on ~1 entities: type_id->announcement_types.type_id + // Dependent entities: ~1 + $this->cleanOptionalReference('announcements', 'type_id', 'announcement_types', 'type_id'); } - // Flag orphaned authors entries by user_group_id - $result = DB::table('authors AS a')->leftJoin('user_groups AS ug', 'ug.user_group_id', '=', 'a.user_group_id')->leftJoin('publications AS p', 'p.publication_id', '=', 'a.publication_id')->whereNull('ug.user_group_id')->select('a.author_id AS author_id', 'a.publication_id AS publication_id', 'a.user_group_id AS user_group_id', 'p.submission_id AS submission_id')->get(); - foreach ($result as $row) { - $this->_installer->log("Found an orphaned author entry with author_id {$row->author_id} for publication_id {$row->publication_id} with submission_id {$row->submission_id} and user_group_id {$row->user_group_id}."); + citations: { + // Depends directly on ~1 entities: publication_id->publications.publication_id + // Dependent entities: ~1 + $this->deleteRequiredReference('citations', 'publication_id', 'publications', 'publication_id'); } - if ($result->count()) { - throw new Exception('There are author records without matching user_group entries. Please correct these before upgrading.'); + + email_log: { + // Depends directly on ~1 entities: sender_id->users.user_id + // Dependent entities: ~1 + $this->deleteOptionalReference('email_log', 'sender_id', 'users', 'user_id'); + } + + email_templates: { + // Depends directly on ~1 entities: context_id->journals.journal_id + // Dependent entities: ~1 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('email_templates', 'context_id', $this->getContextTable(), $this->getContextKeyField()); } - // Clean orphaned author_settings entries - $orphanedIds = DB::table('author_settings AS a_s')->leftJoin('authors AS a', 'a_s.author_id', '=', 'a.author_id')->whereNull('a.author_id')->distinct()->pluck('a_s.author_id'); - foreach ($orphanedIds as $authorId) { - DB::table('author_settings')->where('author_id', '=', $authorId)->delete(); + event_log: { + // Depends directly on ~1 entities: user_id->users.user_id + // Dependent entities: ~1 + $this->deleteRequiredReference('event_log', 'user_id', 'users', 'user_id'); } - // Clean orphaned edit_decisions entries by editor_id - $orphanedIds = DB::table('edit_decisions AS ed')->leftJoin('users AS u', 'u.user_id', '=', 'ed.editor_id')->whereNull('u.user_id')->distinct()->pluck('ed.editor_id'); - foreach ($orphanedIds as $editorId) { - $this->_installer->log("Removing orphaned edit_decisions entry for missing editor_id {$editorId}"); - DB::table('edit_decisions')->where('editor_id', '=', $editorId)->delete(); + + issue_files: { + // Depends directly on ~1 entities: issue_id->issues.issue_id + // Dependent entities: ~1 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('issue_files', 'issue_id', 'issues', 'issue_id'); } - // Clean orphaned edit_decisions entries by submission_id - $orphanedIds = DB::table('edit_decisions AS ed')->leftJoin('submissions AS s', 's.submission_id', '=', 'ed.submission_id')->whereNull('s.submission_id')->distinct()->pluck('ed.submission_id'); - foreach ($orphanedIds as $submissionId) { - $this->_installer->log("Removing orphaned edit_decisions entries for missing submission_id {$submissionId}"); - DB::table('edit_decisions')->where('submission_id', '=', $submissionId)->delete(); + + library_files: { + // Depends directly on ~2 entities: context_id->journals.journal_id submission_id->submissions.submission_id + // Dependent entities: ~1 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('library_files', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + $this->deleteOptionalReference('library_files', 'submission_id', 'submissions', 'submission_id'); } - // Clean orphaned edit_decisions entries by review_round_id - $orphanedIds = DB::table('edit_decisions AS ed')->leftJoin('review_rounds AS r', 'r.review_round_id', '=', 'ed.review_round_id')->whereNull('r.review_round_id')->distinct()->pluck('ed.review_round_id'); - foreach ($orphanedIds as $reviewRoundId) { - $this->_installer->log("Removing orphaned edit_decisions entries for missing review_round_id {$reviewRoundId}"); - DB::table('edit_decisions')->where('review_round_id', '=', $reviewRoundId)->delete(); + + navigation_menus: { + // Depends directly on ~1 entities: context_id->journals.journal_id + // Dependent entities: ~1 + // Custom field (not found in at least one of the softwares) + $this->deleteOptionalReference('navigation_menus', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + } + + notifications: { + // Depends directly on ~2 entities: context_id->journals.journal_id user_id->users.user_id + // Dependent entities: ~1 + $this->deleteOptionalReference('notifications', 'user_id', 'users', 'user_id'); + // Custom field (not found in at least one of the softwares) + $this->deleteOptionalReference('notifications', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + } + + submission_search_objects: { + // Depends directly on ~1 entities: submission_id->submissions.submission_id + // Dependent entities: ~1 + $this->deleteRequiredReference('submission_search_objects', 'submission_id', 'submissions', 'submission_id'); } - // Clean orphaned submission_comments entries by submission_id - $orphanedIds = DB::table('submission_comments AS sc')->leftJoin('submissions AS s', 's.submission_id', '=', 'sc.submission_id')->whereNull('s.submission_id')->distinct()->pluck('sc.submission_id'); - foreach ($orphanedIds as $submissionId) { - $this->_installer->log("Removing orphaned submission_comments entry for missing submission_id {$submissionId}"); - DB::table('submission_comments')->where('submission_id', '=', $submissionId)->delete(); + + subscriptions: { + // Depends directly on ~3 entities: journal_id->journals.journal_id type_id->subscription_types.type_id user_id->users.user_id + // Dependent entities: ~1 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('subscriptions', 'user_id', 'users', 'user_id'); + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('subscriptions', 'type_id', 'subscription_types', 'type_id'); + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('subscriptions', $this->getContextKeyField(), $this->getContextTable(), $this->getContextKeyField()); } - // Clean orphaned submission_comments entries by author_id - $orphanedIds = DB::table('submission_comments AS sc')->leftJoin('users AS u', 'u.user_id', '=', 'sc.author_id')->whereNull('u.user_id')->distinct()->pluck('sc.author_id'); - foreach ($orphanedIds as $userId) { - $this->_installer->log("Removing orphaned submission_comments entry for missing author_id {$userId}"); - DB::table('submission_comments')->where('author_id', '=', $userId)->delete(); + access_keys: { + // Depends directly on ~1 entities: user_id->users.user_id + // Dependent entities: ~0 + $this->deleteRequiredReference('access_keys', 'user_id', 'users', 'user_id'); } - // Clean orphaned subeditor_submission_group entries by context_id - $orphanedIds = DB::table('subeditor_submission_group AS ssg')->leftJoin($this->getContextTable() . ' AS c', 'ssg.context_id', '=', 'c.' . $this->getContextKeyField())->whereNull('c.' . $this->getContextKeyField())->distinct()->pluck('ssg.context_id'); - foreach ($orphanedIds as $contextId) { - DB::table('subeditor_submission_group')->where('context_id', '=', $contextId)->delete(); + announcement_settings: { + // Depends directly on ~1 entities: announcement_id->announcements.announcement_id + // Dependent entities: ~0 + $this->deleteRequiredReference('announcement_settings', 'announcement_id', 'announcements', 'announcement_id'); } - // Clean orphaned subeditor_submission_group entries by user_id - $orphanedIds = DB::table('subeditor_submission_group AS ssg')->leftJoin('users AS u', 'u.user_id', '=', 'ssg.user_id')->whereNull('u.user_id')->distinct()->pluck('ssg.user_id'); - foreach ($orphanedIds as $userId) { - $this->_installer->log("Removing orphaned subeditor_submission_group entry for missing user_id {$userId}"); - DB::table('subeditor_submission_group')->where('user_id', '=', $userId)->delete(); + announcement_type_settings: { + // Depends directly on ~1 entities: type_id->announcement_types.type_id + // Dependent entities: ~0 + $this->deleteRequiredReference('announcement_type_settings', 'type_id', 'announcement_types', 'type_id'); } - // Clean orphaned submission_search_objects entries by submission_id - $orphanedIds = DB::table('submission_search_objects AS sso')->leftJoin('submissions AS s', 's.submission_id', '=', 'sso.submission_id')->whereNull('s.submission_id')->distinct()->pluck('sso.submission_id'); - foreach ($orphanedIds as $submissionId) { - DB::table('submission_search_objects')->where('submission_id', '=', $submissionId)->delete(); + author_settings: { + // Depends directly on ~1 entities: author_id->authors.author_id + // Dependent entities: ~0 + $this->deleteRequiredReference('author_settings', 'author_id', 'authors', 'author_id'); } - // Clean orphaned submission_search_object_keywords entries by object_id - $orphanedIds = DB::table('submission_search_object_keywords AS ssok')->leftJoin('submission_search_objects AS sso', 'ssok.object_id', '=', 'sso.object_id')->whereNull('sso.object_id')->distinct()->pluck('ssok.object_id'); - foreach ($orphanedIds as $objectId) { - DB::table('submission_search_object_keywords')->where('object_id', '=', $objectId)->delete(); + category_settings: { + // Depends directly on ~1 entities: category_id->categories.category_id + // Dependent entities: ~0 + $this->deleteRequiredReference('category_settings', 'category_id', 'categories', 'category_id'); } - // Clean orphaned submission_search_object_keywords entries by keyword_id - $orphanedIds = DB::table('submission_search_object_keywords AS ssok')->leftJoin('submission_search_keyword_list AS sskl', 'ssok.keyword_id', '=', 'sskl.keyword_id')->whereNull('sskl.keyword_id')->distinct()->pluck('ssok.keyword_id'); - foreach ($orphanedIds as $keywordId) { - DB::table('submission_search_object_keywords')->where('keyword_id', '=', $keywordId)->delete(); + citation_settings: { + // Depends directly on ~1 entities: citation_id->citations.citation_id + // Dependent entities: ~0 + $this->deleteRequiredReference('citation_settings', 'citation_id', 'citations', 'citation_id'); } - // Clean orphaned review_round_files entries by submission_id - $orphanedIds = DB::table('review_round_files AS rrf')->leftJoin('submissions AS s', 's.submission_id', '=', 'rrf.submission_id')->whereNull('s.submission_id')->distinct()->pluck('rrf.submission_id'); - foreach ($orphanedIds as $submissionId) { - DB::table('review_round_files')->where('submission_id', '=', $submissionId)->delete(); + completed_payments: { + // Depends directly on ~2 entities: context_id->journals.journal_id user_id->users.user_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('completed_payments', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + // Custom field (not found in at least one of the softwares) + $this->deleteOptionalReference('completed_payments', 'user_id', 'users', 'user_id'); } - // Clean orphaned review_round_files entries by review_round_id - $orphanedIds = DB::table('review_round_files AS rrf')->leftJoin('review_rounds AS s', 's.review_round_id', '=', 'rrf.review_round_id')->whereNull('s.review_round_id')->distinct()->pluck('rrf.review_round_id'); - foreach ($orphanedIds as $reviewRoundId) { - DB::table('review_round_files')->where('review_round_id', '=', $reviewRoundId)->delete(); + + controlled_vocab_entry_settings: { + // Depends directly on ~1 entities: controlled_vocab_entry_id->controlled_vocab_entries.controlled_vocab_entry_id + // Dependent entities: ~0 + $this->deleteRequiredReference('controlled_vocab_entry_settings', 'controlled_vocab_entry_id', 'controlled_vocab_entries', 'controlled_vocab_entry_id'); } - // Clean orphaned review_round_files entries by submission_file_id - $orphanedIds = DB::table('review_round_files AS rrf')->leftJoin('submission_files AS s', 's.submission_file_id', '=', 'rrf.submission_file_id')->whereNull('s.submission_file_id')->distinct()->pluck('rrf.submission_file_id'); - foreach ($orphanedIds as $submissionFileId) { - DB::table('review_round_files')->where('submission_file_id', '=', $submissionFileId)->delete(); + + custom_issue_orders: { + // Depends directly on ~2 entities: issue_id->issues.issue_id journal_id->journals.journal_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('custom_issue_orders', $this->getContextKeyField(), $this->getContextTable(), $this->getContextKeyField()); + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('custom_issue_orders', 'issue_id', 'issues', 'issue_id'); } - // Clean orphaned user_user_groups entries by user_id - $orphanedIds = DB::table('user_user_groups AS uug')->leftJoin('users AS u', 'u.user_id', '=', 'uug.user_id')->whereNull('u.user_id')->distinct()->pluck('uug.user_id'); - foreach ($orphanedIds as $userId) { - DB::table('user_user_groups')->where('user_id', '=', $userId)->delete(); + custom_section_orders: { + // Depends directly on ~2 entities: issue_id->issues.issue_id section_id->sections.section_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('custom_section_orders', 'section_id', 'sections', 'section_id'); + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('custom_section_orders', 'issue_id', 'issues', 'issue_id'); } - // Clean orphaned user_user_groups entries by user_group_id - $orphanedIds = DB::table('user_user_groups AS uug')->leftJoin('user_groups AS u', 'u.user_group_id', '=', 'uug.user_group_id')->whereNull('u.user_group_id')->distinct()->pluck('uug.user_group_id'); - foreach ($orphanedIds as $userGroupId) { - DB::table('user_user_groups')->where('user_group_id', '=', $userGroupId)->delete(); + data_object_tombstone_oai_set_objects: { + // Depends directly on ~1 entities: tombstone_id->data_object_tombstones.tombstone_id + // Dependent entities: ~0 + $this->deleteRequiredReference('data_object_tombstone_oai_set_objects', 'tombstone_id', 'data_object_tombstones', 'tombstone_id'); } - // Clean orphaned user_group_settings entries by user_group_id - $orphanedIds = DB::table('user_group_settings AS ugs')->leftJoin('user_groups AS u', 'u.user_group_id', '=', 'ugs.user_group_id')->whereNull('u.user_group_id')->distinct()->pluck('ugs.user_group_id'); - foreach ($orphanedIds as $userGroupId) { - DB::table('user_group_settings')->where('user_group_id', '=', $userGroupId)->delete(); + data_object_tombstone_settings: { + // Depends directly on ~1 entities: tombstone_id->data_object_tombstones.tombstone_id + // Dependent entities: ~0 + $this->deleteRequiredReference('data_object_tombstone_settings', 'tombstone_id', 'data_object_tombstones', 'tombstone_id'); } - // Clean orphaned user_group_stage entries by context_id - $orphanedIds = DB::table('user_group_stage AS ugs')->leftJoin($this->getContextTable() . ' AS c', 'ugs.context_id', '=', 'c.' . $this->getContextKeyField())->whereNull('c.' . $this->getContextKeyField())->distinct()->pluck('ugs.context_id'); - foreach ($orphanedIds as $contextId) { - DB::table('subeditor_submission_group')->where('context_id', '=', $contextId)->delete(); + edit_decisions: { + // Depends directly on ~3 entities: editor_id->users.user_id review_round_id->review_rounds.review_round_id submission_id->submissions.submission_id + // Dependent entities: ~0 + $this->deleteRequiredReference('edit_decisions', 'submission_id', 'submissions', 'submission_id'); + $this->deleteRequiredReference('edit_decisions', 'editor_id', 'users', 'user_id'); + $this->deleteOptionalReference('edit_decisions', 'review_round_id', 'review_rounds', 'review_round_id'); } - // Clean orphaned user_group_stage entries by user_group_id - $orphanedIds = DB::table('user_group_stage AS ugs')->leftJoin('user_groups AS c', 'ugs.user_group_id', '=', 'c.user_group_id')->whereNull('c.user_group_id')->distinct()->pluck('ugs.user_group_id'); - foreach ($orphanedIds as $userGroupId) { - DB::table('user_group_stage')->where('user_group_id', '=', $userGroupId)->delete(); + email_log_users: { + // Depends directly on ~2 entities: email_log_id->email_log.log_id user_id->users.user_id + // Dependent entities: ~0 + $this->deleteRequiredReference('email_log_users', 'user_id', 'users', 'user_id'); + $this->deleteRequiredReference('email_log_users', 'email_log_id', 'email_log', 'log_id'); } - // Clean orphaned stage_assignments entries by user_id - $orphanedIds = DB::table('stage_assignments AS sa')->leftJoin('users AS u', 'u.user_id', '=', 'sa.user_id')->whereNull('u.user_id')->distinct()->pluck('sa.user_id'); - foreach ($orphanedIds as $userId) { - DB::table('stage_assignments')->where('user_id', '=', $userId)->delete(); + email_templates_settings: { + // Depends directly on ~1 entities: email_id->email_templates.email_id + // Dependent entities: ~0 + $this->deleteRequiredReference('email_templates_settings', 'email_id', 'email_templates', 'email_id'); } - // Clean orphaned stage_assignments entries by user_group_id - $orphanedIds = DB::table('stage_assignments AS sa')->leftJoin('user_groups AS ug', 'ug.user_group_id', '=', 'sa.user_group_id')->whereNull('ug.user_group_id')->distinct()->pluck('sa.user_group_id'); - foreach ($orphanedIds as $userGroupId) { - DB::table('stage_assignments')->where('user_group_id', '=', $userGroupId)->delete(); + event_log_settings: { + // Depends directly on ~1 entities: log_id->event_log.log_id + // Dependent entities: ~0 + $this->deleteRequiredReference('event_log_settings', 'log_id', 'event_log', 'log_id'); } - // Clean orphaned stage_assignments entries by submission_id - $orphanedIds = DB::table('stage_assignments AS sa')->leftJoin('submissions AS s', 'sa.submission_id', '=', 's.submission_id')->whereNull('s.submission_id')->distinct()->pluck('sa.submission_id'); - foreach ($orphanedIds as $submissionId) { - DB::table('stage_assignments')->where('submission_id', '=', $submissionId)->delete(); + filter_settings: { + // Depends directly on ~1 entities: filter_id->filters.filter_id + // Dependent entities: ~0 + $this->deleteRequiredReference('filter_settings', 'filter_id', 'filters', 'filter_id'); } - // Clean orphaned submission_files entries by submission_id - $orphanedIds = DB::table('submission_files AS sf')->leftJoin('submissions AS s', 'sf.submission_id', '=', 's.submission_id')->whereNull('s.submission_id')->pluck('sf.submission_file_id'); - foreach ($orphanedIds as $submissionFileId) { - $this->_installer->log("Removing orphaned submission_files entry {$submissionFileId} with non-existent submission."); - DB::table('review_files')->where('submission_file_id', '=', $submissionFileId)->delete(); - DB::table('submission_file_revisions')->where('submission_file_id', '=', $submissionFileId)->delete(); - DB::table('submission_file_settings')->where('submission_file_id', '=', $submissionFileId)->delete(); - DB::table('submission_files')->where('submission_file_id', '=', $submissionFileId)->delete(); + + genre_settings: { + // Depends directly on ~1 entities: genre_id->genres.genre_id + // Dependent entities: ~0 + $this->deleteRequiredReference('genre_settings', 'genre_id', 'genres', 'genre_id'); } - // Clean orphaned submission_file_revisions entries by submission_file_id - $orphanedIds = DB::table('submission_file_revisions AS sfr')->leftJoin('submission_files AS sf', 'sf.submission_file_id', '=', 'sfr.submission_file_id')->whereNull('sf.submission_file_id')->pluck('sfr.submission_file_id'); - foreach ($orphanedIds as $submissionFileId) { - DB::table('submission_file_revisions')->where('submission_file_id', '=', $submissionFileId)->delete(); + + institutional_subscriptions: { + // Depends directly on ~2 entities: institution_id->institutions.institution_id(not found in previous version) subscription_id->subscriptions.subscription_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('institutional_subscriptions', 'subscription_id', 'subscriptions', 'subscription_id'); + // Deprecated/moved field (not found on previous software version) + // $this->deleteRequiredReference('institutional_subscriptions', 'institution_id', 'institutions', 'institution_id'); } - // Clean orphaned submission_file_revisions entries by file_id - $orphanedIds = DB::table('submission_file_revisions AS sfr')->leftJoin('files AS f', 'f.file_id', '=', 'sfr.file_id')->whereNull('f.file_id')->pluck('sfr.file_id'); - foreach ($orphanedIds as $fileId) { - DB::table('submission_file_revisions')->where('file_id', '=', $fileId)->delete(); + + issue_galley_settings: { + // Depends directly on ~1 entities: galley_id->issue_galleys.galley_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('issue_galley_settings', 'galley_id', 'issue_galleys', 'galley_id'); } - // Clean orphaned submission_files entries by file_id - $orphanedIds = DB::table('submission_files AS sf')->leftJoin('files AS f', 'sf.file_id', '=', 'f.file_id')->whereNull('f.file_id')->distinct()->pluck('sf.file_id'); - foreach ($orphanedIds as $fileId) { - $this->_installer->log("Removing orphaned submission_files entries for non-existent file_id {$fileId}."); - DB::table('submission_files')->where('file_id', '=', $fileId)->delete(); + + issue_settings: { + // Depends directly on ~1 entities: issue_id->issues.issue_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('issue_settings', 'issue_id', 'issues', 'issue_id'); } - // Clean orphaned submission_files entries by genre_id - $orphanedIds = DB::table('submission_files AS sf')->leftJoin('genres AS g', 'sf.genre_id', '=', 'g.genre_id')->whereNull('g.genre_id')->whereNotNull('sf.genre_id')->distinct()->pluck('sf.genre_id'); - foreach ($orphanedIds as $genreId) { - $this->_installer->log("Nulling non-existent genre_id {$genreId} in submission_files."); - DB::table('submission_files')->where('genre_id', '=', $genreId)->update(['genre_id' => null]); + + journal_settings: { + // Depends directly on ~1 entities: journal_id->journals.journal_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference($this->getContextSettingsTable(), $this->getContextKeyField(), $this->getContextTable(), $this->getContextKeyField()); } - // Clean orphaned submission_files entries by uploader_user_id - $orphanedIds = DB::table('submission_files AS sf')->leftJoin('users AS u', 'sf.uploader_user_id', '=', 'u.user_id')->whereNull('u.user_id')->whereNotNull('sf.uploader_user_id')->distinct()->pluck('sf.uploader_user_id'); - foreach ($orphanedIds as $uploaderUserId) { - $this->_installer->log("Nulling non-existent uploader_user_id {$uploaderUserId} in submission_files."); - DB::table('submission_files')->where('uploader_user_id', '=', $uploaderUserId)->update(['uploader_user_id' => null]); + + library_file_settings: { + // Depends directly on ~1 entities: file_id->library_files.file_id + // Dependent entities: ~0 + $this->deleteRequiredReference('library_file_settings', 'file_id', 'library_files', 'file_id'); } - // Clean orphaned submission_files entries by source_submission_file_id - $orphanedIds = DB::table('submission_files AS sf')->leftJoin('submission_files AS sfs', 'sf.source_submission_file_id', '=', 'sfs.submission_file_id')->whereNull('sfs.submission_file_id')->whereNotNull('sf.source_submission_file_id')->distinct()->pluck('sf.source_submission_file_id'); - foreach ($orphanedIds as $sourceSubmissionFileId) { - $this->_installer->log("Nulling non-existent source_submission_file_id {$sourceSubmissionFileId} in submission_files."); - DB::table('submission_files')->where('source_submission_file_id', '=', $sourceSubmissionFileId)->update(['source_submission_file_id' => null]); + + navigation_menu_item_assignment_settings: { + // Depends directly on ~1 entities: navigation_menu_item_assignment_id->navigation_menu_item_assignments.navigation_menu_item_assignment_id + // Dependent entities: ~0 + $this->deleteRequiredReference('navigation_menu_item_assignment_settings', 'navigation_menu_item_assignment_id', 'navigation_menu_item_assignments', 'navigation_menu_item_assignment_id'); } - // Clean orphaned data_object_tombstone_settings entries - $orphanedIds = DB::table('data_object_tombstone_settings AS dots')->leftJoin('data_object_tombstones AS dot', 'dot.tombstone_id', '=', 'dots.tombstone_id')->whereNull('dot.tombstone_id')->distinct()->pluck('dots.tombstone_id'); - foreach ($orphanedIds as $tombstoneId) { - DB::table('data_object_tombstone_settings')->where('tombstone_id', '=', $tombstoneId)->delete(); + + navigation_menu_item_settings: { + // Depends directly on ~1 entities: navigation_menu_item_id->navigation_menu_items.navigation_menu_item_id + // Dependent entities: ~0 + $this->deleteRequiredReference('navigation_menu_item_settings', 'navigation_menu_item_id', 'navigation_menu_items', 'navigation_menu_item_id'); } - // Clean orphaned data_object_tombstone_oai_set_objects entries by tombstone_id - $orphanedIds = DB::table('data_object_tombstone_oai_set_objects AS dotoso')->leftJoin('data_object_tombstones AS dot', 'dot.tombstone_id', '=', 'dotoso.tombstone_id')->whereNull('dot.tombstone_id')->distinct()->pluck('dotoso.tombstone_id'); - foreach ($orphanedIds as $tombstoneId) { - DB::table('data_object_tombstone_oai_set_objects')->where('tombstone_id', '=', $tombstoneId)->delete(); + + notes: { + // Depends directly on ~1 entities: user_id->users.user_id + // Dependent entities: ~0 + $this->deleteRequiredReference('notes', 'user_id', 'users', 'user_id'); + } + + notification_settings: { + // Depends directly on ~1 entities: notification_id->notifications.notification_id + // Dependent entities: ~0 + $this->deleteRequiredReference('notification_settings', 'notification_id', 'notifications', 'notification_id'); + } + + notification_subscription_settings: { + // Depends directly on ~2 entities: context->journals.journal_id user_id->users.user_id + // Dependent entities: ~0 + $this->deleteRequiredReference('notification_subscription_settings', 'user_id', 'users', 'user_id'); + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('notification_subscription_settings', 'context', $this->getContextTable(), $this->getContextKeyField()); + } + + plugin_settings: { + // Depends directly on ~1 entities: context_id->journals.journal_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteOptionalReference('plugin_settings', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + } + + publication_categories: { + // Depends directly on ~2 entities: category_id->categories.category_id publication_id->publications.publication_id + // Dependent entities: ~0 + $this->deleteRequiredReference('publication_categories', 'publication_id', 'publications', 'publication_id'); + $this->deleteRequiredReference('publication_categories', 'category_id', 'categories', 'category_id'); + } + + publication_galley_settings: { + // Depends directly on ~1 entities: galley_id->publication_galleys.galley_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('publication_galley_settings', 'galley_id', 'publication_galleys', 'galley_id'); + } + + publication_settings: { + // Depends directly on ~1 entities: publication_id->publications.publication_id + // Dependent entities: ~0 + $this->deleteRequiredReference('publication_settings', 'publication_id', 'publications', 'publication_id'); + } + + query_participants: { + // Depends directly on ~2 entities: query_id->queries.query_id user_id->users.user_id + // Dependent entities: ~0 + $this->deleteRequiredReference('query_participants', 'user_id', 'users', 'user_id'); + $this->deleteRequiredReference('query_participants', 'query_id', 'queries', 'query_id'); + } + + review_files: { + // Depends directly on ~2 entities: review_id->review_assignments.review_id submission_file_id->submission_files.submission_file_id + // Dependent entities: ~0 + $this->deleteRequiredReference('review_files', 'submission_file_id', 'submission_files', 'submission_file_id'); + $this->deleteRequiredReference('review_files', 'review_id', 'review_assignments', 'review_id'); + } + + review_form_element_settings: { + // Depends directly on ~1 entities: review_form_element_id->review_form_elements.review_form_element_id + // Dependent entities: ~0 + if (Schema::hasTable('review_form_element_settings')) { + $this->deleteRequiredReference('review_form_element_settings', 'review_form_element_id', 'review_form_elements', 'review_form_element_id'); + } + } + + review_form_responses: { + // Depends directly on ~2 entities: review_form_element_id->review_form_elements.review_form_element_id review_id->review_assignments.review_id + // Dependent entities: ~0 + if (Schema::hasTable('review_form_responses')) { + $this->deleteRequiredReference('review_form_responses', 'review_id', 'review_assignments', 'review_id'); + $this->deleteRequiredReference('review_form_responses', 'review_form_element_id', 'review_form_elements', 'review_form_element_id'); + } + } + + review_form_settings: { + // Depends directly on ~1 entities: review_form_id->review_forms.review_form_id + // Dependent entities: ~0 + if (Schema::hasTable('review_form_settings')) { + $this->deleteRequiredReference('review_form_settings', 'review_form_id', 'review_forms', 'review_form_id'); + } + } + + review_round_files: { + // Depends directly on ~3 entities: review_round_id->review_rounds.review_round_id submission_file_id->submission_files.submission_file_id submission_id->submissions.submission_id + // Dependent entities: ~0 + $this->deleteRequiredReference('review_round_files', 'submission_id', 'submissions', 'submission_id'); + $this->deleteRequiredReference('review_round_files', 'submission_file_id', 'submission_files', 'submission_file_id'); + $this->deleteRequiredReference('review_round_files', 'review_round_id', 'review_rounds', 'review_round_id'); + } + + section_settings: { + // Depends directly on ~1 entities: section_id->sections.section_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('section_settings', 'section_id', 'sections', 'section_id'); + } + + sessions: { + // Depends directly on ~1 entities: user_id->users.user_id + // Dependent entities: ~0 + $this->deleteOptionalReference('sessions', 'user_id', 'users', 'user_id'); + } + + stage_assignments: { + // Depends directly on ~3 entities: submission_id->submissions.submission_id user_group_id->user_groups.user_group_id user_id->users.user_id + // Dependent entities: ~0 + $this->deleteRequiredReference('stage_assignments', 'user_id', 'users', 'user_id'); + $this->deleteRequiredReference('stage_assignments', 'user_group_id', 'user_groups', 'user_group_id'); + $this->deleteRequiredReference('stage_assignments', 'submission_id', 'submissions', 'submission_id'); + } + + static_page_settings: { + // Depends directly on ~1 entities: static_page_id->static_pages.static_page_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('static_page_settings', 'static_page_id', 'static_pages', 'static_page_id'); + } + + subeditor_submission_group: { + // Depends directly on ~3 entities: context_id->journals.journal_id user_group_id->user_groups.user_group_id user_id->users.user_id + // Dependent entities: ~0 + $this->deleteRequiredReference('subeditor_submission_group', 'user_id', 'users', 'user_id'); + // Deprecated/moved field (not found on previous software version) + // $this->deleteRequiredReference('subeditor_submission_group', 'user_group_id', 'user_groups', 'user_group_id'); + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('subeditor_submission_group', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + } + + submission_comments: { + // Depends directly on ~2 entities: author_id->users.user_id submission_id->submissions.submission_id + // Dependent entities: ~0 + $this->deleteRequiredReference('submission_comments', 'submission_id', 'submissions', 'submission_id'); + $this->deleteRequiredReference('submission_comments', 'author_id', 'users', 'user_id'); + } + + submission_file_revisions: { + // Depends directly on ~2 entities: file_id->files.file_id submission_file_id->submission_files.submission_file_id + // Dependent entities: ~0 + $this->deleteRequiredReference('submission_file_revisions', 'submission_file_id', 'submission_files', 'submission_file_id'); + $this->deleteRequiredReference('submission_file_revisions', 'file_id', 'files', 'file_id'); + } + + submission_file_settings: { + // Depends directly on ~1 entities: submission_file_id->submission_files.submission_file_id + // Dependent entities: ~0 + $this->deleteRequiredReference('submission_file_settings', 'submission_file_id', 'submission_files', 'submission_file_id'); + } + + submission_search_object_keywords: { + // Depends directly on ~2 entities: keyword_id->submission_search_keyword_list.keyword_id object_id->submission_search_objects.object_id + // Dependent entities: ~0 + $this->deleteRequiredReference('submission_search_object_keywords', 'object_id', 'submission_search_objects', 'object_id'); + $this->deleteRequiredReference('submission_search_object_keywords', 'keyword_id', 'submission_search_keyword_list', 'keyword_id'); + } + + submission_settings: { + // Depends directly on ~1 entities: submission_id->submissions.submission_id + // Dependent entities: ~0 + $this->deleteRequiredReference('submission_settings', 'submission_id', 'submissions', 'submission_id'); + } + + subscription_type_settings: { + // Depends directly on ~1 entities: type_id->subscription_types.type_id + // Dependent entities: ~0 + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('subscription_type_settings', 'type_id', 'subscription_types', 'type_id'); + } + + temporary_files: { + // Depends directly on ~1 entities: user_id->users.user_id + // Dependent entities: ~0 + $this->deleteRequiredReference('temporary_files', 'user_id', 'users', 'user_id'); + } + + user_group_settings: { + // Depends directly on ~1 entities: user_group_id->user_groups.user_group_id + // Dependent entities: ~0 + $this->deleteRequiredReference('user_group_settings', 'user_group_id', 'user_groups', 'user_group_id'); + } + + user_group_stage: { + // Depends directly on ~2 entities: context_id->journals.journal_id user_group_id->user_groups.user_group_id + // Dependent entities: ~0 + $this->deleteRequiredReference('user_group_stage', 'user_group_id', 'user_groups', 'user_group_id'); + // Custom field (not found in at least one of the softwares) + $this->deleteRequiredReference('user_group_stage', 'context_id', $this->getContextTable(), $this->getContextKeyField()); + } + + user_interests: { + // Depends directly on ~2 entities: controlled_vocab_entry_id->controlled_vocab_entries.controlled_vocab_entry_id user_id->users.user_id + // Dependent entities: ~0 + $this->deleteRequiredReference('user_interests', 'user_id', 'users', 'user_id'); + $this->deleteRequiredReference('user_interests', 'controlled_vocab_entry_id', 'controlled_vocab_entries', 'controlled_vocab_entry_id'); + } + + user_settings: { + // Depends directly on ~1 entities: user_id->users.user_id + // Dependent entities: ~0 + $this->deleteRequiredReference('user_settings', 'user_id', 'users', 'user_id'); + } + + user_user_groups: { + // Depends directly on ~2 entities: user_group_id->user_groups.user_group_id user_id->users.user_id + // Dependent entities: ~0 + $this->deleteRequiredReference('user_user_groups', 'user_id', 'users', 'user_id'); + $this->deleteRequiredReference('user_user_groups', 'user_group_id', 'user_groups', 'user_group_id'); + } + } + + protected function processOrphanedTable(string $sourceTable): void + { + static $notified = []; + if (!isset($notified[$sourceTable])) { + $notified[$sourceTable] = true; + $this->onBeforeOrphanedCleanup($sourceTable); + } + } + + protected function onBeforeOrphanedCleanup(string $sourceTable): void + { + } + + /** + * Delete rows from the source table where the foreign key field contains either invalid values or NULL + * Used for NOT NULL/required relationships + */ + protected function deleteRequiredReference(string $sourceTable, string $sourceColumn, string $referenceTable, string $referenceColumn, bool $log = true): static + { + $this->processOrphanedTable($sourceTable); + $ids = DB::table("{$sourceTable} AS s") + ->leftJoin("{$referenceTable} AS r", "s.{$sourceColumn}", '=', "r.{$referenceColumn}") + ->whereNull("r.{$referenceColumn}") + ->distinct() + ->pluck("s.{$sourceColumn}"); + if ($ids->count()) { + $this->_installer->log("Removing orphaned entries from \"{$sourceTable}\" with an invalid value for the required column \"{$sourceColumn}\"\. The following IDs do not exist at the reference table \"{$referenceTable}\":\n{$ids->join(', ')}"); + $removed = 0; + foreach ($ids->chunk(1000) as $chunkedIds) { + $removed += DB::table($sourceTable) + ->whereIn($sourceColumn, $chunkedIds) + ->orWhereNull($sourceColumn) + ->delete(); + } + $this->_installer->log("{$removed} entries removed"); + } + return $this; + } + + /** + * Resets optional/nullable foreign key fields from the source table to NULL when the field contains invalid values + * Used for NULLABLE relationships + */ + protected function cleanOptionalReference(string $sourceTable, string $sourceColumn, string $referenceTable, string $referenceColumn): static + { + $ids = DB::table("{$sourceTable} AS s") + ->leftJoin("{$referenceTable} AS r", "s.{$sourceColumn}", '=', "r.{$referenceColumn}") + ->whereNotNull("s.{$sourceColumn}") + ->whereNull("r.{$referenceColumn}") + ->distinct() + ->pluck("s.{$sourceColumn}"); + if ($ids->count()) { + $this->_installer->log("Cleaning orphaned entries from \"{$sourceTable}\" with an invalid value for the column \"{$sourceColumn}\"\. The following IDs do not exist at the reference table \"{$referenceTable}\" and will be reset to NULL:\n{$ids->join(', ')}"); + $updated = 0; + foreach ($ids->chunk(1000) as $chunkedIds) { + $updated += DB::table($sourceTable) + ->whereIn($sourceColumn, $chunkedIds) + ->update([$sourceColumn => null]); + } + $this->_installer->log("{$updated} entries updated"); + } + return $this; + } + + /** + * Deletes rows from the source table where the foreign key field contains invalid values + * Used for NULLABLE relationships, where the source record lose the meaning without its relationship + */ + protected function deleteOptionalReference(string $sourceTable, string $sourceColumn, string $referenceTable, string $referenceColumn): static + { + $ids = DB::table("{$sourceTable} AS s") + ->leftJoin("{$referenceTable} AS r", "s.{$sourceColumn}", '=', "r.{$referenceColumn}") + ->whereNotNull("s.{$sourceColumn}") + ->whereNull("r.{$referenceColumn}") + ->distinct() + ->pluck("s.{$sourceColumn}"); + if ($ids->count()) { + $this->_installer->log("Removing orphaned entries from \"{$sourceTable}\" with an invalid value for the column \"{$sourceColumn}\"\. The following IDs do not exist at the reference table \"{$referenceTable}\":\n{$ids->join(', ')}"); + $removed = 0; + foreach ($ids->chunk(1000) as $chunkedIds) { + $removed += DB::table($sourceTable) + ->whereIn($sourceColumn, $chunkedIds) + ->delete(); + } + $this->_installer->log("{$removed} entries removed"); } + return $this; } }