, demoted: bool} */ public static function run( string $entity_type, bool $dry_run = false, int $batch = 500, bool $demote = false ): array { global $wpdb; $report = array( 'groups' => array(), 'demoted' => false, ); $adapter = TMDO_Entity_Registry::get_adapter( $entity_type ); if ( ! $adapter ) { return $report; } $id_col = $adapter->get_entity_id_column(); $batch = max( 1, $batch ); $started = get_option( self::STARTED_OPTION, array() ); $started = is_array( $started ) ? $started : array(); if ( ! $dry_run && ! isset( $started[ $entity_type ] ) ) { $started[ $entity_type ] = (string) $wpdb->get_var( 'SELECT NOW()' ); update_option( self::STARTED_OPTION, $started, false ); } $cursor = get_option( self::CURSOR_OPTION, array() ); $cursor = is_array( $cursor ) ? $cursor : array(); $tables = self::group_tables( $entity_type ); foreach ( $tables as $group => $table ) { $entry = array( 'rows' => 0, 'written' => 0, ); $key = "{$entity_type}/{$group}"; $last = $dry_run ? 0 : (int) ( $cursor[ $key ] ?? 0 ); do { $rows = self::fetch( $entity_type, $table, $id_col, "t.`{$id_col}` > %d ORDER BY t.`{$id_col}` LIMIT %d", array( $last, $batch ) ); $fetched = count( $rows ); $entry['rows'] += $fetched; if ( $fetched ) { $last = (int) end( $rows )[ $id_col ]; if ( ! $dry_run ) { $entry['written'] += self::write_rows( $entity_type, $group, $adapter, $rows ); $cursor[ $key ] = $last; update_option( self::CURSOR_OPTION, $cursor, false ); } } } while ( $fetched === $batch ); $report['groups'][ $group ] = $entry; } if ( $dry_run ) { return $report; } // Catch-up: writes that landed in the flat tables after a batch passed their row. foreach ( $tables as $group => $table ) { $rows = self::fetch( $entity_type, $table, $id_col, 't.updated_at >= %s', array( $started[ $entity_type ] ) ); self::write_rows( $entity_type, $group, $adapter, $rows ); } foreach ( array_keys( $tables ) as $group ) { unset( $cursor[ "{$entity_type}/{$group}" ] ); } unset( $started[ $entity_type ] ); $cursor ? update_option( self::CURSOR_OPTION, $cursor, false ) : delete_option( self::CURSOR_OPTION ); $started ? update_option( self::STARTED_OPTION, $started, false ) : delete_option( self::STARTED_OPTION ); if ( $demote ) { $report['demoted'] = true === TMDO_Mode_Manager::set( $entity_type, TMDO_Mode_Manager::MODE_DUAL_WRITE ); } return $report; } /** * Registered groups of the entity whose flat table exists. * * @param string $entity_type Entity type. * @return array Group → table name. */ private static function group_tables( string $entity_type ): array { $tables = array(); foreach ( TMDO_Entity_Registry::get_groups_for_type( $entity_type ) as $group ) { $table = TMDO_Schema_Manager::get_table_name( $entity_type, $group ); if ( TMDO_Schema_Manager::table_exists( $table ) ) { $tables[ $group ] = $table; } } return $tables; } /** * Flat rows matching $where; post rows carry their post type as __post_type. * * @param string $entity_type Entity type. * @param string $table Flat table. * @param string $id_col Entity id column. * @param string $where WHERE clause with placeholders. * @param array $params Placeholder values. * @return array> */ private static function fetch( string $entity_type, string $table, string $id_col, string $where, array $params ): array { global $wpdb; $select = 'post' === $entity_type ? "SELECT t.*, p.post_type AS __post_type FROM `{$table}` t LEFT JOIN `{$wpdb->posts}` p ON p.ID = t.`{$id_col}`" : "SELECT t.* FROM `{$table}` t"; return (array) $wpdb->get_results( $wpdb->prepare( "{$select} WHERE {$where}", ...$params ), ARRAY_A ); } /** * Replace the native meta of the keys this group owns for each row. * * @param string $entity_type Entity type. * @param string $group Group the rows come from. * @param TMDO_Entity_Adapter_Interface $adapter Entity adapter. * @param array $rows Flat rows. * @return int Native values written. */ private static function write_rows( string $entity_type, string $group, TMDO_Entity_Adapter_Interface $adapter, array $rows ): int { global $wpdb; if ( empty( $rows ) ) { return 0; } $meta_table = $adapter->get_native_meta_table(); $id_col = $adapter->get_entity_id_column(); $deletes = array(); $inserts = array(); foreach ( $rows as $row ) { $id = (int) $row[ $id_col ]; $post_type = 'post' === $entity_type ? (string) ( $row['__post_type'] ?? '' ) : ''; foreach ( TMDO_Entity_Registry::get_group_fields( $entity_type, $group ) as $field ) { $owner = TMDO_Entity_Registry::get_field( $entity_type, $field['key'], $post_type ); $col = TMDO_Schema_Manager::sanitize_column_name( $field['key'] ); if ( null === $owner || $owner['group'] !== $group || ! array_key_exists( $col, $row ) ) { continue; } $deletes[ $field['key'] ][] = $id; if ( null !== $row[ $col ] ) { $inserts[] = array( $id, $field['key'], (string) maybe_serialize( TMDO_Type_Caster::from_db( $row[ $col ], $field ) ) ); } } } foreach ( $deletes as $meta_key => $ids ) { $in = implode( ',', array_map( 'intval', $ids ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM `{$meta_table}` WHERE meta_key = %s AND `{$id_col}` IN ({$in})", $meta_key ) ); } if ( $inserts ) { $values = implode( ', ', array_fill( 0, count( $inserts ), '(%d, %s, %s)' ) ); $wpdb->query( $wpdb->prepare( "INSERT INTO `{$meta_table}` (`{$id_col}`, meta_key, meta_value) VALUES {$values}", ...array_merge( ...$inserts ) ) ); } foreach ( array_unique( array_map( static fn( $row ) => (int) $row[ $id_col ], $rows ) ) as $id ) { wp_cache_delete( $id, "{$entity_type}_meta" ); } return count( $inserts ); } }