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(); } }