From 1bc3bc56993bbc4ffef3784a20bc0fe5927d0ead Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 05:49:48 +0000 Subject: [PATCH 1/5] Add restoreDeletedCategory.php script to restore deleted categories and their relations Agent-Logs-Url: https://github.com/kaltura/server/sessions/07c7b26d-48dc-494f-b8d7-7c07b76b860c Co-authored-by: MosheMaorKaltura <10695851+MosheMaorKaltura@users.noreply.github.com> --- .../scripts/utils/restoreDeletedCategory.php | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 alpha/scripts/utils/restoreDeletedCategory.php diff --git a/alpha/scripts/utils/restoreDeletedCategory.php b/alpha/scripts/utils/restoreDeletedCategory.php new file mode 100644 index 0000000000..c621edd1b3 --- /dev/null +++ b/alpha/scripts/utils/restoreDeletedCategory.php @@ -0,0 +1,235 @@ + [realrun|dryrun] [timeDeltaSeconds] + * + * Arguments: + * categoryId - The ID of the category to restore + * realrun|dryrun - Optional. Default is 'dryrun'. Use 'realrun' to actually make changes + * timeDeltaSeconds - Optional. Time delta in seconds to match related deleted records. Default: 60 + * + * Example: + * php restoreDeletedCategory.php 12345 dryrun + * php restoreDeletedCategory.php 12345 realrun 120 + * + * @package Core + * @subpackage scripts + */ + +ini_set('memory_limit', '1024M'); +set_time_limit(0); + +if ($argc < 2) +{ + echo PHP_EOL . ' ---- Restore Deleted Category ---- ' . PHP_EOL; + echo ' Usage: php ' . $argv[0] . ' [realrun|dryrun] [timeDeltaSeconds]' . PHP_EOL; + echo PHP_EOL; + echo ' Arguments:' . PHP_EOL; + echo ' categoryId - The ID of the category to restore' . PHP_EOL; + echo ' realrun|dryrun - Optional. Default is "dryrun". Use "realrun" to actually make changes' . PHP_EOL; + echo ' timeDeltaSeconds - Optional. Time delta in seconds to match related deleted records. Default: 60' . PHP_EOL; + echo PHP_EOL; + echo ' Example:' . PHP_EOL; + echo ' php ' . $argv[0] . ' 12345 dryrun' . PHP_EOL; + echo ' php ' . $argv[0] . ' 12345 realrun 120' . PHP_EOL; + echo PHP_EOL; + die(' Error: Missing categoryId parameter' . PHP_EOL . PHP_EOL); +} + +$categoryId = $argv[1]; + +$dryRun = true; +if (isset($argv[2]) && $argv[2] === 'realrun') +{ + $dryRun = false; +} + +// Time delta in seconds - used to find related records that were deleted around the same time +$timeDeltaSeconds = 60; +if (isset($argv[3]) && is_numeric($argv[3])) +{ + $timeDeltaSeconds = (int)$argv[3]; +} + +require_once(__DIR__ . '/../bootstrap.php'); + +KalturaStatement::setDryRun($dryRun); +KalturaLog::info($dryRun ? 'DRY RUN MODE - No changes will be made' : 'REAL RUN MODE - Changes will be saved'); +KalturaLog::info("Category ID: $categoryId"); +KalturaLog::info("Time delta for matching related records: $timeDeltaSeconds seconds"); + +// Step 1: Retrieve the deleted category +KalturaLog::info("Step 1: Looking for deleted category with ID: $categoryId"); +categoryPeer::setUseCriteriaFilter(false); +$category = categoryPeer::retrieveByPK($categoryId); +categoryPeer::setUseCriteriaFilter(true); + +if (!$category) +{ + KalturaLog::err("Category with ID $categoryId not found"); + die("Error: Category with ID $categoryId not found" . PHP_EOL); +} + +if ($category->getStatus() != CategoryStatus::DELETED) +{ + KalturaLog::warning("Category with ID $categoryId is not in DELETED status (current status: " . $category->getStatus() . ")"); + die("Error: Category with ID $categoryId is not deleted (status: " . $category->getStatus() . ")" . PHP_EOL); +} + +$deletedAt = $category->getDeletedAt(null); // Get as timestamp +if (!$deletedAt) +{ + KalturaLog::err("Category with ID $categoryId has no deletion timestamp"); + die("Error: Category with ID $categoryId has no deletion timestamp" . PHP_EOL); +} + +$deletedAtFormatted = date('Y-m-d H:i:s', $deletedAt); +KalturaLog::info("Category '$categoryId' was deleted at: $deletedAtFormatted"); +KalturaLog::info("Category name: " . $category->getName()); +KalturaLog::info("Category full name: " . $category->getFullName()); +KalturaLog::info("Partner ID: " . $category->getPartnerId()); + +// Calculate time range for finding related deleted records +$deletionTimeStart = $deletedAt - $timeDeltaSeconds; +$deletionTimeEnd = $deletedAt + $timeDeltaSeconds; +$deletionTimeStartFormatted = date('Y-m-d H:i:s', $deletionTimeStart); +$deletionTimeEndFormatted = date('Y-m-d H:i:s', $deletionTimeEnd); +KalturaLog::info("Looking for related records deleted between $deletionTimeStartFormatted and $deletionTimeEndFormatted"); + +// Step 2: Find all categoryEntry records that were deleted around the same time +KalturaLog::info("Step 2: Looking for deleted categoryEntry records for category ID: $categoryId"); +categoryEntryPeer::setUseCriteriaFilter(false); + +$categoryEntryCriteria = new Criteria(); +$categoryEntryCriteria->add(categoryEntryPeer::CATEGORY_ID, $categoryId); +$categoryEntryCriteria->add(categoryEntryPeer::STATUS, CategoryEntryStatus::DELETED); +$categoryEntryCriteria->add(categoryEntryPeer::UPDATED_AT, $deletionTimeStart, Criteria::GREATER_EQUAL); +$categoryEntryCriteria->add(categoryEntryPeer::UPDATED_AT, $deletionTimeEnd, Criteria::LESS_EQUAL); + +$deletedCategoryEntries = categoryEntryPeer::doSelect($categoryEntryCriteria); +categoryEntryPeer::setUseCriteriaFilter(true); + +$categoryEntriesCount = count($deletedCategoryEntries); +KalturaLog::info("Found $categoryEntriesCount deleted categoryEntry records to restore"); + +// Step 3: Find all categoryKuser records that were deleted around the same time +KalturaLog::info("Step 3: Looking for deleted categoryKuser records for category ID: $categoryId"); +categoryKuserPeer::setUseCriteriaFilter(false); + +$categoryKuserCriteria = new Criteria(); +$categoryKuserCriteria->add(categoryKuserPeer::CATEGORY_ID, $categoryId); +$categoryKuserCriteria->add(categoryKuserPeer::STATUS, CategoryKuserStatus::DELETED); +$categoryKuserCriteria->add(categoryKuserPeer::UPDATED_AT, $deletionTimeStart, Criteria::GREATER_EQUAL); +$categoryKuserCriteria->add(categoryKuserPeer::UPDATED_AT, $deletionTimeEnd, Criteria::LESS_EQUAL); + +$deletedCategoryKusers = categoryKuserPeer::doSelect($categoryKuserCriteria); +categoryKuserPeer::setUseCriteriaFilter(true); + +$categoryKusersCount = count($deletedCategoryKusers); +KalturaLog::info("Found $categoryKusersCount deleted categoryKuser records to restore"); + +// Summary before restore +KalturaLog::info("=== Summary of records to restore ==="); +KalturaLog::info("Category: $categoryId (" . $category->getName() . ")"); +KalturaLog::info("Category Entries: $categoryEntriesCount"); +KalturaLog::info("Category Users: $categoryKusersCount"); + +if ($dryRun) +{ + KalturaLog::info("=== DRY RUN - No changes will be made ==="); + + // List category entries that would be restored + if ($categoryEntriesCount > 0) + { + KalturaLog::info("CategoryEntry records that would be restored:"); + foreach ($deletedCategoryEntries as $categoryEntry) + { + KalturaLog::info(" - CategoryEntry ID: " . $categoryEntry->getId() . + ", Entry ID: " . $categoryEntry->getEntryId() . + ", Updated at: " . $categoryEntry->getUpdatedAt()); + } + } + + // List category users that would be restored + if ($categoryKusersCount > 0) + { + KalturaLog::info("CategoryKuser records that would be restored:"); + foreach ($deletedCategoryKusers as $categoryKuser) + { + KalturaLog::info(" - CategoryKuser ID: " . $categoryKuser->getId() . + ", Kuser ID: " . $categoryKuser->getKuserId() . + ", Puser ID: " . $categoryKuser->getPuserId() . + ", Updated at: " . $categoryKuser->getUpdatedAt()); + } + } +} +else +{ + KalturaLog::info("=== Starting restore process ==="); + + // Step 4: Restore the category + KalturaLog::info("Step 4: Restoring category ID: $categoryId"); + $category->setStatus(CategoryStatus::ACTIVE); + $category->setDeletedAt(null); + $category->save(); + KalturaLog::info("Category $categoryId restored to ACTIVE status"); + categoryPeer::clearInstancePool(); + + // Step 5: Restore categoryEntry records + KalturaLog::info("Step 5: Restoring categoryEntry records"); + $restoredCategoryEntries = 0; + foreach ($deletedCategoryEntries as $categoryEntry) + { + KalturaLog::info("Restoring categoryEntry ID: " . $categoryEntry->getId() . + " (Entry ID: " . $categoryEntry->getEntryId() . ")"); + $categoryEntry->setStatus(CategoryEntryStatus::ACTIVE); + $categoryEntry->save(); + $restoredCategoryEntries++; + + // Clear instance pool periodically to manage memory + if ($restoredCategoryEntries % 100 == 0) + { + categoryEntryPeer::clearInstancePool(); + KalturaLog::info("Restored $restoredCategoryEntries categoryEntry records so far..."); + } + } + categoryEntryPeer::clearInstancePool(); + KalturaLog::info("Restored $restoredCategoryEntries categoryEntry records"); + + // Step 6: Restore categoryKuser records + KalturaLog::info("Step 6: Restoring categoryKuser records"); + $restoredCategoryKusers = 0; + foreach ($deletedCategoryKusers as $categoryKuser) + { + KalturaLog::info("Restoring categoryKuser ID: " . $categoryKuser->getId() . + " (Kuser ID: " . $categoryKuser->getKuserId() . ", Puser ID: " . $categoryKuser->getPuserId() . ")"); + $categoryKuser->setStatus(CategoryKuserStatus::ACTIVE); + $categoryKuser->save(); + $restoredCategoryKusers++; + + // Clear instance pool periodically to manage memory + if ($restoredCategoryKusers % 100 == 0) + { + categoryKuserPeer::clearInstancePool(); + KalturaLog::info("Restored $restoredCategoryKusers categoryKuser records so far..."); + } + } + categoryKuserPeer::clearInstancePool(); + KalturaLog::info("Restored $restoredCategoryKusers categoryKuser records"); + + // Flush any pending events + kEventsManager::flushEvents(); + + KalturaLog::info("=== Restore completed ==="); + KalturaLog::info("Total category entries restored: $restoredCategoryEntries"); + KalturaLog::info("Total category users restored: $restoredCategoryKusers"); +} + +KalturaLog::info("Script finished successfully"); From 31fe1f68695944fc882b34a089048254f5850795 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 05:50:43 +0000 Subject: [PATCH 2/5] Improve code quality: add comment and format timestamps consistently Agent-Logs-Url: https://github.com/kaltura/server/sessions/07c7b26d-48dc-494f-b8d7-7c07b76b860c Co-authored-by: MosheMaorKaltura <10695851+MosheMaorKaltura@users.noreply.github.com> --- alpha/scripts/utils/restoreDeletedCategory.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/alpha/scripts/utils/restoreDeletedCategory.php b/alpha/scripts/utils/restoreDeletedCategory.php index c621edd1b3..511d9994a0 100644 --- a/alpha/scripts/utils/restoreDeletedCategory.php +++ b/alpha/scripts/utils/restoreDeletedCategory.php @@ -83,7 +83,8 @@ die("Error: Category with ID $categoryId is not deleted (status: " . $category->getStatus() . ")" . PHP_EOL); } -$deletedAt = $category->getDeletedAt(null); // Get as timestamp +// getDeletedAt(null) returns UNIX timestamp when format is null, otherwise formatted string +$deletedAt = $category->getDeletedAt(null); if (!$deletedAt) { KalturaLog::err("Category with ID $categoryId has no deletion timestamp"); @@ -151,9 +152,10 @@ KalturaLog::info("CategoryEntry records that would be restored:"); foreach ($deletedCategoryEntries as $categoryEntry) { + $updatedAtFormatted = $categoryEntry->getUpdatedAt('Y-m-d H:i:s'); KalturaLog::info(" - CategoryEntry ID: " . $categoryEntry->getId() . ", Entry ID: " . $categoryEntry->getEntryId() . - ", Updated at: " . $categoryEntry->getUpdatedAt()); + ", Updated at: " . $updatedAtFormatted); } } @@ -163,10 +165,11 @@ KalturaLog::info("CategoryKuser records that would be restored:"); foreach ($deletedCategoryKusers as $categoryKuser) { + $updatedAtFormatted = $categoryKuser->getUpdatedAt('Y-m-d H:i:s'); KalturaLog::info(" - CategoryKuser ID: " . $categoryKuser->getId() . ", Kuser ID: " . $categoryKuser->getKuserId() . ", Puser ID: " . $categoryKuser->getPuserId() . - ", Updated at: " . $categoryKuser->getUpdatedAt()); + ", Updated at: " . $updatedAtFormatted); } } } From 4d97bea91b00506e8d760a9d7b597758d8683933 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 05:51:18 +0000 Subject: [PATCH 3/5] Use strict comparison operators for type safety Agent-Logs-Url: https://github.com/kaltura/server/sessions/07c7b26d-48dc-494f-b8d7-7c07b76b860c Co-authored-by: MosheMaorKaltura <10695851+MosheMaorKaltura@users.noreply.github.com> --- alpha/scripts/utils/restoreDeletedCategory.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/alpha/scripts/utils/restoreDeletedCategory.php b/alpha/scripts/utils/restoreDeletedCategory.php index 511d9994a0..27c84bd95f 100644 --- a/alpha/scripts/utils/restoreDeletedCategory.php +++ b/alpha/scripts/utils/restoreDeletedCategory.php @@ -77,7 +77,7 @@ die("Error: Category with ID $categoryId not found" . PHP_EOL); } -if ($category->getStatus() != CategoryStatus::DELETED) +if ($category->getStatus() !== CategoryStatus::DELETED) { KalturaLog::warning("Category with ID $categoryId is not in DELETED status (current status: " . $category->getStatus() . ")"); die("Error: Category with ID $categoryId is not deleted (status: " . $category->getStatus() . ")" . PHP_EOL); @@ -197,7 +197,7 @@ $restoredCategoryEntries++; // Clear instance pool periodically to manage memory - if ($restoredCategoryEntries % 100 == 0) + if ($restoredCategoryEntries % 100 === 0) { categoryEntryPeer::clearInstancePool(); KalturaLog::info("Restored $restoredCategoryEntries categoryEntry records so far..."); @@ -218,7 +218,7 @@ $restoredCategoryKusers++; // Clear instance pool periodically to manage memory - if ($restoredCategoryKusers % 100 == 0) + if ($restoredCategoryKusers % 100 === 0) { categoryKuserPeer::clearInstancePool(); KalturaLog::info("Restored $restoredCategoryKusers categoryKuser records so far..."); From fd061a59806fd735c56804326d598572cc574c32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 06:29:25 +0000 Subject: [PATCH 4/5] Enhance restoreDeletedCategory.php with comprehensive restore logic - Restore child categories recursively (ordered by depth) - Restore categoryKuser only for MANUAL inheritance categories - Rebuild hierarchy fields (reSetFullIds, reSetDepth, reSetFullName) - Recalculate all counts (entries, directEntries, members, subCategories) - Update parent category's sub-categories count - Trigger re-indexing for search Agent-Logs-Url: https://github.com/kaltura/server/sessions/65e08e9e-09d8-48f6-8b43-4cda3ceae986 Co-authored-by: MosheMaorKaltura <10695851+MosheMaorKaltura@users.noreply.github.com> --- .../scripts/utils/restoreDeletedCategory.php | 262 +++++++++++++++--- 1 file changed, 226 insertions(+), 36 deletions(-) diff --git a/alpha/scripts/utils/restoreDeletedCategory.php b/alpha/scripts/utils/restoreDeletedCategory.php index 27c84bd95f..0e76d199b0 100644 --- a/alpha/scripts/utils/restoreDeletedCategory.php +++ b/alpha/scripts/utils/restoreDeletedCategory.php @@ -3,9 +3,13 @@ * Script to restore a deleted category and all its related data. * * This script restores: - * 1. The deleted category itself - * 2. All categoryEntry records that were deleted at the same time (linking entries to the category) - * 3. All categoryKuser records that were deleted at the same time (category user memberships) + * 1. The deleted category itself (status from DELETED to ACTIVE) + * 2. Child categories recursively + * 3. CategoryEntry records that were deleted at the same time (if entries weren't moved to parent) + * 4. CategoryKuser records for MANUAL inheritance categories + * 5. Recalculates all counts (entries, members, sub-categories) + * 6. Rebuilds hierarchy fields (fullIds, depth, fullName) + * 7. Triggers re-indexing for search * * Usage: * php restoreDeletedCategory.php [realrun|dryrun] [timeDeltaSeconds] @@ -96,6 +100,7 @@ KalturaLog::info("Category name: " . $category->getName()); KalturaLog::info("Category full name: " . $category->getFullName()); KalturaLog::info("Partner ID: " . $category->getPartnerId()); +KalturaLog::info("Inheritance Type: " . ($category->getInheritanceType() === InheritanceType::MANUAL ? 'MANUAL' : 'INHERIT')); // Calculate time range for finding related deleted records $deletionTimeStart = $deletedAt - $timeDeltaSeconds; @@ -104,12 +109,37 @@ $deletionTimeEndFormatted = date('Y-m-d H:i:s', $deletionTimeEnd); KalturaLog::info("Looking for related records deleted between $deletionTimeStartFormatted and $deletionTimeEndFormatted"); -// Step 2: Find all categoryEntry records that were deleted around the same time -KalturaLog::info("Step 2: Looking for deleted categoryEntry records for category ID: $categoryId"); +// Step 2: Find all deleted child categories (using fullIds prefix match) +KalturaLog::info("Step 2: Looking for deleted child categories"); +$fullIds = $category->getFullIds(); +categoryPeer::setUseCriteriaFilter(false); + +$childCategoriesCriteria = new Criteria(); +$childCategoriesCriteria->add(categoryPeer::FULL_IDS, $fullIds . categoryPeer::CATEGORY_SEPARATOR . '%', Criteria::LIKE); +$childCategoriesCriteria->add(categoryPeer::STATUS, CategoryStatus::DELETED); +$childCategoriesCriteria->add(categoryPeer::DELETED_AT, $deletionTimeStart, Criteria::GREATER_EQUAL); +$childCategoriesCriteria->add(categoryPeer::DELETED_AT, $deletionTimeEnd, Criteria::LESS_EQUAL); +$childCategoriesCriteria->addAscendingOrderByColumn(categoryPeer::DEPTH); // Restore parents before children + +$deletedChildCategories = categoryPeer::doSelect($childCategoriesCriteria); +categoryPeer::setUseCriteriaFilter(true); + +$childCategoriesCount = count($deletedChildCategories); +KalturaLog::info("Found $childCategoriesCount deleted child categories to restore"); + +// Build list of all category IDs to restore (parent + children) +$allCategoryIds = array($categoryId); +foreach ($deletedChildCategories as $childCategory) +{ + $allCategoryIds[] = $childCategory->getId(); +} + +// Step 3: Find all categoryEntry records for all categories to restore +KalturaLog::info("Step 3: Looking for deleted categoryEntry records for all categories"); categoryEntryPeer::setUseCriteriaFilter(false); $categoryEntryCriteria = new Criteria(); -$categoryEntryCriteria->add(categoryEntryPeer::CATEGORY_ID, $categoryId); +$categoryEntryCriteria->add(categoryEntryPeer::CATEGORY_ID, $allCategoryIds, Criteria::IN); $categoryEntryCriteria->add(categoryEntryPeer::STATUS, CategoryEntryStatus::DELETED); $categoryEntryCriteria->add(categoryEntryPeer::UPDATED_AT, $deletionTimeStart, Criteria::GREATER_EQUAL); $categoryEntryCriteria->add(categoryEntryPeer::UPDATED_AT, $deletionTimeEnd, Criteria::LESS_EQUAL); @@ -120,42 +150,90 @@ $categoryEntriesCount = count($deletedCategoryEntries); KalturaLog::info("Found $categoryEntriesCount deleted categoryEntry records to restore"); -// Step 3: Find all categoryKuser records that were deleted around the same time -KalturaLog::info("Step 3: Looking for deleted categoryKuser records for category ID: $categoryId"); -categoryKuserPeer::setUseCriteriaFilter(false); +// Step 4: Find all categoryKuser records for MANUAL inheritance categories only +KalturaLog::info("Step 4: Looking for deleted categoryKuser records (MANUAL inheritance only)"); + +// Filter category IDs to only include MANUAL inheritance types +$manualInheritanceCategoryIds = array(); +if ($category->getInheritanceType() === InheritanceType::MANUAL) +{ + $manualInheritanceCategoryIds[] = $categoryId; +} +foreach ($deletedChildCategories as $childCategory) +{ + if ($childCategory->getInheritanceType() === InheritanceType::MANUAL) + { + $manualInheritanceCategoryIds[] = $childCategory->getId(); + } +} -$categoryKuserCriteria = new Criteria(); -$categoryKuserCriteria->add(categoryKuserPeer::CATEGORY_ID, $categoryId); -$categoryKuserCriteria->add(categoryKuserPeer::STATUS, CategoryKuserStatus::DELETED); -$categoryKuserCriteria->add(categoryKuserPeer::UPDATED_AT, $deletionTimeStart, Criteria::GREATER_EQUAL); -$categoryKuserCriteria->add(categoryKuserPeer::UPDATED_AT, $deletionTimeEnd, Criteria::LESS_EQUAL); +$deletedCategoryKusers = array(); +$categoryKusersCount = 0; -$deletedCategoryKusers = categoryKuserPeer::doSelect($categoryKuserCriteria); -categoryKuserPeer::setUseCriteriaFilter(true); +if (!empty($manualInheritanceCategoryIds)) +{ + categoryKuserPeer::setUseCriteriaFilter(false); + + $categoryKuserCriteria = new Criteria(); + $categoryKuserCriteria->add(categoryKuserPeer::CATEGORY_ID, $manualInheritanceCategoryIds, Criteria::IN); + $categoryKuserCriteria->add(categoryKuserPeer::STATUS, CategoryKuserStatus::DELETED); + $categoryKuserCriteria->add(categoryKuserPeer::UPDATED_AT, $deletionTimeStart, Criteria::GREATER_EQUAL); + $categoryKuserCriteria->add(categoryKuserPeer::UPDATED_AT, $deletionTimeEnd, Criteria::LESS_EQUAL); + + $deletedCategoryKusers = categoryKuserPeer::doSelect($categoryKuserCriteria); + categoryKuserPeer::setUseCriteriaFilter(true); + + $categoryKusersCount = count($deletedCategoryKusers); +} -$categoryKusersCount = count($deletedCategoryKusers); -KalturaLog::info("Found $categoryKusersCount deleted categoryKuser records to restore"); +KalturaLog::info("Found $categoryKusersCount deleted categoryKuser records to restore (from " . count($manualInheritanceCategoryIds) . " MANUAL inheritance categories)"); // Summary before restore KalturaLog::info("=== Summary of records to restore ==="); -KalturaLog::info("Category: $categoryId (" . $category->getName() . ")"); +KalturaLog::info("Main Category: $categoryId (" . $category->getName() . ")"); +KalturaLog::info("Child Categories: $childCategoriesCount"); KalturaLog::info("Category Entries: $categoryEntriesCount"); -KalturaLog::info("Category Users: $categoryKusersCount"); +KalturaLog::info("Category Users (MANUAL inheritance only): $categoryKusersCount"); if ($dryRun) { KalturaLog::info("=== DRY RUN - No changes will be made ==="); + // List child categories that would be restored + if ($childCategoriesCount > 0) + { + KalturaLog::info("Child categories that would be restored:"); + foreach ($deletedChildCategories as $childCategory) + { + $deletedAtFormatted = $childCategory->getDeletedAt('Y-m-d H:i:s'); + $inheritanceType = $childCategory->getInheritanceType() === InheritanceType::MANUAL ? 'MANUAL' : 'INHERIT'; + KalturaLog::info(" - Category ID: " . $childCategory->getId() . + ", Name: " . $childCategory->getName() . + ", Depth: " . $childCategory->getDepth() . + ", Inheritance: " . $inheritanceType . + ", Deleted at: " . $deletedAtFormatted); + } + } + // List category entries that would be restored if ($categoryEntriesCount > 0) { KalturaLog::info("CategoryEntry records that would be restored:"); + $displayLimit = min($categoryEntriesCount, 50); + $displayCount = 0; foreach ($deletedCategoryEntries as $categoryEntry) { + if ($displayCount >= $displayLimit) + { + KalturaLog::info(" ... and " . ($categoryEntriesCount - $displayLimit) . " more"); + break; + } $updatedAtFormatted = $categoryEntry->getUpdatedAt('Y-m-d H:i:s'); KalturaLog::info(" - CategoryEntry ID: " . $categoryEntry->getId() . + ", Category ID: " . $categoryEntry->getCategoryId() . ", Entry ID: " . $categoryEntry->getEntryId() . ", Updated at: " . $updatedAtFormatted); + $displayCount++; } } @@ -163,13 +241,22 @@ if ($categoryKusersCount > 0) { KalturaLog::info("CategoryKuser records that would be restored:"); + $displayLimit = min($categoryKusersCount, 50); + $displayCount = 0; foreach ($deletedCategoryKusers as $categoryKuser) { + if ($displayCount >= $displayLimit) + { + KalturaLog::info(" ... and " . ($categoryKusersCount - $displayLimit) . " more"); + break; + } $updatedAtFormatted = $categoryKuser->getUpdatedAt('Y-m-d H:i:s'); KalturaLog::info(" - CategoryKuser ID: " . $categoryKuser->getId() . + ", Category ID: " . $categoryKuser->getCategoryId() . ", Kuser ID: " . $categoryKuser->getKuserId() . ", Puser ID: " . $categoryKuser->getPuserId() . ", Updated at: " . $updatedAtFormatted); + $displayCount++; } } } @@ -177,26 +264,42 @@ { KalturaLog::info("=== Starting restore process ==="); - // Step 4: Restore the category - KalturaLog::info("Step 4: Restoring category ID: $categoryId"); + // Step 5: Restore the main category + KalturaLog::info("Step 5: Restoring main category ID: $categoryId"); $category->setStatus(CategoryStatus::ACTIVE); $category->setDeletedAt(null); $category->save(); - KalturaLog::info("Category $categoryId restored to ACTIVE status"); - categoryPeer::clearInstancePool(); + KalturaLog::info("Main category $categoryId restored to ACTIVE status"); - // Step 5: Restore categoryEntry records - KalturaLog::info("Step 5: Restoring categoryEntry records"); + // Step 6: Restore child categories (ordered by depth - parents first) + KalturaLog::info("Step 6: Restoring child categories"); + $restoredChildCategories = 0; + foreach ($deletedChildCategories as $childCategory) + { + KalturaLog::info("Restoring child category ID: " . $childCategory->getId() . + " (Name: " . $childCategory->getName() . ", Depth: " . $childCategory->getDepth() . ")"); + $childCategory->setStatus(CategoryStatus::ACTIVE); + $childCategory->setDeletedAt(null); + $childCategory->save(); + $restoredChildCategories++; + + if ($restoredChildCategories % 50 === 0) + { + categoryPeer::clearInstancePool(); + KalturaLog::info("Restored $restoredChildCategories child categories so far..."); + } + } + KalturaLog::info("Restored $restoredChildCategories child categories"); + + // Step 7: Restore categoryEntry records + KalturaLog::info("Step 7: Restoring categoryEntry records"); $restoredCategoryEntries = 0; foreach ($deletedCategoryEntries as $categoryEntry) { - KalturaLog::info("Restoring categoryEntry ID: " . $categoryEntry->getId() . - " (Entry ID: " . $categoryEntry->getEntryId() . ")"); $categoryEntry->setStatus(CategoryEntryStatus::ACTIVE); $categoryEntry->save(); $restoredCategoryEntries++; - // Clear instance pool periodically to manage memory if ($restoredCategoryEntries % 100 === 0) { categoryEntryPeer::clearInstancePool(); @@ -206,18 +309,15 @@ categoryEntryPeer::clearInstancePool(); KalturaLog::info("Restored $restoredCategoryEntries categoryEntry records"); - // Step 6: Restore categoryKuser records - KalturaLog::info("Step 6: Restoring categoryKuser records"); + // Step 8: Restore categoryKuser records (MANUAL inheritance only) + KalturaLog::info("Step 8: Restoring categoryKuser records (MANUAL inheritance only)"); $restoredCategoryKusers = 0; foreach ($deletedCategoryKusers as $categoryKuser) { - KalturaLog::info("Restoring categoryKuser ID: " . $categoryKuser->getId() . - " (Kuser ID: " . $categoryKuser->getKuserId() . ", Puser ID: " . $categoryKuser->getPuserId() . ")"); $categoryKuser->setStatus(CategoryKuserStatus::ACTIVE); $categoryKuser->save(); $restoredCategoryKusers++; - // Clear instance pool periodically to manage memory if ($restoredCategoryKusers % 100 === 0) { categoryKuserPeer::clearInstancePool(); @@ -227,12 +327,102 @@ categoryKuserPeer::clearInstancePool(); KalturaLog::info("Restored $restoredCategoryKusers categoryKuser records"); + // Step 9: Rebuild hierarchy and recalculate counts for all restored categories + KalturaLog::info("Step 9: Rebuilding hierarchy fields and recalculating counts"); + + // Re-fetch the main category to get fresh data + categoryPeer::clearInstancePool(); + categoryPeer::setUseCriteriaFilter(false); + $category = categoryPeer::retrieveByPK($categoryId); + categoryPeer::setUseCriteriaFilter(true); + + // Collect all categories to process (main + children), ordered by depth + $allCategoriesToProcess = array($category); + foreach ($deletedChildCategories as $childCategory) + { + // Re-fetch to get fresh data + categoryPeer::setUseCriteriaFilter(false); + $freshChild = categoryPeer::retrieveByPK($childCategory->getId()); + categoryPeer::setUseCriteriaFilter(true); + if ($freshChild) + { + $allCategoriesToProcess[] = $freshChild; + } + } + + // Sort by depth to ensure parents are processed before children + usort($allCategoriesToProcess, function($a, $b) { + return $a->getDepth() - $b->getDepth(); + }); + + $processedCategories = 0; + foreach ($allCategoriesToProcess as $categoryToProcess) + { + $catId = $categoryToProcess->getId(); + KalturaLog::info("Processing category ID: $catId - Rebuilding hierarchy and counts"); + + // Rebuild hierarchy fields + $categoryToProcess->reSetFullIds(); + $categoryToProcess->reSetDepth(); + $categoryToProcess->reSetFullName(); + + // Recalculate counts + $categoryToProcess->reSetEntriesCount(); + $categoryToProcess->reSetDirectEntriesCount(); + $categoryToProcess->reSetDirectSubCategoriesCount(); + + // Recalculate member counts for MANUAL inheritance categories + if ($categoryToProcess->getInheritanceType() === InheritanceType::MANUAL) + { + $categoryToProcess->reSetMembersCount(); + $categoryToProcess->reSetPendingMembersCount(); + } + + $categoryToProcess->save(); + $processedCategories++; + + if ($processedCategories % 50 === 0) + { + categoryPeer::clearInstancePool(); + KalturaLog::info("Processed $processedCategories categories so far..."); + } + } + KalturaLog::info("Processed $processedCategories categories for hierarchy rebuild and count recalculation"); + + // Step 10: Update parent category's direct sub-categories count + KalturaLog::info("Step 10: Updating parent category's sub-categories count"); + $parentId = $category->getParentId(); + if ($parentId) + { + categoryPeer::setUseCriteriaFilter(false); + $parentCategory = categoryPeer::retrieveByPK($parentId); + categoryPeer::setUseCriteriaFilter(true); + + if ($parentCategory && $parentCategory->getStatus() === CategoryStatus::ACTIVE) + { + $parentCategory->reSetDirectSubCategoriesCount(); + $parentCategory->save(); + KalturaLog::info("Updated parent category $parentId direct sub-categories count"); + } + } + + // Step 11: Trigger re-indexing for search + KalturaLog::info("Step 11: Triggering re-indexing for search"); + foreach ($allCategoriesToProcess as $categoryToIndex) + { + $categoryToIndex->indexToSearchIndex(); + } + KalturaLog::info("Triggered re-indexing for " . count($allCategoriesToProcess) . " categories"); + // Flush any pending events kEventsManager::flushEvents(); KalturaLog::info("=== Restore completed ==="); - KalturaLog::info("Total category entries restored: $restoredCategoryEntries"); - KalturaLog::info("Total category users restored: $restoredCategoryKusers"); + KalturaLog::info("Main category restored: 1"); + KalturaLog::info("Child categories restored: $restoredChildCategories"); + KalturaLog::info("Category entries restored: $restoredCategoryEntries"); + KalturaLog::info("Category users restored: $restoredCategoryKusers"); + KalturaLog::info("Categories processed for hierarchy rebuild: $processedCategories"); } KalturaLog::info("Script finished successfully"); From 093f639b2c4b42403157fb5c145a3adb50a2a391 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Apr 2026 06:32:03 +0000 Subject: [PATCH 5/5] Address code review feedback: add constants, escape LIKE pattern, optimize batch queries Agent-Logs-Url: https://github.com/kaltura/server/sessions/65e08e9e-09d8-48f6-8b43-4cda3ceae986 Co-authored-by: MosheMaorKaltura <10695851+MosheMaorKaltura@users.noreply.github.com> --- .../scripts/utils/restoreDeletedCategory.php | 70 ++++++++++--------- 1 file changed, 37 insertions(+), 33 deletions(-) diff --git a/alpha/scripts/utils/restoreDeletedCategory.php b/alpha/scripts/utils/restoreDeletedCategory.php index 0e76d199b0..2b7f90137b 100644 --- a/alpha/scripts/utils/restoreDeletedCategory.php +++ b/alpha/scripts/utils/restoreDeletedCategory.php @@ -27,7 +27,13 @@ * @subpackage scripts */ -ini_set('memory_limit', '1024M'); +// Constants for batch processing and display +const DATE_FORMAT = 'Y-m-d H:i:s'; +const CATEGORY_BATCH_SIZE = 50; +const RELATION_BATCH_SIZE = 100; +const DISPLAY_LIMIT = 50; + +ini_set('memory_limit', '2048M'); set_time_limit(0); if ($argc < 2) @@ -95,7 +101,7 @@ die("Error: Category with ID $categoryId has no deletion timestamp" . PHP_EOL); } -$deletedAtFormatted = date('Y-m-d H:i:s', $deletedAt); +$deletedAtFormatted = date(DATE_FORMAT, $deletedAt); KalturaLog::info("Category '$categoryId' was deleted at: $deletedAtFormatted"); KalturaLog::info("Category name: " . $category->getName()); KalturaLog::info("Category full name: " . $category->getFullName()); @@ -105,17 +111,20 @@ // Calculate time range for finding related deleted records $deletionTimeStart = $deletedAt - $timeDeltaSeconds; $deletionTimeEnd = $deletedAt + $timeDeltaSeconds; -$deletionTimeStartFormatted = date('Y-m-d H:i:s', $deletionTimeStart); -$deletionTimeEndFormatted = date('Y-m-d H:i:s', $deletionTimeEnd); +$deletionTimeStartFormatted = date(DATE_FORMAT, $deletionTimeStart); +$deletionTimeEndFormatted = date(DATE_FORMAT, $deletionTimeEnd); KalturaLog::info("Looking for related records deleted between $deletionTimeStartFormatted and $deletionTimeEndFormatted"); // Step 2: Find all deleted child categories (using fullIds prefix match) KalturaLog::info("Step 2: Looking for deleted child categories"); $fullIds = $category->getFullIds(); +// Escape special SQL LIKE characters to prevent unintended matches +// Full IDs are numeric category IDs separated by '>' so shouldn't contain these, but escape for safety +$escapedFullIds = str_replace(array('%', '_'), array('\\%', '\\_'), $fullIds); categoryPeer::setUseCriteriaFilter(false); $childCategoriesCriteria = new Criteria(); -$childCategoriesCriteria->add(categoryPeer::FULL_IDS, $fullIds . categoryPeer::CATEGORY_SEPARATOR . '%', Criteria::LIKE); +$childCategoriesCriteria->add(categoryPeer::FULL_IDS, $escapedFullIds . categoryPeer::CATEGORY_SEPARATOR . '%', Criteria::LIKE); $childCategoriesCriteria->add(categoryPeer::STATUS, CategoryStatus::DELETED); $childCategoriesCriteria->add(categoryPeer::DELETED_AT, $deletionTimeStart, Criteria::GREATER_EQUAL); $childCategoriesCriteria->add(categoryPeer::DELETED_AT, $deletionTimeEnd, Criteria::LESS_EQUAL); @@ -205,7 +214,7 @@ KalturaLog::info("Child categories that would be restored:"); foreach ($deletedChildCategories as $childCategory) { - $deletedAtFormatted = $childCategory->getDeletedAt('Y-m-d H:i:s'); + $deletedAtFormatted = $childCategory->getDeletedAt(DATE_FORMAT); $inheritanceType = $childCategory->getInheritanceType() === InheritanceType::MANUAL ? 'MANUAL' : 'INHERIT'; KalturaLog::info(" - Category ID: " . $childCategory->getId() . ", Name: " . $childCategory->getName() . @@ -219,16 +228,15 @@ if ($categoryEntriesCount > 0) { KalturaLog::info("CategoryEntry records that would be restored:"); - $displayLimit = min($categoryEntriesCount, 50); $displayCount = 0; foreach ($deletedCategoryEntries as $categoryEntry) { - if ($displayCount >= $displayLimit) + if ($displayCount >= DISPLAY_LIMIT) { - KalturaLog::info(" ... and " . ($categoryEntriesCount - $displayLimit) . " more"); + KalturaLog::info(" ... and " . ($categoryEntriesCount - DISPLAY_LIMIT) . " more"); break; } - $updatedAtFormatted = $categoryEntry->getUpdatedAt('Y-m-d H:i:s'); + $updatedAtFormatted = $categoryEntry->getUpdatedAt(DATE_FORMAT); KalturaLog::info(" - CategoryEntry ID: " . $categoryEntry->getId() . ", Category ID: " . $categoryEntry->getCategoryId() . ", Entry ID: " . $categoryEntry->getEntryId() . @@ -241,16 +249,15 @@ if ($categoryKusersCount > 0) { KalturaLog::info("CategoryKuser records that would be restored:"); - $displayLimit = min($categoryKusersCount, 50); $displayCount = 0; foreach ($deletedCategoryKusers as $categoryKuser) { - if ($displayCount >= $displayLimit) + if ($displayCount >= DISPLAY_LIMIT) { - KalturaLog::info(" ... and " . ($categoryKusersCount - $displayLimit) . " more"); + KalturaLog::info(" ... and " . ($categoryKusersCount - DISPLAY_LIMIT) . " more"); break; } - $updatedAtFormatted = $categoryKuser->getUpdatedAt('Y-m-d H:i:s'); + $updatedAtFormatted = $categoryKuser->getUpdatedAt(DATE_FORMAT); KalturaLog::info(" - CategoryKuser ID: " . $categoryKuser->getId() . ", Category ID: " . $categoryKuser->getCategoryId() . ", Kuser ID: " . $categoryKuser->getKuserId() . @@ -283,7 +290,7 @@ $childCategory->save(); $restoredChildCategories++; - if ($restoredChildCategories % 50 === 0) + if ($restoredChildCategories % CATEGORY_BATCH_SIZE === 0) { categoryPeer::clearInstancePool(); KalturaLog::info("Restored $restoredChildCategories child categories so far..."); @@ -300,7 +307,7 @@ $categoryEntry->save(); $restoredCategoryEntries++; - if ($restoredCategoryEntries % 100 === 0) + if ($restoredCategoryEntries % RELATION_BATCH_SIZE === 0) { categoryEntryPeer::clearInstancePool(); KalturaLog::info("Restored $restoredCategoryEntries categoryEntry records so far..."); @@ -318,7 +325,7 @@ $categoryKuser->save(); $restoredCategoryKusers++; - if ($restoredCategoryKusers % 100 === 0) + if ($restoredCategoryKusers % RELATION_BATCH_SIZE === 0) { categoryKuserPeer::clearInstancePool(); KalturaLog::info("Restored $restoredCategoryKusers categoryKuser records so far..."); @@ -330,26 +337,18 @@ // Step 9: Rebuild hierarchy and recalculate counts for all restored categories KalturaLog::info("Step 9: Rebuilding hierarchy fields and recalculating counts"); - // Re-fetch the main category to get fresh data + // Re-fetch all categories to process in a single batch query categoryPeer::clearInstancePool(); - categoryPeer::setUseCriteriaFilter(false); - $category = categoryPeer::retrieveByPK($categoryId); - categoryPeer::setUseCriteriaFilter(true); - - // Collect all categories to process (main + children), ordered by depth - $allCategoriesToProcess = array($category); + $allCategoryIdsToProcess = array($categoryId); foreach ($deletedChildCategories as $childCategory) { - // Re-fetch to get fresh data - categoryPeer::setUseCriteriaFilter(false); - $freshChild = categoryPeer::retrieveByPK($childCategory->getId()); - categoryPeer::setUseCriteriaFilter(true); - if ($freshChild) - { - $allCategoriesToProcess[] = $freshChild; - } + $allCategoryIdsToProcess[] = $childCategory->getId(); } + categoryPeer::setUseCriteriaFilter(false); + $allCategoriesToProcess = categoryPeer::retrieveByPKs($allCategoryIdsToProcess); + categoryPeer::setUseCriteriaFilter(true); + // Sort by depth to ensure parents are processed before children usort($allCategoriesToProcess, function($a, $b) { return $a->getDepth() - $b->getDepth(); @@ -381,7 +380,7 @@ $categoryToProcess->save(); $processedCategories++; - if ($processedCategories % 50 === 0) + if ($processedCategories % CATEGORY_BATCH_SIZE === 0) { categoryPeer::clearInstancePool(); KalturaLog::info("Processed $processedCategories categories so far..."); @@ -391,6 +390,11 @@ // Step 10: Update parent category's direct sub-categories count KalturaLog::info("Step 10: Updating parent category's sub-categories count"); + // Re-fetch main category after processing + categoryPeer::setUseCriteriaFilter(false); + $category = categoryPeer::retrieveByPK($categoryId); + categoryPeer::setUseCriteriaFilter(true); + $parentId = $category->getParentId(); if ($parentId) {