Files
2meet-data-optimizer/includes/class-tmdo-comment-stress-tester.php
T
wpdev f524ea3f16 build: 移植 PHPCS + PHPStan L6 品質 gate(PR-I 前半)
- phpcs.xml(自 A 移植):ruleset 改名、*/tools/* 例外換成 */back-compat/*、
  中文註解全域排除 Squiz.Commenting.InlineComment.InvalidEndChar、
  interface 別名檔排除 OneObjectStructurePerFile
- 檔頭正規化:16 個檔案的 declare(strict_types=1) 與前導 // 註解移到
  file docblock 之後,並移除 <?php 後多餘空行(phpcbf 另自動修 190 處)
- phpstan.neon + .phpstan/stubs.php(TMDO_ 與 WPDO_ 兩套常數)+
  重新產生的 phpstan-baseline.neon(710 errors,A 的 3877 行 baseline
  因前綴與路徑不同無法沿用)

現況:PHPCS 0 errors / 0 warnings、PHPStan L6 No errors、
unit 451 / integration 398 GREEN

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 06:22:00 +08:00

868 lines
26 KiB
PHP

<?php
/**
* TMDO_Comment_Stress_Tester — Async stress fixture generator for comment entity (v2.13.1).
*
* Mirrors TMDO_Term_Stress_Tester (v2.13.0) but adapted for comment entity:
*
* - Source: $wpdb->comments (comment_ID, comment_post_ID, comment_author, ...)
* - Meta: $wpdb->commentmeta
* - Flat: wpdo_comment_hp_review (single hp_rating key) + wpdo_comment_misc
* - Realistic mode: wp_insert_comment() + update_comment_meta()
* - Fast mode: bulk INSERT to wp_comments + wp_commentmeta
*
* Test comments are identified by `comment_author_email LIKE '%@wpdo-stress.local'`.
* Each comment is attached to a caller-specified `comment_post_ID` (must exist).
*
* 🔒 v2.13.x frozen contract: never touches user / post / term entities or
* their flat tables.
*
* @package WP_Data_Optimizer
* @since 2.13.1
*/
// phpcs:ignore WPDO.AntiEAV -- platform stress tester: intentional raw meta SQL for baseline comparison
declare(strict_types=1);
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Internal stress fixture: $wpdb->comments / wp_commentmeta WP-managed; meta_key strings static class constants; user-controlled values use prepare() placeholders.
/**
* Async bulk fixture generator for comment entity stress tests.
*/
final class TMDO_Comment_Stress_Tester {
/** Email domain marker — comments matching this are stress test fixtures. */
public const TEST_EMAIL_DOMAIN = 'wpdo-stress.local';
/** Hard cap to prevent runaway calls. */
public const MAX_COUNT = 100000;
// State machine constants.
public const OPT_STATE = 'wpdo_comment_stress_test_state';
public const CRON_HOOK = 'wpdo_comment_stress_test_batch';
public const CANCEL_FLAG = 'wpdo_comment_stress_cancel_flag';
public const DEFAULT_BATCH_SIZE = 200;
public const MAX_BATCH_SIZE = 1000;
public const MIN_BATCH_DELAY_SEC = 1;
public const BATCH_DEADLINE_SEC = 8;
public const MODE_FAST = 'fast';
public const MODE_REALISTIC = 'realistic';
/**
* Per-key seed map (hp_review group: just hp_rating).
*
* @var array<string,mixed>|null
*/
private static ?array $seed_map_cache = null;
// ─────────────────────────────────────────────────────────────────────────
// Public API — sync helpers (also used internally by state machine batches)
// ─────────────────────────────────────────────────────────────────────────
/**
* Bulk-create N stress test comments on the given post via direct SQL
* (Fast mode — bypasses WP filter chain).
*
* @param int $post_id WP post ID to attach comments to.
* @param int $count Number of comments to insert. Capped at MAX_COUNT.
* @return array{created:int,post_id:int,first_id:int|null,last_id:int|null}
* @throws InvalidArgumentException When inputs invalid.
*/
public static function create( int $post_id, int $count ): array {
self::validate_inputs( $post_id, $count );
global $wpdb;
$first_id = null;
$last_id = null;
$created = 0;
$seed_map = self::seed_map();
$now = current_time( 'mysql' );
$now_gmt = current_time( 'mysql', true );
for ( $i = 0; $i < $count; $i++ ) {
$suffix = wp_generate_password( 8, false );
$ok = $wpdb->insert(
$wpdb->comments,
array(
'comment_post_ID' => $post_id,
'comment_author' => 'WPDO Stress ' . $suffix,
'comment_author_email' => 'wpdo+' . $suffix . '@' . self::TEST_EMAIL_DOMAIN,
'comment_author_url' => '',
'comment_author_IP' => '127.0.0.1',
'comment_date' => $now,
'comment_date_gmt' => $now_gmt,
'comment_content' => 'Stress test comment ' . $suffix,
'comment_karma' => 0,
'comment_approved' => '1',
'comment_agent' => 'wpdo-stress-tester',
'comment_type' => 'comment',
'comment_parent' => 0,
'user_id' => 0,
)
);
if ( ! $ok ) {
continue;
}
$comment_id = (int) $wpdb->insert_id;
if ( null === $first_id ) {
$first_id = $comment_id;
}
$last_id = $comment_id;
++$created;
// Direct INSERT to wp_commentmeta (bypassing Hook Bus). For aeav_only
// mode validation use create_realistic() instead.
foreach ( $seed_map as $meta_key => $value_spec ) {
$value = is_callable( $value_spec ) ? $value_spec( $i ) : $value_spec;
$wpdb->insert(
$wpdb->commentmeta,
array(
'comment_id' => $comment_id,
'meta_key' => $meta_key,
'meta_value' => (string) $value,
)
);
}
}
return array(
'created' => $created,
'post_id' => $post_id,
'first_id' => $first_id,
'last_id' => $last_id,
);
}
/**
* Realistic-mode counterpart — uses wp_insert_comment() +
* update_comment_meta() so Hook Bus / entity registry / mode_manager all
* engage on the write path.
*
* @param int $post_id WP post ID.
* @param int $count Number of comments.
* @return array{created:int,post_id:int,mode:string,first_id:int|null,last_id:int|null}
* @throws InvalidArgumentException When inputs invalid.
*/
public static function create_realistic( int $post_id, int $count ): array {
self::validate_inputs( $post_id, $count );
$first_id = null;
$last_id = null;
$created = 0;
$seed_map = self::seed_map();
for ( $i = 0; $i < $count; $i++ ) {
$suffix = wp_generate_password( 8, false );
$comment_id = wp_insert_comment(
array(
'comment_post_ID' => $post_id,
'comment_author' => 'WPDO Stress ' . $suffix,
'comment_author_email' => 'wpdo+' . $suffix . '@' . self::TEST_EMAIL_DOMAIN,
'comment_content' => 'Stress test comment ' . $suffix,
'comment_approved' => 1,
'comment_type' => 'comment',
)
);
if ( ! $comment_id ) {
continue;
}
$comment_id = (int) $comment_id;
if ( null === $first_id ) {
$first_id = $comment_id;
}
$last_id = $comment_id;
++$created;
foreach ( $seed_map as $meta_key => $value_spec ) {
$value = is_callable( $value_spec ) ? $value_spec( $i ) : $value_spec;
update_comment_meta( $comment_id, $meta_key, $value );
}
}
return array(
'created' => $created,
'post_id' => $post_id,
'mode' => 'realistic',
'first_id' => $first_id,
'last_id' => $last_id,
);
}
/**
* Count comments whose author email ends with TEST_EMAIL_DOMAIN.
*
* @return int
*/
public static function count_test_comments(): int {
global $wpdb;
return (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_author_email LIKE %s",
'%@' . $wpdb->esc_like( self::TEST_EMAIL_DOMAIN )
)
);
}
/**
* Delete every stress-test comment + its commentmeta + flat table rows.
*
* @return array{deleted_comments:int,deleted_meta:int,deleted_flat:int}
*/
public static function cleanup(): array {
global $wpdb;
set_transient( self::CANCEL_FLAG, 1, 600 );
wp_clear_scheduled_hook( self::CRON_HOOK );
$comment_ids = $wpdb->get_col(
$wpdb->prepare(
"SELECT comment_ID FROM {$wpdb->comments} WHERE comment_author_email LIKE %s",
'%@' . $wpdb->esc_like( self::TEST_EMAIL_DOMAIN )
)
);
if ( empty( $comment_ids ) ) {
return array(
'deleted_comments' => 0,
'deleted_meta' => 0,
'deleted_flat' => 0,
);
}
$id_list = implode( ',', array_map( 'absint', $comment_ids ) );
$flat_deleted = 0;
foreach ( self::get_comment_flat_tables() as $tbl ) {
$exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $tbl ) );
if ( ! $exists ) {
continue;
}
$rows = (int) $wpdb->query( "DELETE FROM `{$tbl}` WHERE comment_id IN ({$id_list})" );
$flat_deleted += $rows;
}
$meta_deleted = (int) $wpdb->query( "DELETE FROM {$wpdb->commentmeta} WHERE comment_id IN ({$id_list})" );
$comment_deleted = (int) $wpdb->query( "DELETE FROM {$wpdb->comments} WHERE comment_ID IN ({$id_list})" );
delete_option( self::OPT_STATE );
delete_transient( self::CANCEL_FLAG );
return array(
'deleted_comments' => $comment_deleted,
'deleted_meta' => $meta_deleted,
'deleted_flat' => $flat_deleted,
);
}
// ─────────────────────────────────────────────────────────────────────────
// State machine
// ─────────────────────────────────────────────────────────────────────────
/**
* Start an async stress run.
*
* @param int $post_id Post ID to attach comments to.
* @param int $target Total comments to create.
* @param string $mode MODE_FAST | MODE_REALISTIC.
* @param int $batch_size Per-batch insert count.
* @return array{ok:bool,error?:string,state?:array}
*/
public static function start( int $post_id, int $target, string $mode = self::MODE_FAST, int $batch_size = self::DEFAULT_BATCH_SIZE ): array {
if ( $post_id < 1 || ! self::post_exists( $post_id ) ) {
return array(
'ok' => false,
'error' => 'unknown_post: ' . $post_id,
);
}
if ( $target < 1 ) {
return array(
'ok' => false,
'error' => 'target must be >= 1',
);
}
if ( $target > self::MAX_COUNT ) {
return array(
'ok' => false,
'error' => 'target too large (max ' . self::MAX_COUNT . ')',
);
}
if ( ! in_array( $mode, array( self::MODE_FAST, self::MODE_REALISTIC ), true ) ) {
return array(
'ok' => false,
'error' => 'invalid mode',
);
}
$batch_size = max( 1, min( self::MAX_BATCH_SIZE, $batch_size ) );
$current = self::get_state();
if ( ! empty( $current['status'] ) && 'running' === $current['status'] ) {
return array(
'ok' => false,
'error' => 'already_running',
'state' => $current,
);
}
$state = array(
'job_id' => uniqid( 'cstress_', true ),
'status' => 'running',
'mode' => $mode,
'post_id' => $post_id,
'target' => $target,
'batch_size' => $batch_size,
'started_at' => time(),
'processed' => 0,
'batches_done' => 0,
'batches_log' => array(),
'errors' => array(),
'peak_memory' => 0,
'completed_at' => null,
'last_pushed_at' => 0,
'benchmark' => null,
);
update_option( self::OPT_STATE, $state, false );
delete_transient( self::CANCEL_FLAG );
wp_clear_scheduled_hook( self::CRON_HOOK );
wp_schedule_single_event( time(), self::CRON_HOOK );
return array(
'ok' => true,
'state' => $state,
);
}
/**
* Cancel a running stress test.
*
* @return array{ok:bool,state?:array,message?:string}
*/
public static function cancel(): array {
$state = self::get_state();
if ( empty( $state ) ) {
return array(
'ok' => true,
'message' => 'no_active_job',
);
}
set_transient( self::CANCEL_FLAG, 1, 600 );
wp_clear_scheduled_hook( self::CRON_HOOK );
$state = self::get_state();
$state['status'] = 'cancelled';
$state['completed_at'] = time();
update_option( self::OPT_STATE, $state, false );
return array(
'ok' => true,
'state' => $state,
);
}
/**
* Read raw state.
*
* @return array
*/
public static function get_state(): array {
$state = get_option( self::OPT_STATE, array() );
return is_array( $state ) ? $state : array();
}
/**
* Get progress with computed pct/rate/ETA.
*
* @param bool $pump When true, opportunistically pump.
* @return array
*/
public static function get_progress( bool $pump = true ): array {
if ( $pump ) {
self::pump_if_due();
}
$state = self::get_state();
if ( empty( $state ) ) {
return array(
'status' => 'idle',
'processed' => 0,
'target' => 0,
'pct' => 0,
);
}
$processed = (int) ( $state['processed'] ?? 0 );
$target = (int) ( $state['target'] ?? 0 );
$started = (int) ( $state['started_at'] ?? 0 );
$ended = (int) ( $state['completed_at'] ?? 0 );
$now = $ended > 0 ? $ended : time();
$elapsed = max( 1, $now - $started );
$rate = $processed > 0 ? round( $processed / $elapsed, 1 ) : 0;
$eta_sec = ( $rate > 0 && $processed < $target ) ? (int) ceil( ( $target - $processed ) / $rate ) : 0;
$pct = $target > 0 ? round( ( $processed / $target ) * 100, 1 ) : 0;
return array_merge(
$state,
array(
'pct' => $pct,
'rate_per_sec' => $rate,
'elapsed_sec' => $elapsed,
'eta_sec' => $eta_sec,
'test_comment_count' => self::count_test_comments(),
)
);
}
/**
* Opportunistic batch pump (transient-locked).
*
* @return void
*/
public static function pump_if_due(): void {
$state = self::get_state();
if ( empty( $state ) || 'running' !== ( $state['status'] ?? '' ) ) {
return;
}
$last_pushed_at = (int) ( $state['last_pushed_at'] ?? $state['started_at'] ?? 0 );
if ( time() - $last_pushed_at < 1 ) {
return;
}
$lock_key = 'wpdo_comment_stress_pump_lock';
if ( false !== get_transient( $lock_key ) ) {
return;
}
set_transient( $lock_key, 1, 30 );
if ( function_exists( 'set_time_limit' ) ) {
@set_time_limit( self::BATCH_DEADLINE_SEC + 10 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors
}
try {
self::run_batch();
} finally {
delete_transient( $lock_key );
}
}
/**
* Cron entrypoint.
*
* @return void
*/
public static function run_batch(): void {
$state = self::get_state();
if ( empty( $state ) || 'running' !== ( $state['status'] ?? '' ) ) {
return;
}
if ( false !== get_transient( self::CANCEL_FLAG ) ) {
return;
}
$post_id = (int) ( $state['post_id'] ?? 0 );
$target = (int) ( $state['target'] ?? 0 );
$processed = (int) ( $state['processed'] ?? 0 );
$batch_size = (int) ( $state['batch_size'] ?? self::DEFAULT_BATCH_SIZE );
$mode = (string) ( $state['mode'] ?? self::MODE_FAST );
$remaining = $target - $processed;
if ( $remaining <= 0 ) {
self::finalize( $state );
return;
}
$this_batch_size = min( $batch_size, $remaining );
$batch_started = microtime( true );
try {
if ( self::MODE_FAST === $mode ) {
$inserted = self::run_batch_fast( $post_id, $this_batch_size );
} else {
$inserted = self::run_batch_realistic( $post_id, $this_batch_size );
}
} catch ( \Throwable $e ) {
$state['errors'][] = array(
'time' => time(),
'message' => $e->getMessage(),
);
$state['status'] = 'failed';
$state['completed_at'] = time();
update_option( self::OPT_STATE, $state, false );
if ( class_exists( 'TMDO_Logger' ) ) {
TMDO_Logger::error( 'stress_test', 'comment_batch_failed', $e->getMessage() );
}
return;
}
$batch_elapsed = microtime( true ) - $batch_started;
$latest = self::get_state();
if ( empty( $latest ) ) {
return;
}
$is_cancelled = ( 'cancelled' === ( $latest['status'] ?? '' ) ) || false !== get_transient( self::CANCEL_FLAG );
$latest['processed'] = ( (int) ( $latest['processed'] ?? 0 ) ) + $inserted;
$latest['batches_done'] = ( (int) ( $latest['batches_done'] ?? 0 ) ) + 1;
$latest['batches_log'][] = array(
'n' => $inserted,
'duration_ms' => (int) round( $batch_elapsed * 1000 ),
);
if ( count( $latest['batches_log'] ) > 200 ) {
$latest['batches_log'] = array_slice( $latest['batches_log'], -200 );
}
$latest['peak_memory'] = max( (int) ( $latest['peak_memory'] ?? 0 ), (int) memory_get_peak_usage( true ) );
$latest['last_pushed_at'] = time();
if ( $is_cancelled ) {
update_option( self::OPT_STATE, $latest, false );
return;
}
update_option( self::OPT_STATE, $latest, false );
if ( $latest['processed'] >= $target ) {
self::finalize( $latest );
return;
}
wp_schedule_single_event( time() + self::MIN_BATCH_DELAY_SEC, self::CRON_HOOK );
}
/**
* Run one fast-mode batch.
*
* @param int $post_id Post ID.
* @param int $count Batch size.
* @return int Inserted count.
*/
private static function run_batch_fast( int $post_id, int $count ): int {
$result = self::create( $post_id, $count );
return (int) ( $result['created'] ?? 0 );
}
/**
* Run one realistic-mode batch with deadline + cancel check per comment.
*
* @param int $post_id Post ID.
* @param int $count Batch size.
* @return int Inserted count.
*/
private static function run_batch_realistic( int $post_id, int $count ): int {
$deadline = microtime( true ) + self::BATCH_DEADLINE_SEC;
$inserted = 0;
$seed_map = self::seed_map();
for ( $i = 0; $i < $count; $i++ ) {
if ( microtime( true ) > $deadline ) {
break;
}
if ( false !== get_transient( self::CANCEL_FLAG ) ) {
break;
}
$suffix = wp_generate_password( 8, false );
$comment_id = wp_insert_comment(
array(
'comment_post_ID' => $post_id,
'comment_author' => 'WPDO Stress ' . $suffix,
'comment_author_email' => 'wpdo+' . $suffix . '@' . self::TEST_EMAIL_DOMAIN,
'comment_content' => 'Stress test comment ' . $suffix,
'comment_approved' => 1,
'comment_type' => 'comment',
)
);
if ( ! $comment_id ) {
continue;
}
++$inserted;
foreach ( $seed_map as $meta_key => $value_spec ) {
$value = is_callable( $value_spec ) ? $value_spec( $i ) : $value_spec;
update_comment_meta( (int) $comment_id, $meta_key, $value );
}
}
return $inserted;
}
/**
* Finalize: clear cron, run benchmark, persist completed state.
*
* @param array $state Pre-finalize state.
* @return void
*/
private static function finalize( array $state ): void {
wp_clear_scheduled_hook( self::CRON_HOOK );
$state['status'] = 'benchmarking';
$state['completed_at'] = time();
update_option( self::OPT_STATE, $state, false );
$report = self::run_benchmark( $state );
$state['benchmark'] = $report;
$state['status'] = 'completed';
update_option( self::OPT_STATE, $state, false );
}
/**
* Run benchmark.
*
* @param array|null $state Optional state snapshot.
* @return array
*/
public static function run_benchmark( ?array $state = null ): array {
$state = $state ?? self::get_state();
$post_id = (int) ( $state['post_id'] ?? 0 );
return array(
'generated_at' => time(),
'post_id' => $post_id,
'write' => self::compute_write_metrics( $state ),
'db_sizes' => self::measure_db_sizes(),
'query' => self::measure_query_performance(),
);
}
/**
* Write throughput metrics from state.
*
* @param array $state State snapshot.
* @return array
*/
private static function compute_write_metrics( array $state ): array {
$started = (int) ( $state['started_at'] ?? 0 );
$ended = (int) ( $state['completed_at'] ?? time() );
$processed = (int) ( $state['processed'] ?? 0 );
$elapsed = max( 1, $ended - $started );
$batches = $state['batches_log'] ?? array();
$durations = array_column( $batches, 'duration_ms' );
$min_ms = ! empty( $durations ) ? min( $durations ) : 0;
$max_ms = ! empty( $durations ) ? max( $durations ) : 0;
$avg_ms = ! empty( $durations ) ? (int) ( array_sum( $durations ) / count( $durations ) ) : 0;
return array(
'mode' => $state['mode'] ?? '',
'post_id' => (int) ( $state['post_id'] ?? 0 ),
'target' => (int) ( $state['target'] ?? 0 ),
'processed' => $processed,
'elapsed_sec' => $elapsed,
'rate_per_sec' => round( $processed / $elapsed, 2 ),
'batches_done' => (int) ( $state['batches_done'] ?? 0 ),
'batch_min_ms' => $min_ms,
'batch_max_ms' => $max_ms,
'batch_avg_ms' => $avg_ms,
'peak_memory_mb' => round( (int) ( $state['peak_memory'] ?? 0 ) / 1048576, 1 ),
);
}
/**
* Measure DB sizes for comment-related tables.
*
* @return array
*/
private static function measure_db_sizes(): array {
global $wpdb;
$tables = array( $wpdb->comments, $wpdb->commentmeta );
foreach ( self::get_comment_flat_tables() as $tbl ) {
if ( self::table_exists( $tbl ) ) {
$tables[] = $tbl;
}
}
if ( ! self::is_mysql() ) {
return array_map(
static fn( $t ) => array(
'table' => $t,
'rows' => self::table_row_count( $t ),
),
$tables
);
}
$placeholders = implode( ',', array_fill( 0, count( $tables ), '%s' ) );
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT TABLE_NAME AS t, TABLE_ROWS AS rows_count, DATA_LENGTH AS dl, INDEX_LENGTH AS il
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME IN ({$placeholders})",
...$tables
),
ARRAY_A
);
$out = array();
foreach ( (array) $rows as $r ) {
$dl = (int) $r['dl'];
$il = (int) $r['il'];
$total = $dl + $il;
$out[] = array(
'table' => $r['t'],
'rows' => (int) $r['rows_count'],
'data_mb' => round( $dl / 1048576, 2 ),
'index_mb' => round( $il / 1048576, 2 ),
'total_mb' => round( $total / 1048576, 2 ),
'avg_bytes' => $r['rows_count'] > 0 ? (int) ( $total / (int) $r['rows_count'] ) : 0,
);
}
return $out;
}
/**
* Measure 3 representative queries:
* - point: hp_rating = 5 lookup on flat
* - range: hp_rating > 3 ORDER BY DESC on flat
* - EAV baseline: same point query on wp_commentmeta
*
* @return array
*/
private static function measure_query_performance(): array {
global $wpdb;
$flat = $wpdb->prefix . 'wpdo_comment_hp_review';
if ( ! self::table_exists( $flat ) ) {
return array( 'note' => 'flat_table_missing' );
}
return array(
'point_rating_5' => self::time_query(
"SELECT comment_id FROM `{$flat}` WHERE hp_rating = 5 LIMIT 100"
),
'range_rating_top' => self::time_query(
"SELECT comment_id FROM `{$flat}` WHERE hp_rating > 3 ORDER BY hp_rating DESC LIMIT 100"
),
'eav_baseline' => self::time_query(
"SELECT comment_id FROM {$wpdb->commentmeta} WHERE meta_key = 'hp_rating' AND meta_value = '5' LIMIT 100"
),
);
}
// ─────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────
/**
* Validate inputs for create() / create_realistic().
*
* @param int $post_id Post ID.
* @param int $count Count.
* @return void
* @throws InvalidArgumentException When invalid.
*/
private static function validate_inputs( int $post_id, int $count ): void {
if ( $post_id < 1 ) {
throw new InvalidArgumentException( 'post_id must be >= 1.' );
}
if ( $count <= 0 ) {
throw new InvalidArgumentException( 'Count must be > 0.' );
}
if ( $count > self::MAX_COUNT ) {
$msg = 'Count exceeds MAX_COUNT (' . self::MAX_COUNT . ').';
throw new InvalidArgumentException( $msg ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
}
/**
* Check whether $post_id exists in wp_posts.
*
* @param int $post_id Post ID.
* @return bool
*/
private static function post_exists( int $post_id ): bool {
global $wpdb;
return (bool) $wpdb->get_var(
$wpdb->prepare( "SELECT 1 FROM {$wpdb->posts} WHERE ID = %d LIMIT 1", $post_id )
);
}
/**
* Lazy-built seed map for hp_review group.
*
* @return array<string,mixed>
*/
private static function seed_map(): array {
if ( null !== self::$seed_map_cache ) {
return self::$seed_map_cache;
}
self::$seed_map_cache = array(
'hp_rating' => static fn( int $i ) => (string) ( ( $i % 5 ) + 1 ),
);
return self::$seed_map_cache;
}
/**
* Names of all wp_wpdo_comment_* flat tables.
*
* @return string[]
*/
private static function get_comment_flat_tables(): array {
global $wpdb;
$prefix = $wpdb->prefix . 'wpdo_comment_';
return array(
$prefix . 'hp_review',
$prefix . 'misc',
);
}
/**
* Memoized table-exists probe.
*
* @param string $table Table name.
* @return bool
*/
private static function table_exists( string $table ): bool {
global $wpdb;
static $cache = array();
if ( isset( $cache[ $table ] ) ) {
return $cache[ $table ];
}
$found = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
$cache[ $table ] = ( $found === $table );
return $cache[ $table ];
}
/**
* Cheap row count helper (SQLite fallback).
*
* @param string $table Table name.
* @return int
*/
private static function table_row_count( string $table ): int {
global $wpdb;
if ( ! self::table_exists( $table ) ) {
return 0;
}
return (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
}
/**
* Time a SQL query.
*
* @param string $sql Query.
* @return array{duration_ms:float}
*/
private static function time_query( string $sql ): array {
global $wpdb;
$start = microtime( true );
$wpdb->get_results( $sql );
$elapsed_ms = ( microtime( true ) - $start ) * 1000;
return array( 'duration_ms' => round( $elapsed_ms, 2 ) );
}
/**
* Detect MySQL vs SQLite.
*
* @return bool
*/
private static function is_mysql(): bool {
return ! ( class_exists( 'WP_SQLite_DB' ) || class_exists( 'WP_SQLite_Translator' ) || class_exists( 'WP_SQLite_Driver' ) );
}
}