entity_type = $entity_type; } // ── Logging ─────────────────────────────────────────────────────────────── protected function log( array &$job, string $message ): void { $line = '[' . gmdate( 'H:i:s' ) . '] ' . $message; $job['log'][] = $line; if ( count( $job['log'] ) > self::MAX_LOG_LINES ) { $job['log'] = array_slice( $job['log'], -self::MAX_LOG_LINES ); } $job['updated_at'] = time(); } // ── Registry helpers ────────────────────────────────────────────────────── protected function get_managed_keys(): array { $keys = array(); foreach ( TMDO_Entity_Registry::get_groups_for_type( $this->entity_type ) as $group ) { $keys = array_merge( $keys, TMDO_Entity_Registry::get_group_keys( $this->entity_type, $group ) ); } return array_values( array_unique( $keys ) ); } protected function group_has_json_field( string $group ): bool { foreach ( TMDO_Entity_Registry::get_group_fields( $this->entity_type, $group ) as $field ) { if ( 'json' === ( $field['type'] ?? '' ) ) { return true; } } return false; } // ── DB helpers ──────────────────────────────────────────────────────────── /** Count EAV rows for the given meta keys (or all managed keys if empty). */ protected function count_eav_residue( array $keys = array() ): int { global $wpdb; if ( empty( $keys ) ) { $keys = $this->get_managed_keys(); } if ( empty( $keys ) ) { return 0; } $adapter = TMDO_Entity_Registry::get_adapter( $this->entity_type ); $meta_table = $adapter->get_native_meta_table(); $placeholders = implode( ',', array_fill( 0, count( $keys ), '%s' ) ); return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$meta_table}` WHERE meta_key IN ({$placeholders})", ...$keys ) ); } /** * Bulk pivot for a text-only group via INSERT … SELECT … GROUP BY … * ON DUPLICATE KEY UPDATE. * * @param string $group Entity group to pivot. * @return int Number of rows affected. * @throws \RuntimeException On DB failure. */ protected function execute_bulk_pivot( string $group ): int { global $wpdb; $fields = TMDO_Entity_Registry::get_group_fields( $this->entity_type, $group ); if ( empty( $fields ) ) { return 0; } $adapter = TMDO_Entity_Registry::get_adapter( $this->entity_type ); $id_col = $adapter->get_entity_id_column(); $meta_table = $adapter->get_native_meta_table(); $table = TMDO_Schema_Manager::get_table_name( $this->entity_type, $group ); $select_cases = array(); $update_cols = array(); $col_names = array(); $keys = array(); foreach ( $fields as $f ) { $key = $f['key']; $col = TMDO_Schema_Manager::sanitize_column_name( $key ); $keys[] = $key; $col_names[] = "`{$col}`"; $select_cases[] = $wpdb->prepare( "MAX(CASE WHEN um.meta_key = %s THEN um.meta_value END) AS `{$col}`", $key ); $update_cols[] = "`{$col}` = COALESCE(VALUES(`{$col}`), `{$col}`)"; } $placeholders = implode( ',', array_fill( 0, count( $keys ), '%s' ) ); $sql = sprintf( 'INSERT INTO `%s` (`%s`, %s) SELECT um.user_id, %s FROM `%s` um WHERE um.meta_key IN (%s) GROUP BY um.user_id ON DUPLICATE KEY UPDATE %s', $table, $id_col, implode( ', ', $col_names ), implode( ', ', $select_cases ), $meta_table, $placeholders, implode( ', ', $update_cols ) ); $result = $wpdb->query( $wpdb->prepare( $sql, ...$keys ) ); if ( false === $result ) { // phpcs:ignore WordPress.Security.EscapeOutput -- Exception message not echoed to browser. throw new \RuntimeException( 'Bulk pivot failed for group ' . $group . ': ' . $wpdb->last_error ); } return (int) $result; } /** Loose equality check for EAV vs flat value comparison (null/empty/scalar). */ protected function values_loose_equal( $a, $b ): bool { if ( null === $a && null === $b ) { return true; } if ( null === $a || null === $b ) { $other = null === $a ? $b : $a; return '' === $other || array() === $other || 0 === $other || '0' === $other; } if ( is_array( $a ) || is_array( $b ) ) { return wp_json_encode( $a ) === wp_json_encode( $b ); } return (string) $a === (string) $b; } }