alignStatisticsKey($wpdb); dbDelta(Schema::sql($wpdb->prefix, $wpdb->get_charset_collate())); // ==================================================================== // THE ONE DATA STEP, AND THE REASON THERE NEEDS TO BE ONE. // ==================================================================== // Everything else WConvert stores is normalised on write against a // closed vocabulary and read tolerantly with defaults, which is what // lets schemas and shapes change without a migration ever running. // The published set is the exception, because nothing writes it on its // own: ADR 0003 rebuilds it on write and never on read, so an Optin // published once and never edited again keeps whatever shape the // projection had that day — indefinitely, and invisibly. // // Rebuilt here rather than tagged with a version on the wire. The // loader's `payload.ts` is defensive throughout and null is a real // answer everywhere in it, so the browser tolerating an old shape is // already the design; what was missing was anything that ever produced // a new one. // // AFTER the DDL and BEFORE the version is stored. The rebuild reads // the table `dbDelta` has just brought up to date, and leaving the // version unwritten if it fails is what makes the next `admin_init` // try again rather than record a half-done upgrade as finished. $this->optins->rebuildForInstall(); $this->options->set(self::VERSION_OPTION, self::VERSION); } /** dbDelta cannot replace a primary key; reconcile the approved scope/key together. */ private function alignStatisticsKey(\wpdb $db): void { $table = $db->prefix . Connection::TABLE_STATS; if ($db->get_var($db->prepare('SHOW TABLES LIKE %s', $db->esc_like($table))) !== $table) { return; } $columns = $db->get_col($db->prepare('SHOW COLUMNS FROM %i', $table)); $indexes = $db->get_results($db->prepare("SHOW INDEX FROM %i WHERE Key_name = 'PRIMARY'", $table), ARRAY_A); $primary = array_column(is_array($indexes) ? $indexes : [], 'Column_name'); if (in_array('scope', $columns, true) && $primary === ['optin_id', 'stat_date', 'kind', 'scope']) { return; } $changes = []; if (!in_array('scope', $columns, true)) { $changes[] = "ADD COLUMN scope VARCHAR(160) NOT NULL DEFAULT '' AFTER kind"; } if ($primary !== []) { $changes[] = 'DROP PRIMARY KEY'; } $changes[] = 'ADD PRIMARY KEY (optin_id,stat_date,kind,scope)'; if ($db->query($db->prepare('ALTER TABLE %i ' . implode(', ', $changes), $table)) === false) { throw new DatabaseException('Could not install the approved journey statistics key.'); } } /** * **Compared with `!==`, so a downgrade re-runs too**, and that is benign * rather than merely tolerated: an older build re-runs its own DDL, which * `dbDelta` reconciles without touching the newer column, and rebuilds the * published set at the shape that build's projection actually produces. * Which is what a site running that build needs. */ public function upgradeIfNeeded(): void { if ($this->options->get(self::VERSION_OPTION) === self::VERSION) { return; } $this->install(); } }