'decimal(12,2) NOT NULL DEFAULT 0', 'hp_featured' => 'tinyint(1) NOT NULL DEFAULT 0', 'hp_verified' => 'tinyint(1) NOT NULL DEFAULT 0', 'hp_vendor' => 'bigint(20) NOT NULL DEFAULT 0', 'hp_expired_time' => 'bigint(20) NOT NULL DEFAULT 0', ); private ?mysqli $db; private string $prefix; private int $vendors; private int $listings; private int $seed; private int $batch; /** @var array Pending multi-row INSERTs per table. */ private array $buffers = array(); /** * Refuse to run against anything that is not a dedicated bench database. * * @throws InvalidArgumentException When the name does not end in `_bench`. */ public static function assert_bench_db( string $db_name ): void { if ( ! preg_match( '/^[A-Za-z0-9_]+_bench$/', $db_name ) ) { throw new InvalidArgumentException( "Refusing to seed '{$db_name}': database name must end in _bench." ); } } /** * @throws InvalidArgumentException When a count is out of range. */ public static function assert_count( string $label, int $value ): void { if ( $value < 1 || $value > self::MAX_ROWS ) { throw new InvalidArgumentException( "--{$label} must be between 1 and " . self::MAX_ROWS . ", got {$value}." ); } } public function __construct( ?mysqli $db, string $prefix, int $vendors, int $listings, int $seed = 42, int $batch = 2000 ) { $this->db = $db; $this->prefix = $prefix; $this->vendors = $vendors; $this->listings = $listings; $this->seed = $seed; $this->batch = max( 1, $batch ); } // ── Deterministic generators (pure — unit tested) ─────────────────────── private function det( int $i, string $key, int $mod ): int { return crc32( "{$this->seed}:{$i}:{$key}" ) % $mod; } /** 1-based vendor index (1..vendors) that owns listing $i. */ public function listing_vendor( int $i ): int { return $this->det( $i, 'vendor', $this->vendors ) + 1; } /** 90% publish, 5% draft, 3% pending, 2% private. */ public function listing_status( int $i ): string { $r = $this->det( $i, 'status', 100 ); if ( $r < 90 ) { return 'publish'; } if ( $r < 95 ) { return 'draft'; } return $r < 98 ? 'pending' : 'private'; } /** * Meta a HivePress listing carries. Keys absent from the array are not stored natively. * * @return array */ public function listing_meta( int $i, int $vendor_post_id ): array { $featured = $this->det( $i, 'featured', 100 ) < 5 ? 1 : 0; $meta = array( 'hp_price' => sprintf( '%d.00', 100 + $this->det( $i, 'price', 99901 ) ), 'hp_status' => $this->listing_status( $i ), 'hp_featured' => $featured, 'hp_verified' => $this->det( $i, 'verified', 100 ) < 30 ? 1 : 0, 'hp_vendor' => $vendor_post_id, 'hp_expired_time' => self::BASE_TS + $this->det( $i, 'expire', 365 ) * 86400, 'hp_view_count' => $this->det( $i, 'views', 5000 ), 'hp_rating' => sprintf( '%.2f', 1 + $this->det( $i, 'rating', 401 ) / 100 ), 'hp_rating_count' => $this->det( $i, 'rating_count', 200 ), ); if ( $featured ) { $meta['hp_featured_time'] = self::BASE_TS + $this->det( $i, 'featured_until', 90 ) * 86400; } return $meta; } /** @return array */ public function vendor_meta( int $v ): array { return array( 'hp_user' => $v, 'hp_verified' => $this->det( $v, 'v_verified', 100 ) < 40 ? 1 : 0, 'hp_hourly_rate' => sprintf( '%d.00', 20 + $this->det( $v, 'rate', 281 ) ), 'hp_rating' => sprintf( '%.2f', 1 + $this->det( $v, 'v_rating', 401 ) / 100 ), 'hp_rating_count' => $this->det( $v, 'v_rating_count', 500 ), ); } /** @return array */ public function user_meta( int $v ): array { return array( 'first_name' => 'Vendor', 'last_name' => (string) $v, 'nickname' => "vendor_{$v}", 'description' => "Bench vendor {$v}", 'hp_verified' => $this->det( $v, 'u_verified', 100 ) < 40 ? 1 : 0, 'hp_image' => (string) ( 100000 + $v ), ); } // ── Seeding ───────────────────────────────────────────────────────────── /** * Drop and rebuild every bench table, then load the dataset. * * @return array Row counts per table. */ public function run(): array { $this->exec( 'SET SESSION unique_checks = 0' ); $this->exec( 'SET SESSION foreign_key_checks = 0' ); $this->create_core_tables(); $this->create_flat_tables(); $this->seed_vendors(); $this->seed_listings(); $counts = array(); foreach ( array_keys( $this->table_names() ) as $table ) { $counts[ $table ] = (int) $this->db->query( "SELECT COUNT(*) FROM `{$table}`" )->fetch_row()[0]; } return $counts; } /** @return array Every table this seeder owns. */ private function table_names(): array { $p = $this->prefix; return array_fill_keys( array( "{$p}users", "{$p}usermeta", "{$p}posts", "{$p}postmeta", "{$p}wpdo_user_hp_user", "{$p}wpdo_post_hp_vendor_core", "{$p}wpdo_post_hp_listing_core", "{$p}wpdo_hot_hp_listing", ), true ); } /** * Core tables with the columns and indexes WordPress 6.x uses for these queries. */ private function create_core_tables(): void { $p = $this->prefix; foreach ( array( 'users', 'usermeta', 'posts', 'postmeta' ) as $t ) { $this->exec( "DROP TABLE IF EXISTS `{$p}{$t}`" ); } $this->exec( "CREATE TABLE `{$p}users` ( ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_login varchar(60) NOT NULL DEFAULT '', user_nicename varchar(50) NOT NULL DEFAULT '', user_email varchar(100) NOT NULL DEFAULT '', user_registered datetime NOT NULL DEFAULT '0000-00-00 00:00:00', display_name varchar(250) NOT NULL DEFAULT '', PRIMARY KEY (ID), KEY user_login_key (user_login), KEY user_nicename (user_nicename), KEY user_email (user_email) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci" ); $this->exec( "CREATE TABLE `{$p}usermeta` ( umeta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned NOT NULL DEFAULT 0, meta_key varchar(255) DEFAULT NULL, meta_value longtext, PRIMARY KEY (umeta_id), KEY user_id (user_id), KEY meta_key (meta_key(191)) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci" ); $this->exec( "CREATE TABLE `{$p}posts` ( ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_author bigint(20) unsigned NOT NULL DEFAULT 0, post_date datetime NOT NULL DEFAULT '0000-00-00 00:00:00', post_title text NOT NULL, post_status varchar(20) NOT NULL DEFAULT 'publish', post_name varchar(200) NOT NULL DEFAULT '', post_parent bigint(20) unsigned NOT NULL DEFAULT 0, post_type varchar(20) NOT NULL DEFAULT 'post', PRIMARY KEY (ID), KEY post_name (post_name(191)), KEY type_status_date (post_type,post_status,post_date,ID), KEY post_parent (post_parent), KEY post_author (post_author) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci" ); $this->exec( "CREATE TABLE `{$p}postmeta` ( meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_id bigint(20) unsigned NOT NULL DEFAULT 0, meta_key varchar(255) DEFAULT NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY post_id (post_id), KEY meta_key (meta_key(191)) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci" ); } /** * Flat tables are created by TMDO's own code so the bench measures the real schema. */ private function create_flat_tables(): void { TMDO_Entity_Registry::init(); TMDO_Entity_Registry::register_adapter( 'post', new TMDO_Adapter_Post() ); TMDO_Entity_Registry::register_adapter( 'user', new TMDO_Adapter_User() ); TMDO_Post_Fields::register_entity_fields(); TMDO_Member_Fields::register(); // Schema_Manager keeps a schema hash per group in wpdo_registry_meta and skips // creation when it matches, so a rebuilt table needs its hash row cleared too. TMDO_Installer::install_v2_tables(); $registry_meta = $this->prefix . TMDO_TABLE_PREFIX . 'registry_meta'; foreach ( array( array( 'post', 'hp_listing_core' ), array( 'post', 'hp_vendor_core' ), array( 'user', 'hp_user' ) ) as [ $type, $group ] ) { $this->exec( 'DROP TABLE IF EXISTS `' . TMDO_Schema_Manager::get_table_name( $type, $group ) . '`' ); $this->exec( "DELETE FROM `{$registry_meta}` WHERE entity_type = '{$type}' AND group_name = '{$group}'" ); if ( ! TMDO_Schema_Manager::create_or_upgrade_table( $type, $group, TMDO_Entity_Registry::get_group_fields( $type, $group ) ) ) { throw new RuntimeException( "Failed to create entity table {$type}/{$group}." ); } } $this->exec( "DROP TABLE IF EXISTS `{$this->prefix}wpdo_hot_hp_listing`" ); TMDO_Installer::create_hot_table( 'hp_listing', self::HOT_COLUMNS ); } private function seed_vendors(): void { $p = $this->prefix; for ( $v = 1; $v <= $this->vendors; $v++ ) { $this->push( "{$p}users", array( 'ID', 'user_login', 'user_nicename', 'user_email', 'user_registered', 'display_name' ), array( $v, "vendor_{$v}", "vendor-{$v}", "vendor{$v}@bench.invalid", '2026-01-01 00:00:00', "Vendor {$v}" ) ); $user_meta = $this->user_meta( $v ); foreach ( $user_meta as $key => $value ) { $this->push( "{$p}usermeta", array( 'user_id', 'meta_key', 'meta_value' ), array( $v, $key, $value ) ); } $this->push_entity( 'user', 'hp_user', $v, $user_meta ); // Vendor post IDs are 1..vendors; listing post IDs follow. $this->push( "{$p}posts", array( 'ID', 'post_author', 'post_date', 'post_title', 'post_status', 'post_name', 'post_parent', 'post_type' ), array( $v, $v, '2026-01-01 00:00:00', "Vendor {$v}", 'publish', "vendor-{$v}", 0, 'hp_vendor' ) ); $vendor_meta = $this->vendor_meta( $v ); foreach ( $vendor_meta as $key => $value ) { $this->push( "{$p}postmeta", array( 'post_id', 'meta_key', 'meta_value' ), array( $v, $key, $value ) ); } $this->push_entity( 'post', 'hp_vendor_core', $v, $vendor_meta ); } $this->flush_all(); } private function seed_listings(): void { $p = $this->prefix; for ( $i = 1; $i <= $this->listings; $i++ ) { $post_id = $this->vendors + $i; $vendor_id = $this->listing_vendor( $i ); $meta = $this->listing_meta( $i, $vendor_id ); $date = gmdate( 'Y-m-d H:i:s', self::BASE_TS + $this->det( $i, 'date', 365 * 86400 ) ); $this->push( "{$p}posts", array( 'ID', 'post_author', 'post_date', 'post_title', 'post_status', 'post_name', 'post_parent', 'post_type' ), array( $post_id, $vendor_id, $date, "Listing {$i}", $meta['hp_status'], "listing-{$i}", $vendor_id, 'hp_listing' ) ); foreach ( $meta as $key => $value ) { $this->push( "{$p}postmeta", array( 'post_id', 'meta_key', 'meta_value' ), array( $post_id, $key, $value ) ); } $this->push_entity( 'post', 'hp_listing_core', $post_id, $meta ); $hot = array( 'post_id' => $post_id ); foreach ( array_keys( self::HOT_COLUMNS ) as $col ) { $hot[ $col ] = $meta[ $col ] ?? 0; } $this->push( "{$p}wpdo_hot_hp_listing", array_keys( $hot ), array_values( $hot ) ); } $this->flush_all(); } /** * One entity-table row per entity; registry keys without a value are stored as NULL. * * @param array $meta */ private function push_entity( string $type, string $group, int $entity_id, array $meta ): void { $id_col = TMDO_Entity_Registry::get_adapter( $type )->get_entity_id_column(); $cols = array( $id_col ); $vals = array( $entity_id ); foreach ( TMDO_Entity_Registry::get_group_keys( $type, $group ) as $key ) { $cols[] = TMDO_Schema_Manager::sanitize_column_name( $key ); $vals[] = $meta[ $key ] ?? null; } $this->push( TMDO_Schema_Manager::get_table_name( $type, $group ), $cols, $vals ); } /** * @param string[] $cols * @param array $vals */ private function push( string $table, array $cols, array $vals ): void { $escaped = array_map( fn( $v ) => null === $v ? 'NULL' : ( is_int( $v ) ? (string) $v : "'" . $this->db->real_escape_string( (string) $v ) . "'" ), $vals ); $this->buffers[ $table ]['cols'] = $cols; $this->buffers[ $table ]['rows'][] = '(' . implode( ',', $escaped ) . ')'; if ( count( $this->buffers[ $table ]['rows'] ) >= $this->batch ) { $this->flush( $table ); } } private function flush( string $table ): void { if ( empty( $this->buffers[ $table ]['rows'] ) ) { return; } $cols = '`' . implode( '`,`', $this->buffers[ $table ]['cols'] ) . '`'; $this->exec( "INSERT INTO `{$table}` ({$cols}) VALUES " . implode( ',', $this->buffers[ $table ]['rows'] ) ); $this->buffers[ $table ]['rows'] = array(); } private function flush_all(): void { foreach ( array_keys( $this->buffers ) as $table ) { $this->flush( $table ); } } private function exec( string $sql ): void { if ( ! $this->db->query( $sql ) ) { throw new RuntimeException( 'Bench seed query failed: ' . $this->db->error . ' — ' . substr( $sql, 0, 200 ) ); } } }