*/ private const STAGED_VIEW_TABLE_SUFFIXES = array( 'abj404_view_build', 'abj404_view_done', 'abj404_view_deleteme', ); /** * Drop the transient staged view-build tables (view_build, view_done, * view_deleteme) if present. Runs from the upgrade-only post-create hook * (DatabaseUpgradeTableRepair::correctIssuesAfter); never from cron. * * Returns the count of tables actually dropped (0 on a fresh install, a * re-upgrade, a missing $wpdb, or a write-blocked DB) so callers/tests can * assert idempotency: the first post-3e-D upgrade returns >=1 when residue * exists, every subsequent run returns 0. * * @return int Number of transient tables that existed and were dropped. */ public function dropStagedViewTables(): int { // CRON GUARD (Rule G): operator-driven upgrade path only. A daily cron // tick must never reach a DROP TABLE on these (or any) tables. Must be // the first executable statement so the lint can see it precedes the // destructive SQL below. if (function_exists('wp_doing_cron') && wp_doing_cron()) { return 0; } global $wpdb; if (!isset($wpdb)) { return 0; } // Read-only replica / disk full: leave the residue in place (it is // inert) and let the next healthy upgrade tick clean it up. Logging a // failed DROP as an error here would email the admin about a hosting // condition the plugin degrades past, so skip quietly instead. if ($this->dbCore->noticeState()->isWriteBlockActive()) { $this->logger->debugMessage( 'dropStagedViewTables skipped: DB write block active (read-only / disk full). ' . 'Transient staged-view residue is inert; the next healthy upgrade tick will drop it.' ); return 0; } $dropped = 0; foreach (self::STAGED_VIEW_TABLE_SUFFIXES as $suffix) { $tableName = $this->dbCore->doTableNameReplacements('{wp_' . $suffix . '}'); if (!$this->stagedTableExists($tableName)) { continue; } // @utf8-audit: opt-out - $tableName is doTableNameReplacements() of a fixed internal placeholder (lowercase prefix + literal suffix); system-controlled, cannot contain invalid UTF-8. // DAO-bypass-approved: idempotent DROP TABLE IF EXISTS on a deprecated transient table guarded by a SHOW TABLES existence probe; routing through queryAndGetResults would surface a benign infrastructure warning on a write-blocked host even though the table is inert residue. $result = ABJ_404_Solution_DatabaseDriverCall::query($wpdb, 'DROP TABLE IF EXISTS `' . esc_sql($tableName) . '`'); if ($result === false) { // A write failed despite the write-block probe passing (e.g. a // lock timeout). Inert residue, hosting-side cause: warn (not // error, no email) and move on; the next upgrade retries. $this->logger->warn( 'dropStagedViewTables: DROP of transient table ' . $tableName . ' failed (' . (is_string($wpdb->last_error) ? $wpdb->last_error : 'unknown') . '). ' . 'Residue is inert; the next upgrade tick will retry.' ); continue; } $dropped++; $this->logger->infoMessage( 'dropStagedViewTables: dropped vestigial staged view-build table ' . $tableName . ' (denorm Step 3e-D). The live read now serves off the abj404_redirects denorm columns.' ); } return $dropped; } /** * Case-sensitive SHOW TABLES existence probe for a transient staged table. * * Bypasses the DAO on purpose: routing a "does this table exist" probe * through queryAndGetResults would log a benign "table doesn't exist" line * on every fresh-install upgrade (the common case, where the residue was * never present). Same probe shape the denorm backfill/reconcile use. * * @param string $tableName Fully-qualified, lowercase-prefixed table name. * @return bool */ private function stagedTableExists(string $tableName): bool { global $wpdb; if (!isset($wpdb)) { return false; } // @utf8-audit: opt-out - $tableName is a fully-qualified, lowercase-prefixed plugin table name from doTableNameReplacements(); system-controlled, cannot contain invalid UTF-8. // DAO-bypass-approved: schema existence probe (SHOW TABLES); see method docblock. $found = ABJ_404_Solution_DatabaseDriverCall::getVar($wpdb, "SHOW TABLES LIKE '" . esc_sql($tableName) . "'"); return $found === $tableName; } }