feat(2meet-data-optimizer): support composite indexes on entity groups
- register_group() accepts $args['indexes'] (name => ordered keys); indexes on unknown keys are dropped. Kept outside the field definition, so schema hashes are unchanged. - Schema_Manager::ensure_group_indexes() adds missing cidx_* indexes to the existing flat table in place; the applied definition is remembered in tmdo_entity_indexes so later requests skip SHOW INDEX. - Bench (1M listings, scenario 2): Entity p50 337 ms -> 278 ms with (hp_verified, hp_price), on par with Hot (269 ms). - Verify: core unit 656 / integration 459 green, phpcs clean - PR: PR-027
This commit is contained in:
@@ -40,6 +40,9 @@ final class TMDO_Entity_Registry {
|
||||
/** @var array<string, array<string, string[]>> 實體類型 → 群組名 → 限定的 post_type */
|
||||
private static array $scopes = array();
|
||||
|
||||
/** @var array<string, array<string, array<string, string[]>>> 實體類型 → 群組名 → 索引名 → 欄位 key */
|
||||
private static array $indexes = array();
|
||||
|
||||
/** @var array<string, array> "entity/group" → 同名群組以不同定義重複註冊的紀錄 */
|
||||
private static array $collisions = array();
|
||||
|
||||
@@ -74,6 +77,7 @@ final class TMDO_Entity_Registry {
|
||||
self::$unscoped_index = array();
|
||||
self::$scoped_index = array();
|
||||
self::$scopes = array();
|
||||
self::$indexes = array();
|
||||
self::$pending_schemas = array();
|
||||
self::$collisions = array();
|
||||
self::$restored = array();
|
||||
@@ -119,6 +123,9 @@ final class TMDO_Entity_Registry {
|
||||
* 多個群組共用 meta_key 時,依 post 本身的類型決定歸屬;
|
||||
* 不屬於任何宣告 post_type 的 post 不受管理(走原生 meta)。
|
||||
* 不列入欄位定義,所以不影響 schema hash 與衝突比對。
|
||||
* indexes (array<string, string[]>) — 複合索引:名稱 → 欄位 key(依序);
|
||||
* 含未知欄位的索引會被略過。同樣不影響 schema hash,
|
||||
* 由 TMDO_Schema_Manager::ensure_group_indexes() 補建到既有表。
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
@@ -151,6 +158,13 @@ final class TMDO_Entity_Registry {
|
||||
if ( $post_types ) {
|
||||
self::$scopes[ $entity_type ][ $group_name ] = $post_types;
|
||||
}
|
||||
$keys = array_column( $normalized, 'key' );
|
||||
foreach ( (array) ( $args['indexes'] ?? array() ) as $index_name => $columns ) {
|
||||
$columns = array_values( array_map( 'strval', (array) $columns ) );
|
||||
if ( $columns && ! array_diff( $columns, $keys ) ) {
|
||||
self::$indexes[ $entity_type ][ $group_name ][ (string) $index_name ] = $columns;
|
||||
}
|
||||
}
|
||||
foreach ( $normalized as $normalized_field ) {
|
||||
self::index_field( $entity_type, $normalized_field, $post_types );
|
||||
}
|
||||
@@ -372,6 +386,15 @@ final class TMDO_Entity_Registry {
|
||||
return array_keys( self::$groups[ $entity_type ] ?? array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* 群組宣告的複合索引。
|
||||
*
|
||||
* @return array<string, string[]> 索引名 → 欄位 key
|
||||
*/
|
||||
public static function get_group_indexes( string $entity_type, string $group_name ): array {
|
||||
return self::$indexes[ $entity_type ][ $group_name ] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* 群組限定的 post_type(未限定時為空陣列)。
|
||||
*
|
||||
|
||||
@@ -20,6 +20,9 @@ defined( 'ABSPATH' ) || exit;
|
||||
|
||||
final class TMDO_Schema_Manager {
|
||||
|
||||
/** Option: "entity/group" → md5 of the composite index definition already applied. */
|
||||
public const INDEXES_OPTION = 'tmdo_entity_indexes';
|
||||
|
||||
/** Request-scoped cache for table_exists() — prevents repeated SHOW TABLES per request. */
|
||||
private static array $table_exists_cache = array();
|
||||
|
||||
@@ -60,6 +63,7 @@ final class TMDO_Schema_Manager {
|
||||
$schema['group'],
|
||||
$schema['fields']
|
||||
);
|
||||
self::ensure_group_indexes( $schema['type'], $schema['group'] );
|
||||
}
|
||||
|
||||
TMDO_Entity_Registry::clear_pending_schemas();
|
||||
@@ -164,6 +168,50 @@ final class TMDO_Schema_Manager {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the group's declared composite indexes that its table lacks.
|
||||
*
|
||||
* Indexes are not part of the schema hash, so an existing table (which
|
||||
* may already hold aeav_only data) gets them in place — no rebuild. The
|
||||
* applied definition is remembered per group (one autoloaded option), so
|
||||
* requests only run SHOW INDEX / ALTER when the declaration changes.
|
||||
*
|
||||
* @param string $entity_type Entity type.
|
||||
* @param string $group_name Group name.
|
||||
* @return void
|
||||
*/
|
||||
public static function ensure_group_indexes( string $entity_type, string $group_name ): void {
|
||||
global $wpdb;
|
||||
$declared = TMDO_Entity_Registry::get_group_indexes( $entity_type, $group_name );
|
||||
if ( empty( $declared ) ) {
|
||||
return;
|
||||
}
|
||||
$id = "{$entity_type}/{$group_name}";
|
||||
$signature = md5( (string) wp_json_encode( $declared ) );
|
||||
$applied = get_option( self::INDEXES_OPTION, array() );
|
||||
$applied = is_array( $applied ) ? $applied : array();
|
||||
if ( ( $applied[ $id ] ?? '' ) === $signature ) {
|
||||
return;
|
||||
}
|
||||
$table = self::get_table_name( $entity_type, $group_name );
|
||||
if ( ! self::table_exists( $table ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$existing = array_unique( array_column( (array) $wpdb->get_results( "SHOW INDEX FROM `{$table}`", ARRAY_A ), 'Key_name' ) );
|
||||
foreach ( $declared as $name => $keys ) {
|
||||
$index = 'cidx_' . self::sanitize_column_name( $name );
|
||||
if ( in_array( $index, $existing, true ) ) {
|
||||
continue;
|
||||
}
|
||||
$columns = implode( ', ', array_map( static fn( $key ) => '`' . self::sanitize_column_name( $key ) . '`', $keys ) );
|
||||
$wpdb->query( "ALTER TABLE `{$table}` ADD INDEX `{$index}` ({$columns})" );
|
||||
}
|
||||
|
||||
$applied[ $id ] = $signature;
|
||||
update_option( self::INDEXES_OPTION, $applied, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算 Schema Hash(用於偵測欄位變動)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user