feat(2meet-data-optimizer): guard orphaned flat-table groups
- On init:1, re-register aeav_only groups whose flat table exists and whose definition is in wpdo_registry_meta but no active plugin registers them, so their data stays readable (routing only, never DDL) - Keys owned by a live group, or with rows in native meta (newer data written while orphaned), are left alone; the native-meta check is cached for a day - Surface restored groups via Site Health (wpdo_orphaned_entity_groups), the admin Doctor tab, and wp wpdo doctor - Verify: EntityRegistryRestoreTest (9), OrphanGroupRestoreTest (7); core unit 622 / integration 439, HP 141/49, WC 10/22 OK; dry-run on dev20–22 restores 16/17/4 groups, keeps hp_taxonomy hp_sort_order/hp_icon on native - PR: PR-018
This commit is contained in:
@@ -268,6 +268,113 @@ final class TMDO_Schema_Manager {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 還原孤兒群組:扁平表還在、定義存在 wpdo_registry_meta,但已沒有外掛註冊。
|
||||
*
|
||||
* 在所有註冊都完成後(init:1 晚綁定)呼叫。只處理 aeav_only 的實體:其他
|
||||
* 模式仍會寫原生 meta,孤兒 key 從原生讀取即正確;全部非 aeav_only 時不查資料庫。
|
||||
* 原生 meta 已有資料的 key(孤兒期間寫入,比扁平表新)留在原生,不還原。
|
||||
* 還原只影響讀寫路由,不會建立或修改任何資料表。
|
||||
*
|
||||
* @return int 還原的群組數
|
||||
*/
|
||||
public static function restore_orphaned_groups(): int {
|
||||
global $wpdb;
|
||||
|
||||
if ( ! class_exists( 'TMDO_Mode_Manager' ) ) {
|
||||
return 0;
|
||||
}
|
||||
$types = array_values(
|
||||
array_filter(
|
||||
TMDO_Entity_Registry::VALID_ENTITY_TYPES,
|
||||
static fn( $type ) => TMDO_Mode_Manager::MODE_AEAV_ONLY === TMDO_Mode_Manager::get( $type )
|
||||
)
|
||||
);
|
||||
if ( empty( $types ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$meta_table = $wpdb->prefix . TMDO_TABLE_PREFIX . 'registry_meta';
|
||||
$placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare( "SELECT entity_type, group_name, field_definitions FROM `{$meta_table}` WHERE entity_type IN ({$placeholders})", ...$types ),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
$orphans = array();
|
||||
foreach ( (array) $rows as $row ) {
|
||||
if ( ! TMDO_Entity_Registry::get_group_fields( $row['entity_type'], $row['group_name'] ) ) {
|
||||
$orphans[] = $row;
|
||||
}
|
||||
}
|
||||
if ( empty( $orphans ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// One SHOW TABLES for all candidates instead of one per orphan.
|
||||
$existing = (array) $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $wpdb->prefix . TMDO_TABLE_PREFIX ) . '%' ) );
|
||||
|
||||
// "entity:key" → native meta has rows. Stable while the key is restored
|
||||
// (aeav_only writes go to the flat table only), so a day-long cache is safe.
|
||||
$native_cache = get_transient( 'tmdo_orphan_native_keys' );
|
||||
$native_cache = is_array( $native_cache ) ? $native_cache : array();
|
||||
$cache_dirty = false;
|
||||
|
||||
$restored = 0;
|
||||
foreach ( $orphans as $row ) {
|
||||
if ( ! in_array( self::get_table_name( $row['entity_type'], $row['group_name'] ), $existing, true ) ) {
|
||||
continue;
|
||||
}
|
||||
$fields = json_decode( (string) $row['field_definitions'], true );
|
||||
if ( ! is_array( $fields ) ) {
|
||||
continue;
|
||||
}
|
||||
$native_keys = array();
|
||||
foreach ( $fields as $field ) {
|
||||
$key = (string) ( $field['key'] ?? '' );
|
||||
if ( '' === $key ) {
|
||||
continue;
|
||||
}
|
||||
$cache_key = $row['entity_type'] . ':' . $key;
|
||||
if ( ! array_key_exists( $cache_key, $native_cache ) ) {
|
||||
$native_cache[ $cache_key ] = self::native_meta_has_key( $row['entity_type'], $key );
|
||||
$cache_dirty = true;
|
||||
}
|
||||
if ( $native_cache[ $cache_key ] ) {
|
||||
$native_keys[] = $key;
|
||||
}
|
||||
}
|
||||
if ( TMDO_Entity_Registry::restore_group( $row['entity_type'], $row['group_name'], $fields, $native_keys ) ) {
|
||||
++$restored;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $cache_dirty ) {
|
||||
set_transient( 'tmdo_orphan_native_keys', $native_cache, DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $restored;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the entity's native meta table holds any row for this key.
|
||||
*/
|
||||
private static function native_meta_has_key( string $entity_type, string $meta_key ): bool {
|
||||
global $wpdb;
|
||||
$tables = array(
|
||||
'post' => $wpdb->postmeta,
|
||||
'user' => $wpdb->usermeta,
|
||||
'term' => $wpdb->termmeta,
|
||||
'comment' => $wpdb->commentmeta,
|
||||
);
|
||||
if ( empty( $tables[ $entity_type ] ) ) {
|
||||
return false;
|
||||
}
|
||||
return (bool) $wpdb->get_var(
|
||||
$wpdb->prepare( "SELECT 1 FROM `{$tables[ $entity_type ]}` WHERE meta_key = %s LIMIT 1", $meta_key )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得已儲存的 Schema hash
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user