JezK
Edit File: Migrate_8_1_0.php
<?php namespace GeminiLabs\SiteReviews\Migrations; use GeminiLabs\SiteReviews\Contracts\MigrateContract; use GeminiLabs\SiteReviews\Database; use GeminiLabs\SiteReviews\Database\Query; class Migrate_8_1_0 implements MigrateContract { public function run(): bool { return $this->backfillMissingPostDateGmt(); } /** * Backfill missing post_date_gmt values. This computes the site's UTC * offset periods (DST transitions) and runs one bulk UPDATE per period. */ public function backfillMissingPostDateGmt(): bool { $range = $this->dateRange(); if (2 !== count($range)) { return true; // nothing to migrate } [$minDate, $maxDate] = $range; $result = true; foreach ($this->offsetPeriods($minDate, $maxDate) as $period) { // The INTERVAL value must never be negative: while negative // values are valid in MySQL/MariaDB, both translators of the // SQLite integration plugin prepend the sign to the value, // producing an invalid double-signed SQLite modifier which // silently evaluates to NULL. Choosing DATE_ADD/DATE_SUB by // the offset's sign is compatible with all of them. $function = $period['offset'] < 0 ? 'DATE_ADD' : 'DATE_SUB'; $sql = " UPDATE table|posts SET post_date_gmt = {$function}(post_date, INTERVAL %d SECOND) WHERE 1=1 AND post_type = %s AND post_date_gmt = '0000-00-00 00:00:00' AND post_date >= %s AND post_date < %s "; $query = glsr(Query::class)->sql($sql, abs($period['offset']), glsr()->post_type, $period['from'], $period['to'] ); if (false === glsr(Database::class)->dbQuery($query)) { glsr_log()->error( sprintf('The missing post_date_gmt of reviews between [%s] and [%s] could not be updated.', $period['from'], $period['to'] ) ); $result = false; } } return $result; } /** * The min/max post_date of the affected reviews. */ protected function dateRange(): array { $sql = " SELECT MIN(post_date) AS min_date, MAX(post_date) AS max_date FROM table|posts WHERE 1=1 AND post_type = %s AND post_date_gmt = '0000-00-00 00:00:00' AND post_date <> '0000-00-00 00:00:00' "; $query = glsr(Query::class)->sql($sql, glsr()->post_type); $row = glsr(Database::class)->dbGetRow($query, \ARRAY_A); if (empty($row['min_date']) || empty($row['max_date'])) { return []; } return [$row['min_date'], $row['max_date']]; } /** * The UTC offset periods (i.e. DST transitions) covering the given local * datetime range. This is typically 2 periods per year of reviews (or a * single period for timezones without DST). * * @return array<array{offset:int, from:string, to:string}> */ protected function offsetPeriods(string $minDate, string $maxDate): array { $timezone = wp_timezone(); $begin = (int) strtotime("{$minDate} UTC") - (2 * \DAY_IN_SECONDS); $end = (int) strtotime("{$maxDate} UTC") + (2 * \DAY_IN_SECONDS); $transitions = $timezone->getTransitions($begin, $end); if (empty($transitions)) { // fixed offset timezone $transitions = [[ 'offset' => $timezone->getOffset(new \DateTimeImmutable('now', $timezone)), 'ts' => $begin, ]]; } $periods = []; foreach ($transitions as $index => $transition) { $offset = (int) $transition['offset']; $from = ((int) $transition['ts']) + $offset; if (isset($transitions[$index + 1])) { $next = $transitions[$index + 1]; // Use the larger of the two offsets at the boundary so that // local times inside a spring-forward gap (times skipped by // the clock) still fall into a period instead of leaving the // row unmigrated. This matches how PHP's DateTime normalizes // nonexistent local times. $to = ((int) $next['ts']) + max($offset, (int) $next['offset']); } else { $to = $end + $offset; } $periods[] = [ 'offset' => $offset, 'from' => gmdate('Y-m-d H:i:s', $from), 'to' => gmdate('Y-m-d H:i:s', $to), ]; } return $periods; } }