chore: initial snapshot of 2meet-data-optimizer v0.1.0

Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
2026-07-31 05:06:36 +08:00
commit d36bb954d1
206 changed files with 66538 additions and 0 deletions
@@ -0,0 +1,224 @@
<?php
/**
* TMDO_Term_Comment_Backfill — Pivot-style backfill from wp_*meta to flat
* tables for term + comment entities (v2.12.6 Phase 6).
*
* Background
* ----------
* Phase 15 cover write-time interception (anything written via the
* metadata API after v2.12.x activation goes to flat tables / wp_options /
* misc bucket / drop). But **historical** wp_termmeta / wp_commentmeta rows
* predating v2.12.x are still in those tables — they were never intercepted
* because they were already there.
*
* Phase 5's shadow_read verifier surfaces these as `missing_flat` drift
* (e.g. dev10 baseline: 42 term + 23 comment = 65 missing_flat rows).
* Without backfill, promoting to aeav_only would silently drop reads of
* those historical values (the read path goes flat → empty → fall-through
* to wp_termmeta DOES still work for now, but only because we left the
* fall-through in place; in v3.0.0 we DROP the source tables).
*
* Strategy
* --------
* One-shot SQL pivot per group, mirroring the user-side backfill pattern
* (v2.5.5 user_membership backfill, etc.):
*
* INSERT INTO wp_wpdo_term_hp_taxonomy (term_id, hp_sort_order, hp_default, hp_icon)
* SELECT t.term_id,
* MAX(CASE WHEN tm.meta_key = 'hp_sort_order' THEN tm.meta_value END),
* MAX(CASE WHEN tm.meta_key = 'hp_default' THEN tm.meta_value END),
* MAX(CASE WHEN tm.meta_key = 'hp_icon' THEN tm.meta_value END)
* FROM wp_terms t
* INNER JOIN wp_termmeta tm ON tm.term_id = t.term_id
* WHERE tm.meta_key IN (...registered keys...)
* GROUP BY t.term_id
* ON DUPLICATE KEY UPDATE
* hp_sort_order = COALESCE(VALUES(hp_sort_order), hp_sort_order),
* ...
*
* COALESCE preserves any value already in the flat table when the wp_*meta
* side has NULL for that key — important for partial-coverage backfills
* where some keys are present and others aren't on a given entity.
*
* 🔒 Frozen contract: never touches user / post entities; only term/comment.
*
* @package WP_Data_Optimizer
* @since 2.12.6
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared -- Pivot SQL: keys + table from registry, parameterized via prepare placeholders for keys list. One-shot ops command.
/**
* Pivot-style backfill from wp_termmeta / wp_commentmeta to flat tables.
*/
final class TMDO_Term_Comment_Backfill {
/**
* Group → flat table suffix mapping. Mirror of TMDO_Hivepress_Term_Comment_Fields.
*
* @var array<string, array{entity_type:string, table_suffix:string}>
*/
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<string, array{group:string, entity_type:string, candidates:int, written:int, dry_run:bool}>
*/
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 );
}
}