d36bb954d1
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
213 lines
6.3 KiB
PHP
213 lines
6.3 KiB
PHP
<?php
|
|
/**
|
|
* Zone A (Hot) handler for flat-column custom tables.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Zone A (Hot) handler — flat-column custom tables for search/filter fields.
|
|
*
|
|
* Each post type gets its own table: wpdo_hot_{post_type}
|
|
* Columns are defined by the Schema Registry and created via TMDO_Installer.
|
|
*
|
|
* Key advantage over HPCT's KV table (hpct_listing_meta):
|
|
* KV table: N meta_query conditions = N LEFT JOINs
|
|
* Flat table: N conditions = 1 LEFT JOIN + N WHERE clauses
|
|
*/
|
|
class TMDO_Zone_Hot {
|
|
|
|
/**
|
|
* Get the table name for a post type.
|
|
*
|
|
* @param string $post_type Post type slug.
|
|
* @return string Full table name.
|
|
*/
|
|
public static function table( string $post_type ): string {
|
|
return TMDO_DB::table( 'wpdo_hot_' . sanitize_key( $post_type ) );
|
|
}
|
|
|
|
/**
|
|
* Ensure the hot table exists for a post type.
|
|
* Creates it dynamically from Schema Registry if missing.
|
|
*
|
|
* @param string $post_type Post type slug.
|
|
* @return void
|
|
*/
|
|
public static function ensure_table( string $post_type ): void {
|
|
$columns = TMDO_Schema_Registry::instance()->get_hot_columns( $post_type );
|
|
if ( empty( $columns ) ) {
|
|
return;
|
|
}
|
|
|
|
global $wpdb;
|
|
$table = self::table( $post_type );
|
|
|
|
// Quick existence check.
|
|
if ( TMDO_IS_SQLITE ) {
|
|
$exists = $wpdb->get_var(
|
|
$wpdb->prepare( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=%s", $table )
|
|
);
|
|
} else {
|
|
$exists = $wpdb->get_var(
|
|
$wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $table )
|
|
);
|
|
}
|
|
|
|
if ( ! $exists ) {
|
|
TMDO_Installer::create_hot_table( $post_type, $columns );
|
|
return;
|
|
}
|
|
|
|
// v2.1.2 critical fix: ensure existing tables have all columns declared
|
|
// by Schema_Registry. Partner plugins registering new hot fields after
|
|
// the initial migration would otherwise silently fall back to postmeta.
|
|
//
|
|
// v2.1.3 optimization: schema fingerprint stored in option. When the hash
|
|
// of declared columns matches the stored hash, skip SHOW COLUMNS entirely.
|
|
// Only diff + ALTER fires when fingerprint actually changed (drift detected).
|
|
// This avoids per-request SHOW COLUMNS overhead on stable production sites.
|
|
static $checked = array();
|
|
if ( isset( $checked[ $post_type ] ) ) {
|
|
return;
|
|
}
|
|
|
|
$fingerprint = self::compute_fingerprint( $columns );
|
|
$opt_key = 'wpdo_hot_fp_' . $post_type;
|
|
$stored_fp = (string) get_option( $opt_key, '' );
|
|
|
|
if ( $stored_fp === $fingerprint ) {
|
|
// Schema unchanged since last successful ensure → no need to SHOW COLUMNS.
|
|
$checked[ $post_type ] = true;
|
|
return;
|
|
}
|
|
|
|
// Fingerprint mismatch (or first run) → run diff + ALTER, then store new hash.
|
|
TMDO_Installer::ensure_hot_columns( $post_type, $columns );
|
|
update_option( $opt_key, $fingerprint, false );
|
|
$checked[ $post_type ] = true;
|
|
}
|
|
|
|
/**
|
|
* Stable hash of declared column definitions. Used to short-circuit SHOW COLUMNS
|
|
* when Schema_Registry hasn't changed since the last successful ensure.
|
|
*
|
|
* @param array $columns column_name => sql_type map.
|
|
* @return string Short SHA-1 prefix (10 chars — collision-safe at our scale).
|
|
*
|
|
* @since 2.1.3
|
|
*/
|
|
private static function compute_fingerprint( array $columns ): string {
|
|
ksort( $columns );
|
|
return substr( sha1( wp_json_encode( $columns ) ?: '' ), 0, 10 );
|
|
}
|
|
|
|
/**
|
|
* Read a single field value from the hot table.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $post_type Post type.
|
|
* @param string $column Column name in hot table.
|
|
* @return mixed|null Value or null if not found.
|
|
*/
|
|
public static function get( int $post_id, string $post_type, string $column ): mixed {
|
|
global $wpdb;
|
|
$table = self::table( $post_type );
|
|
$column = sanitize_key( $column );
|
|
|
|
return $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT `{$column}` FROM `{$table}` WHERE post_id = %d LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- column from sanitize_key(); table from TMDO_DB::table().
|
|
$post_id
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Read all hot fields for a post as an associative array.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $post_type Post type.
|
|
* @return array|null Column => value pairs, or null.
|
|
*/
|
|
public static function get_row( int $post_id, string $post_type ): ?array {
|
|
global $wpdb;
|
|
$table = self::table( $post_type );
|
|
|
|
$row = $wpdb->get_row(
|
|
$wpdb->prepare( "SELECT * FROM `{$table}` WHERE post_id = %d LIMIT 1", $post_id ), // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from TMDO_Zone_Hot::table() via TMDO_DB::table().
|
|
ARRAY_A
|
|
);
|
|
|
|
return $row ?: null;
|
|
}
|
|
|
|
/**
|
|
* Upsert a single field into the hot table.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $post_type Post type.
|
|
* @param string $column Column name.
|
|
* @param mixed $value Value to set.
|
|
*/
|
|
public static function set( int $post_id, string $post_type, string $column, mixed $value ): void {
|
|
$table = self::table( $post_type );
|
|
$column = sanitize_key( $column );
|
|
$now = TMDO_DB::now();
|
|
|
|
TMDO_DB::upsert(
|
|
$table,
|
|
array(
|
|
'post_id' => $post_id,
|
|
$column => $value,
|
|
'updated_at' => $now,
|
|
),
|
|
array( $column, 'updated_at' ),
|
|
'post_id'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Upsert multiple fields at once for a post.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $post_type Post type.
|
|
* @param array $data Column => value pairs.
|
|
*/
|
|
public static function set_many( int $post_id, string $post_type, array $data ): void {
|
|
$table = self::table( $post_type );
|
|
$now = TMDO_DB::now();
|
|
|
|
$data['post_id'] = $post_id;
|
|
$data['updated_at'] = $now;
|
|
$update_cols = array_diff( array_keys( $data ), array( 'post_id' ) );
|
|
|
|
TMDO_DB::upsert( $table, $data, $update_cols, 'post_id' );
|
|
}
|
|
|
|
/**
|
|
* Delete a post's row from the hot table.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $post_type Post type slug.
|
|
* @return void
|
|
*/
|
|
public static function delete( int $post_id, string $post_type ): void {
|
|
global $wpdb;
|
|
$wpdb->delete( self::table( $post_type ), array( 'post_id' => $post_id ), array( '%d' ) );
|
|
}
|
|
|
|
/**
|
|
* Get all post types that have hot zone tables registered.
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public static function get_post_types(): array {
|
|
return TMDO_Schema_Registry::instance()->get_hot_post_types();
|
|
}
|
|
}
|