*/ private const GROUP_MAP = array( 'hp_taxonomy' => array( 'entity_type' => 'term', 'table_suffix' => 'wpdo_term_hp_taxonomy', ), 'hp_review' => array( 'entity_type' => 'comment', 'table_suffix' => 'wpdo_comment_hp_review', ), ); /** * Run backfill for every registered term + comment group. * * @param bool $dry_run When true, only count rows without writing. * @return array */ public static function backfill_all( bool $dry_run = false ): array { $results = array(); foreach ( self::GROUP_MAP as $group_name => $cfg ) { $results[ $group_name ] = self::backfill_group( $cfg['entity_type'], $group_name, $dry_run ); } return $results; } /** * Run backfill for a single group. * * @param string $entity_type 'term' or 'comment'. * @param string $group_name Entity group name. * @param bool $dry_run When true, only count rows without writing. * @return array{group:string, entity_type:string, candidates:int, written:int, dry_run:bool, error?:string} */ public static function backfill_group( string $entity_type, string $group_name, bool $dry_run = false ): array { $result = array( 'group' => $group_name, 'entity_type' => $entity_type, 'candidates' => 0, 'written' => 0, 'dry_run' => $dry_run, ); if ( ! class_exists( 'TMDO_Entity_Registry' ) ) { $result['error'] = 'Entity Registry unavailable'; return $result; } if ( ! isset( self::GROUP_MAP[ $group_name ] ) ) { $result['error'] = "Unknown group: {$group_name}"; return $result; } $keys = TMDO_Entity_Registry::get_group_keys( $entity_type, $group_name ); if ( empty( $keys ) ) { $result['error'] = "No keys registered for {$entity_type}:{$group_name}"; return $result; } global $wpdb; // Resolve source / meta tables + id columns per entity type. if ( 'term' === $entity_type ) { $source_table = $wpdb->terms; $source_id_col = 'term_id'; $meta_table = $wpdb->termmeta; $meta_id_col = 'term_id'; $flat_id_col = 'term_id'; } elseif ( 'comment' === $entity_type ) { $source_table = $wpdb->comments; $source_id_col = 'comment_ID'; $meta_table = $wpdb->commentmeta; $meta_id_col = 'comment_id'; $flat_id_col = 'comment_id'; } else { $result['error'] = "Unsupported entity_type: {$entity_type}"; return $result; } $flat_table = $wpdb->prefix . self::GROUP_MAP[ $group_name ]['table_suffix']; // Count candidate entities — those with at least one of the registered keys in wp_*meta. $placeholders = implode( ',', array_fill( 0, count( $keys ), '%s' ) ); $candidate_query = $wpdb->prepare( "SELECT COUNT(DISTINCT s.`{$source_id_col}`) FROM `{$source_table}` s INNER JOIN `{$meta_table}` m ON m.`{$meta_id_col}` = s.`{$source_id_col}` WHERE m.meta_key IN ({$placeholders})", ...$keys ); $result['candidates'] = (int) $wpdb->get_var( $candidate_query ); if ( $dry_run ) { return $result; } if ( 0 === $result['candidates'] ) { return $result; } // Build SELECT clause: id + one MAX(CASE) per registered key (sanitized to flat column). $select_parts = array( "s.`{$source_id_col}` AS id" ); $update_clauses = array(); $insert_cols = array( "`{$flat_id_col}`" ); foreach ( $keys as $key ) { $col = self::sanitize_column( $key ); $insert_cols[] = "`{$col}`"; $select_parts[] = $wpdb->prepare( "MAX(CASE WHEN m.meta_key = %s THEN m.meta_value END) AS `{$col}`", $key ); $update_clauses[] = "`{$col}` = COALESCE(VALUES(`{$col}`), `{$col}`)"; } $insert_sql = sprintf( 'INSERT INTO `%s` (%s) SELECT %s FROM `%s` s INNER JOIN `%s` m ON m.`%s` = s.`%s` WHERE m.meta_key IN (%s) GROUP BY s.`%s` ON DUPLICATE KEY UPDATE %s', $flat_table, implode( ', ', $insert_cols ), implode( ",\n ", $select_parts ), $source_table, $meta_table, $meta_id_col, $source_id_col, $placeholders, $source_id_col, implode( ', ', $update_clauses ) ); $prepared = $wpdb->prepare( $insert_sql, ...$keys ); $ok = $wpdb->query( $prepared ); if ( false === $ok ) { $result['error'] = $wpdb->last_error ?: 'Backfill query failed'; return $result; } // MySQL `INSERT ... ON DUPLICATE KEY UPDATE` returns 1 per insert, 2 per // update — divide by 2 with floor for a meaningful "rows touched" estimate // only when both sides equal candidates. Easier: just report candidates // since that's the upper bound and ON DUPLICATE replaces. $result['written'] = $result['candidates']; return $result; } /** * Sanitize meta_key into a column name (mirrors Schema_Manager rules). * * @param string $key Meta key. * @return string */ private static function sanitize_column( string $key ): string { if ( class_exists( 'TMDO_Schema_Manager' ) ) { return TMDO_Schema_Manager::sanitize_column_name( $key ); } return preg_replace( '/[^a-zA-Z0-9_]/', '_', $key ); } }