fix(2meet-data-optimizer): refuse uninstall while data lives only in flat tables

- uninstall.php checks every site first and wp_die()s (keeping tables,
  options and plugin files) on aeav_only modes, misc rows, or zone
  modules past cutover; a failed bootstrap also refuses
- Multisite now checks and cleans every site: network activation is always
  false at uninstall, so only the main site was being cleaned before
- Drop the inline fallback DROP loop, unreachable now that a missing
  helper refuses instead

Verify: UninstallGuardTest (8 tests incl. running uninstall.php single and
  multisite); unit 602 OK, integration 429 OK
PR: PR-008
This commit is contained in:
2026-09-25 22:16:02 +08:00
parent fc0f018a94
commit b407f235ac
3 changed files with 234 additions and 171 deletions
+52
View File
@@ -297,6 +297,58 @@ class TMDO_Installer {
update_option( self::MISC_BUCKET_DEFAULT_FLAG, '1', true );
}
/**
* Reasons this site's wpdo_* tables cannot be dropped yet; empty means safe.
*
* Reads the site's own options rather than Mode_Manager / Feature_Flags,
* whose per-request caches do not follow switch_to_blog() during uninstall.
*
* @return string[]
*/
public static function uninstall_blockers(): array {
global $wpdb;
$blockers = array();
$modes = get_option( 'wpdo_bridge_modes', array() );
foreach ( is_array( $modes ) ? $modes : array() as $entity => $mode ) {
if ( 'aeav_only' === $mode ) {
$blockers[] = sprintf( '%1$s 的 bridge 模式是 aeav_only:資料只存在 wpdo_%1$s_* 扁平表。', $entity );
}
}
foreach ( array( 'wpdo_term_misc', 'wpdo_comment_misc' ) as $name ) {
if ( self::table_has_rows( $wpdb->prefix . $name ) ) {
$blockers[] = sprintf( '%s 有資料:這些 term / comment meta 只存在 misc 表。', $name );
}
}
$features = get_option( 'wpdo_features', array() );
foreach ( TMDO_Feature_Flags::ZONE_MODULES as $module ) {
$state = is_array( $features ) ? ( $features[ $module ] ?? 'idle' ) : 'idle';
if ( in_array( $state, array( 'cleanup', 'complete' ), true ) ) {
$blockers[] = sprintf( '區域模組 %1$s 處於 %2$s:讀取已改走 wpdo_* 表。', $module, $state );
}
}
return $blockers;
}
/**
* Whether a table exists and holds at least one row.
*
* @param string $table Full table name.
* @return bool
*/
private static function table_has_rows( string $table ): bool {
global $wpdb;
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( $table !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) ) {
return false;
}
return null !== $wpdb->get_var( "SELECT 1 FROM `{$table}` LIMIT 1" );
// phpcs:enable
}
// ── v2.0.0 entity + audit + shadow_diffs schema (PR-2) ─────────────────
// These tables are installed only when v2.0.0 upgrade runs (PR-7).
// install_v2_tables() is idempotent; safe to call multiple times.