diff --git a/.github/skills/architecture/hook-architecture.md b/.github/skills/architecture/hook-architecture.md index a06951f..ce87d06 100644 --- a/.github/skills/architecture/hook-architecture.md +++ b/.github/skills/architecture/hook-architecture.md @@ -45,8 +45,8 @@ ### Header Hooks -| Hook | Parameters | Purpose | -| --------------------------------------- | ----------------------------- | -------------------------------------------------------- | +| Hook | Parameters | Purpose | +| -------------------------------------------------- | ----------------------------- | -------------------------------------------------------- | | `fe_csv_import_export_basic_post_fields` | `$fields, $scope` | Customize basic export fields (ID, title, content, etc.) | | `fe_csv_import_export_additional_post_fields` | `$fields, $scope` | Customize 'all' scope additional fields | | `fe_csv_import_export_filter_taxonomy_objects` | `$taxonomy_objects, $args` | Filter taxonomy objects before header creation | @@ -54,34 +54,35 @@ ### Query Hooks -| Hook | Parameters | Purpose | -| ----------------------------- | -------------------- | ---------------------------------------------- | +| Hook | Parameters | Purpose | +| ---------------------------------------- | -------------------- | ---------------------------------------------- | | `fe_csv_import_export_sample_query_args` | `$query_args, $args` | Customize sample post query for meta discovery | | `fe_csv_import_export_export_query_args` | `$query_args, $args` | Customize main export query | ### Import Hooks -| Hook | Parameters | Purpose | -| ---------------------------- | ------------------------- | -------------------------------------- | -| `fe_csv_import_export_pre_ajax_import` | `$result, $_POST` | Preflight import request validation | -| `fe_csv_import_export_user_can_import` | `$allowed` | Override import capability checks | -| `fe_csv_import_export_before_import` | `$args` | Pre-import preparation | -| `fe_csv_import_export_import_row` | `$row_data, $args` | Per-row data transformation | -| `fe_csv_import_export_after_import` | `$stats, $args` | Post-import cleanup | -| `fe_csv_import_export_cancelled_import` | `$cancelled, $session_id` | Override import cancellation detection | +| Hook | Parameters | Purpose | +| -------------------------------------------- | --------------------------------------- | -------------------------------------------------- | +| `fe_csv_import_export_pre_ajax_import` | `$result, $_POST` | Preflight import request validation | +| `fe_csv_import_export_user_can_import` | `$allowed` | Override import capability checks | +| `fe_csv_import_export_before_import` | `$args` | Pre-import preparation | +| `fe_csv_import_export_import_row` | `$row_data, $args` | Per-row data transformation | +| `fe_csv_import_export_after_import` | `$stats, $args` | Post-import cleanup | +| `fe_csv_import_export_cancelled_import` | `$cancelled, $session_id` | Override import cancellation detection | +| `fe_csv_import_export_import_max_batch_size` | `$max_batch_size, $total_rows, $config` | Customize maximum import batch size (default: 500) | ### Export Hooks -| Hook | Parameters | Purpose | -| ---------------------------- | ------------------------- | -------------------------------------- | +| Hook | Parameters | Purpose | +| --------------------------------------- | ------------------------- | -------------------------------------- | | `fe_csv_import_export_pre_ajax_export` | `$result, $_POST` | Preflight export request validation | | `fe_csv_import_export_user_can_export` | `$allowed` | Override export capability checks | | `fe_csv_import_export_cancelled_export` | `$cancelled, $session_id` | Override export cancellation detection | ### Import Processing Hooks -| Hook | Parameters | Purpose | -| ------------------------------------ | ----------------------------------- | ---------------------------------- | +| Hook | Parameters | Purpose | +| ----------------------------------------------- | ----------------------------------- | ---------------------------------- | | `fe_csv_import_export_import_row_context` | `$context, $row_data, $args` | Customize row context creation | | `fe_csv_import_export_import_persist_result` | `$result, $post_data, $context` | Modify post persistence result | | `fe_csv_import_export_taxonomy_term_resolution` | `$term_ids, $term_value, $taxonomy` | Customize taxonomy term resolution | diff --git a/AGENT.md b/AGENT.md index c15162e..23d2de6 100644 --- a/AGENT.md +++ b/AGENT.md @@ -8,7 +8,7 @@ **FE CSV Import & Export** is a WordPress plugin for CSV import/export. Supports custom post types, custom taxonomies, and custom fields. -- **Version**: 0.9.9 +- **Version**: 1.0.0 - **License**: GPL-2.0+ - **PHP**: >= 8.1 (recommended) - **Repository**: https://github.com/firstelementjp/fe-csv-import-export diff --git a/README.md b/README.md index 02bfddd..c37b436 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![FE CSV Import & Export Banner](https://github.com/firstelementjp/fe-csv-import-export/blob/main/assets/images/img-sns-banner-80.jpg?raw=true) -[![Version](https://img.shields.io/badge/version-0.9.9.4-green.svg)](https://github.com/firstelementjp/fe-csv-import-export/releases) +[![Version](https://img.shields.io/badge/version-1.0.0-green.svg)](https://github.com/firstelementjp/fe-csv-import-export/releases) [![License](https://img.shields.io/badge/License-GPLv2%2B-blue.svg)](https://www.gnu.org/licenses/gpl-2.0.html) [![WordPress](https://img.shields.io/badge/WordPress-6.6%2B-blue.svg)](https://wordpress.org/) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/firstelementjp/fe-csv-import-export) @@ -15,12 +15,11 @@ For end-user guides and full documentation, see the links below. ## ✨ Recent Highlights -- Bundled JavaScript files to reduce HTTP requests and improve page load performance -- Updated export batch planner to use memory_limit for dynamic batch size calculation -- Reduced memory usage for large CSV imports by streaming batch reads and reusing file offsets -- Improved import log tab behavior and preserved recent logs at completion -- Fixed CSV quote parsing for edge cases involving backslash-escaped double quotes in legacy data -- Cleaned up local release workflow so `test-release` and generated minified assets do not remain tracked in Git +- Taxonomy optimization for unchanged terms significantly reduces processing time for update-heavy imports +- Increased default import batch size from 100 to 500 rows to reduce AJAX overhead +- Added configurable batch size limit filter for per-site customization +- Detailed import profiling with WP_DEBUG batch profile logging for performance analysis +- Fixed JavaScript ES module import syntax to resolve browser compatibility issues ## 📖 Overview diff --git a/assets/js/fe-csv-import-export-main.js b/assets/js/fe-csv-import-export-main.js index 5f52aa6..a78d621 100644 --- a/assets/js/fe-csv-import-export-main.js +++ b/assets/js/fe-csv-import-export-main.js @@ -5,20 +5,6 @@ * */ -// Import core modules -import './fe-csv-import-export-core.js'; - -// Import export modules -import './export/fe-csv-import-export/ajax.js'; -import './export/fe-csv-import-export/download.js'; -import './export/fe-csv-import-export/form.js'; -import './export/fe-csv-import-export/ui.js'; -import './export/fe-csv-import-export/logs.js'; -import './fe-csv-import-export-export-unified.js'; -import './export/fe-csv-import-export/original.js'; -import './fe-csv-import-export-import.js'; -import './fe-csv-import-export-license.js'; - /** * Bootstrap FE CSV Import & Export admin scripts when the DOM is ready. */ diff --git a/docs/README.md b/docs/README.md index 159d17a..6635d3f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,7 +5,7 @@ > CSV import/export for WordPress with localized admin UI, automatic batch processing, and developer extensibility. [![License](https://img.shields.io/badge/license-GPL--2.0%2B-blue.svg)](legal.md) -[![Version](https://img.shields.io/badge/version-0.9.9.4-green.svg)](https://github.com/firstelementjp/fe-csv-import-export/releases) +[![Version](https://img.shields.io/badge/version-1.0.0-green.svg)](https://github.com/firstelementjp/fe-csv-import-export/releases) [![WordPress](https://img.shields.io/badge/WordPress-6.6%2B-blue.svg)](https://wordpress.org/) FE CSV Import & Export is a WordPress plugin for importing and exporting post data as CSV from the admin area. @@ -29,11 +29,11 @@ FE CSV Import & Export is a WordPress plugin for importing and exporting post da ## 📥 Download -[Download fe-csv-import-export-0.9.9.4.zip](https://github.com/firstelementjp/fe-csv-import-export/releases/download/v0.9.9.4/fe-csv-import-export-0.9.9.4.zip){: .download-btn } +[Download fe-csv-import-export-1.0.0.zip](https://github.com/firstelementjp/fe-csv-import-export/releases/download/v1.0.0/fe-csv-import-export-1.0.0.zip){: .download-btn } ## 🚀 Quick Start -1. Download `fe-csv-import-export-0.9.9.4.zip` +1. Download `fe-csv-import-export-1.0.0.zip` 2. Upload and activate the plugin in WordPress 3. Open **Tools → FE CSV Import & Export** 4. Run a small test import or export first diff --git a/docs/changes.md b/docs/changes.md index cabf724..137ae0c 100644 --- a/docs/changes.md +++ b/docs/changes.md @@ -1,5 +1,20 @@ # 📋 Changelog +## [1.0.0] - 2026-06-02 + +### 🚀 Performance Improvements + +- **Taxonomy optimization for unchanged terms** - Import now skips `wp_set_post_terms()` calls when taxonomy terms are unchanged, significantly reducing processing time for update-heavy imports. +- **Increased default batch size** - Default import batch size increased from 100 to 500 rows, reducing AJAX overhead for large imports. +- **Configurable batch size limit** - New `fe_csv_import_export_import_max_batch_size` filter allows per-site customization of the maximum batch size. +- **Detailed import profiling** - Added granular profiling for import phases (preload, row context, row processor, meta/taxonomy preparation and application) with WP_DEBUG batch profile logging. + +### 🛠️ Bug Fixes + +- **Fixed JavaScript ES module import syntax** - Removed static ES module imports from main JS file to resolve browser syntax errors. + +--- + ## [0.9.9.4] - 2026-05-27 ### 🚀 Performance Improvements diff --git a/docs/install.md b/docs/install.md index 004f4eb..65f8c05 100644 --- a/docs/install.md +++ b/docs/install.md @@ -10,7 +10,7 @@ ## Manual Installation -1. [Download latest version (v0.9.9.4)](https://github.com/firstelementjp/fe-csv-import-export/releases/download/v0.9.9.4/fe-csv-import-export-0.9.9.4.zip) +1. [Download latest version (v1.0.0)](https://github.com/firstelementjp/fe-csv-import-export/releases/download/v1.0.0/fe-csv-import-export-1.0.0.zip) 2. Extract the downloaded ZIP file 3. Upload `fe-csv-import-export` folder to `/wp-content/plugins/` 4. Navigate to **Plugins** in WordPress admin diff --git a/fe-csv-import-export.php b/fe-csv-import-export.php index 8715aa8..29fd955 100644 --- a/fe-csv-import-export.php +++ b/fe-csv-import-export.php @@ -3,7 +3,7 @@ * Plugin Name: FE CSV Import & Export * Plugin URI: https://github.com/firstelementjp/fe-csv-import-export * Description: Simple yet powerful CSV import/export plugin for WordPress. Supports custom post types, custom taxonomies, and custom fields. - * Version: 0.9.9.4 + * Version: 1.0.0 * Author: FirstElement K.K., Daijiro Miyazawa * Author URI: https://www.firstelement.co.jp/ * License: GPL-2.0+ @@ -20,7 +20,7 @@ } // Define plugin constants. -define( 'FE_CSV_IMPORT_EXPORT_VERSION', '0.9.9.4' ); +define( 'FE_CSV_IMPORT_EXPORT_VERSION', '1.0.0' ); define( 'FE_CSV_IMPORT_EXPORT_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'FE_CSV_IMPORT_EXPORT_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); define( 'FE_CSV_IMPORT_EXPORT_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/includes/admin/class-fe-csv-import-export-admin-assets.php b/includes/admin/class-fe-csv-import-export-admin-assets.php index d56cda5..ee463e1 100644 --- a/includes/admin/class-fe-csv-import-export-admin-assets.php +++ b/includes/admin/class-fe-csv-import-export-admin-assets.php @@ -73,8 +73,12 @@ public function enqueue_scripts( $hook ) { ? (bool) FE_CSV_Import_Export_Settings_Helper::get( 'advanced', 'enable_logs', true ) : true; + // Temporarily force individual files for both debug and production modes + // to resolve dialog opening issue in production mode + $force_individual_files = true; + // Use bundled script for production, individual files for debug mode - if ( $debug_mode ) { + if ( $debug_mode || $force_individual_files ) { // Debug mode: load individual files for easier debugging $script_url = static function ( $relative_path ) use ( $suffix ) { $min_path = preg_replace( '/\.js$/', $suffix . '.js', $relative_path ); @@ -315,7 +319,8 @@ public function enqueue_scripts( $hook ) { } // Localize script (use the main script handle based on mode) - $main_script_handle = $debug_mode ? 'fe-csv-import-export-core' : 'fe-csv-import-export-admin'; + // When forcing individual files, always use core handle + $main_script_handle = ( $debug_mode || $force_individual_files ) ? 'fe-csv-import-export-core' : 'fe-csv-import-export-admin'; wp_localize_script( $main_script_handle, diff --git a/includes/import/class-fe-csv-import-export-ajax-import-batch-planner.php b/includes/import/class-fe-csv-import-export-ajax-import-batch-planner.php index 0546077..286c2f8 100644 --- a/includes/import/class-fe-csv-import-export-ajax-import-batch-planner.php +++ b/includes/import/class-fe-csv-import-export-ajax-import-batch-planner.php @@ -29,17 +29,82 @@ class FE_CSV_Import_Export_Ajax_Import_Batch_Planner { * @return int Batch size. */ public function get_import_batch_size( int $total_rows, array $config ): int { - $max_execution_time = ini_get( 'max_execution_time' ); - $max_execution_time = $max_execution_time ? (int) $max_execution_time : 30; - $max_execution_time = max( 1, $max_execution_time ); + if ( $total_rows <= 0 ) { + return 1; + } + + $max_execution_time = $this->get_max_execution_time(); + $safe_time_limit = $max_execution_time * 0.8; + $avg_time_per_row = 0.02; + $time_based_batch = max( 1, (int) floor( $safe_time_limit / $avg_time_per_row ) ); - $safe_time_limit = $max_execution_time * 0.5; - $avg_time_per_row = 0.35; - $time_based_batch = max( 1, (int) floor( $safe_time_limit / $avg_time_per_row ) ); - $optimized_batch_size = max( 10, min( 50, $time_based_batch ) ); + $memory_limit = $this->convert_memory_limit_to_bytes( (string) ini_get( 'memory_limit' ) ); + $memory_limit = max( 64 * 1024 * 1024, $memory_limit ); + $available_memory = max( 16 * 1024 * 1024, $memory_limit - ( 48 * 1024 * 1024 ) ); + $memory_per_row = 128 * 1024; + $memory_based_batch = max( 1, (int) floor( $available_memory / $memory_per_row ) ); - $base_batch_size = max( 1, min( $total_rows, $optimized_batch_size ) ); + $optimized_batch_size = min( $time_based_batch, $memory_based_batch, $total_rows ); + $max_batch_size = (int) apply_filters( 'fe_csv_import_export_import_max_batch_size', 500, $total_rows, $config ); + $max_batch_size = max( 10, $max_batch_size ); + $base_batch_size = max( 10, min( $max_batch_size, $optimized_batch_size ) ); + $base_batch_size = min( $total_rows, $base_batch_size ); return (int) apply_filters( 'fe_csv_import_export_import_batch_size', $base_batch_size, $total_rows, $config ); } + + /** + * Get max execution time with a safe fallback. + * + * @since 0.9.8 + * @return int Max execution time in seconds. + */ + private function get_max_execution_time(): int { + $max_execution_time = ini_get( 'max_execution_time' ); + $max_execution_time = false === $max_execution_time ? 30 : (int) $max_execution_time; + + if ( $max_execution_time <= 0 ) { + return 120; + } + + return max( 1, $max_execution_time ); + } + + /** + * Convert memory limit string to bytes. + * + * @since 0.9.8 + * @param string $memory_limit Memory limit string. + * @return int Memory limit in bytes. + */ + private function convert_memory_limit_to_bytes( string $memory_limit ): int { + $memory_limit = strtoupper( trim( $memory_limit ) ); + + if ( '-1' === $memory_limit ) { + return PHP_INT_MAX; + } + + if ( '' === $memory_limit ) { + return 128 * 1024 * 1024; + } + + $unit = substr( $memory_limit, -1 ); + $value = (int) substr( $memory_limit, 0, -1 ); + + switch ( $unit ) { + case 'G': + $value *= 1024 * 1024 * 1024; + break; + case 'M': + $value *= 1024 * 1024; + break; + case 'K': + $value *= 1024; + break; + default: + $value = (int) $memory_limit; + } + + return $value > 0 ? $value : 128 * 1024 * 1024; + } } diff --git a/includes/import/class-fe-csv-import-export-import-batch-processor.php b/includes/import/class-fe-csv-import-export-import-batch-processor.php index 173723b..04cf89f 100644 --- a/includes/import/class-fe-csv-import-export-import-batch-processor.php +++ b/includes/import/class-fe-csv-import-export-import-batch-processor.php @@ -220,6 +220,32 @@ protected function initialize_counters(): array { 'errors' => 0, 'dry_run_log' => [], 'dry_run_details' => [], + 'import_profile' => $this->initialize_import_profile(), + ]; + } + + /** + * Initialize import profile counters. + * + * @since 0.9.9.5 + * @return array + */ + private function initialize_import_profile(): array { + return [ + 'preload' => 0.0, + 'row_context' => 0.0, + 'row_processor' => 0.0, + 'success_log' => 0.0, + 'success_counter_guid' => 0.0, + 'prepare_meta_tax' => 0.0, + 'compat_hooks' => 0.0, + 'meta_apply' => 0.0, + 'tax_apply' => 0.0, + 'tax_resolve' => 0.0, + 'tax_set_terms' => 0.0, + 'tax_set_terms_calls' => 0, + 'tax_set_terms_skipped' => 0, + 'rows_profiled' => 0, ]; } @@ -255,6 +281,7 @@ private function process_batch_import( array $config, array $csv_data, array &$c } if ( isset( $csv_data['batch_lines'] ) && is_array( $csv_data['batch_lines'] ) ) { + $preload_started_at = microtime( true ); $current_batch_data = array_map( function ( string $line ) use ( $csv_data ): array { return $this->get_csv_util()->parse_csv_row( $line, (string) $csv_data['delimiter'] ); @@ -270,6 +297,7 @@ function ( string $line ) use ( $csv_data ): array { 'post_type' => (string) ( $config['post_type'] ?? 'post' ), ] ); + $counters['import_profile']['preload'] += microtime( true ) - $preload_started_at; } wp_defer_term_counting( true ); @@ -322,11 +350,11 @@ protected function validate_id_column_or_send_error( array $config, array $csv_d * Run the batch loop. * * @since 0.9.8 - * @param wpdb $wpdb WordPress database handler. - * @param array $config Import configuration. - * @param array $csv_data Parsed CSV data. - * @param array $allowed_post_fields Allowed post fields. - * @param array $counters Counters (by reference). + * @param wpdb $wpdb WordPress database handler. + * @param array $config Import configuration. + * @param array $csv_data Parsed CSV data. + * @param array $allowed_post_fields Allowed post fields. + * @param array $counters Counters (by reference). * @param FE_CSV_Import_Export_Import_Row_Context $row_context_util Row context util. * @param FE_CSV_Import_Export_Import_Row_Processor $row_processor_util Row processor util. * @param FE_CSV_Import_Export_Import_Persister $persister_util Persister util. @@ -348,6 +376,7 @@ protected function process_batch_loop( $import_session = (string) ( $config['import_session'] ?? '' ); $batch_lines = isset( $csv_data['batch_lines'] ) && is_array( $csv_data['batch_lines'] ) ? $csv_data['batch_lines'] : []; $batch_count = count( $batch_lines ); + $batch_items = []; for ( $i = 0; $i < $batch_count; $i++ ) { if ( '' !== $import_session && $this->get_cancel_manager()->is_cancelled( $import_session ) ) { break; @@ -363,25 +392,30 @@ protected function process_batch_loop( $row_context_util, $row_processor_util, $persister_util, - $meta_tax_util + $meta_tax_util, + $batch_items ); } + + $context = $this->build_row_processing_context( $config, $csv_data, $csv_data['headers'], $allowed_post_fields ); + $meta_tax_util->apply_prepared_meta_and_taxonomies_for_batch( $wpdb, $batch_items, $context, $counters['dry_run_log'], $counters['import_profile'] ); } /** * Import loop iteration logic * * @since 0.9.8 - * @param wpdb $wpdb WordPress database handler. - * @param array $config Import configuration. - * @param array $csv_data Parsed CSV data. - * @param array $allowed_post_fields Allowed post fields. - * @param int $index Row index. - * @param array $counters Counters (by reference). + * @param wpdb $wpdb WordPress database handler. + * @param array $config Import configuration. + * @param array $csv_data Parsed CSV data. + * @param array $allowed_post_fields Allowed post fields. + * @param int $index Row index. + * @param array $counters Counters (by reference). * @param FE_CSV_Import_Export_Import_Row_Context $row_context_util Row context util. * @param FE_CSV_Import_Export_Import_Row_Processor $row_processor_util Row processor util. * @param FE_CSV_Import_Export_Import_Persister $persister_util Persister util. * @param FE_CSV_Import_Export_Import_Meta_Tax $meta_tax_util Meta/tax util. + * @param array $batch_items Prepared batch items. * @return void */ private function process_import_loop_iteration( @@ -394,7 +428,8 @@ private function process_import_loop_iteration( FE_CSV_Import_Export_Import_Row_Context $row_context_util, FE_CSV_Import_Export_Import_Row_Processor $row_processor_util, FE_CSV_Import_Export_Import_Persister $persister_util, - FE_CSV_Import_Export_Import_Meta_Tax $meta_tax_util + FE_CSV_Import_Export_Import_Meta_Tax $meta_tax_util, + array &$batch_items ): void { $lines = $csv_data['batch_lines']; $delimiter = $csv_data['delimiter']; @@ -408,39 +443,41 @@ private function process_import_loop_iteration( return; } - // Use Ajax_Import's original row processing logic. - $row_context = $this->build_import_row_context_from_config( $wpdb, $row_context_util, $config, $line, $delimiter, $headers, $allowed_post_fields, $parsed_data ); + $row_context_started_at = microtime( true ); + $row_context = $this->build_import_row_context_from_config( $wpdb, $row_context_util, $config, $line, $delimiter, $headers, $allowed_post_fields, $parsed_data ); + $counters['import_profile']['row_context'] += microtime( true ) - $row_context_started_at; if ( null === $row_context ) { return; } $context = $this->build_row_processing_context( $config, $csv_data, $headers, $allowed_post_fields ); - // Use the original row processor utility. + $row_processor_started_at = microtime( true ); $row_processor_util->process_row_context_with_persister( $wpdb, $row_context, $context, $counters, $persister_util, - function ( wpdb $wpdb, int $post_id, bool $is_update, array $context, array &$counters ) use ( $meta_tax_util ): void { - $this->handle_successful_row_import( $wpdb, $post_id, $is_update, $context, $counters, $meta_tax_util ); + function ( wpdb $wpdb, int $post_id, bool $is_update, array $context, array &$counters ) use ( $meta_tax_util, &$batch_items ): void { + $this->handle_successful_row_import( $wpdb, $post_id, $is_update, $context, $counters, $meta_tax_util, $batch_items ); } ); + $counters['import_profile']['row_processor'] += microtime( true ) - $row_processor_started_at; } /** * Row context building logic * * @since 0.9.8 - * @param wpdb $wpdb WordPress database handler. + * @param wpdb $wpdb WordPress database handler. * @param FE_CSV_Import_Export_Import_Row_Context $row_context_util Row context util. - * @param array $config Import configuration. - * @param string $line CSV line. - * @param string $delimiter CSV delimiter. - * @param array $headers CSV headers. - * @param array $allowed_post_fields Allowed post fields. - * @param array $parsed_data Parsed CSV row data. + * @param array $config Import configuration. + * @param string $line CSV line. + * @param string $delimiter CSV delimiter. + * @param array $headers CSV headers. + * @param array $allowed_post_fields Allowed post fields. + * @param array $parsed_data Parsed CSV row data. * @return array|null Row context or null if invalid. */ protected function build_import_row_context_from_config( wpdb $wpdb, FE_CSV_Import_Export_Import_Row_Context $row_context_util, array $config, string $line, string $delimiter, array $headers, array $allowed_post_fields, array $parsed_data = [] ): ?array { @@ -484,22 +521,22 @@ protected function build_row_processing_context( array $config, array $csv_data, * Successful row import handling logic. * * @since 0.9.8 - * @param wpdb $wpdb WordPress database object. - * @param int $post_id Post ID. - * @param bool $is_update Whether this was an update. - * @param array $context Row processing context. - * @param array $counters Counters (by reference). + * @param wpdb $wpdb WordPress database object. + * @param int $post_id Post ID. + * @param bool $is_update Whether this was an update. + * @param array $context Row processing context. + * @param array $counters Counters (by reference). * @param FE_CSV_Import_Export_Import_Meta_Tax $meta_tax_util Meta/tax util. + * @param array $batch_items Prepared batch items. * @return void */ - protected function handle_successful_row_import( wpdb $wpdb, int $post_id, bool $is_update, array $context, array &$counters, FE_CSV_Import_Export_Import_Meta_Tax $meta_tax_util ): void { - $headers = $context['headers']; - $data = $context['data']; - $allowed_post_fields = $context['allowed_post_fields']; - $taxonomy_format = $context['taxonomy_format']; - $taxonomy_format_validation = $context['taxonomy_format_validation']; - $dry_run = $context['dry_run']; - $should_generate_logs = isset( $context['append_log'] ) && is_callable( $context['append_log'] ); + protected function handle_successful_row_import( wpdb $wpdb, int $post_id, bool $is_update, array $context, array &$counters, FE_CSV_Import_Export_Import_Meta_Tax $meta_tax_util, array &$batch_items ): void { + $headers = $context['headers']; + $data = $context['data']; + $allowed_post_fields = $context['allowed_post_fields']; + $dry_run = $context['dry_run']; + $should_generate_logs = isset( $context['append_log'] ) && is_callable( $context['append_log'] ); + $success_log_started_at = microtime( true ); if ( $should_generate_logs ) { $row_number = $context['start_row'] + $counters['processed'] + 1; // Correct row number from context. @@ -526,25 +563,26 @@ protected function handle_successful_row_import( wpdb $wpdb, int $post_id, bool call_user_func( $context['append_log'], $detail ); } + $counters['import_profile']['success_log'] += microtime( true ) - $success_log_started_at; + $success_counter_started_at = microtime( true ); $this->get_row_processor_util()->apply_success_counters_and_guid_without_callbacks( $wpdb, $post_id, $is_update, $counters ); + $counters['import_profile']['success_counter_guid'] += microtime( true ) - $success_counter_started_at; - $result = $meta_tax_util->process_meta_and_taxonomies_for_row_with_args( - $wpdb, + $prepare_started_at = microtime( true ); + $result = $meta_tax_util->prepare_meta_and_taxonomies_for_row_with_args( $post_id, $headers, $data, - $allowed_post_fields, - $taxonomy_format, - $taxonomy_format_validation, - $dry_run, - $counters + $allowed_post_fields ); + $batch_items[] = $result; + $counters['import_profile']['prepare_meta_tax'] += microtime( true ) - $prepare_started_at; $prepare_args = [ 'headers' => $headers, @@ -553,11 +591,14 @@ protected function handle_successful_row_import( wpdb $wpdb, int $post_id, bool 'post_type' => $context['post_type'] ?? 'post', 'dry_run' => $dry_run, ]; + $hooks_started_at = microtime( true ); $prepared_meta_fields = apply_filters( 'fe_csv_import_export_prepare_import_fields', $result['meta_fields'], $post_id, $prepare_args ); do_action( 'fe_csv_import_export_import_phase_map_prepared', $post_id, $prepared_meta_fields, $prepare_args ); do_action( 'fe_csv_import_export_import_phase_post_persist', $post_id, $prepared_meta_fields, $prepare_args ); do_action( 'fe_csv_import_export_process_custom_fields', $post_id, $prepared_meta_fields ); + $counters['import_profile']['compat_hooks'] += microtime( true ) - $hooks_started_at; + ++$counters['import_profile']['rows_profiled']; } /** diff --git a/includes/import/class-fe-csv-import-export-import-meta-tax.php b/includes/import/class-fe-csv-import-export-import-meta-tax.php index 22ccef5..e7d383b 100644 --- a/includes/import/class-fe-csv-import-export-import-meta-tax.php +++ b/includes/import/class-fe-csv-import-export-import-meta-tax.php @@ -178,6 +178,83 @@ public function process_meta_and_taxonomies_for_row_with_args( ]; } + /** + * Prepare meta fields and taxonomies for a post using explicit arguments. + * + * @since 0.9.0 + * @param int $post_id Post ID. + * @param array $headers CSV headers. + * @param array $data CSV row data. + * @param array $allowed_post_fields Allowed WP post fields. + * @return array{ + * post_id: int, + * meta_fields: array, + * taxonomies: array>, + * context_data: array + * } + */ + public function prepare_meta_and_taxonomies_for_row_with_args( + int $post_id, + array $headers, + array $data, + array $allowed_post_fields + ): array { + $collected_fields = $this->collect_taxonomies_and_meta_fields_from_row( $headers, $data, $allowed_post_fields ); + + $collected_fields = apply_filters( + 'fe_csv_import_export_import_field_mapping', + $collected_fields, + $headers, + $data, + $allowed_post_fields + ); + do_action( + 'fe_csv_import_export_import_phase_map', + $collected_fields, + $headers, + $data, + $allowed_post_fields + ); + + return [ + 'post_id' => $post_id, + 'meta_fields' => $collected_fields['meta_fields'], + 'taxonomies' => $collected_fields['taxonomies'], + 'context_data' => $data, + ]; + } + + /** + * Apply prepared meta fields and taxonomies for a batch. + * + * @since 0.9.0 + * @param wpdb $wpdb WordPress database handler. + * @param array $items Prepared batch items. + * @param array $context Context values for row processing. + * @param array $dry_run_log Dry run log. + * @param array|null $profile Import profile counters. + * @return void + */ + public function apply_prepared_meta_and_taxonomies_for_batch( + wpdb $wpdb, + array $items, + array $context, + array &$dry_run_log, + ?array &$profile = null + ): void { + if ( null === $profile ) { + $profile = []; + } + + $meta_started_at = microtime( true ); + $this->apply_prepared_meta_fields_for_batch( $wpdb, $items, $context, $dry_run_log ); + $profile['meta_apply'] = (float) ( $profile['meta_apply'] ?? 0.0 ) + microtime( true ) - $meta_started_at; + + $tax_started_at = microtime( true ); + $this->apply_prepared_taxonomies_for_batch( $wpdb, $items, $context, $dry_run_log, $profile ); + $profile['tax_apply'] = (float) ( $profile['tax_apply'] ?? 0.0 ) + microtime( true ) - $tax_started_at; + } + /** * Collect taxonomy fields from a CSV row. * @@ -219,20 +296,6 @@ public function collect_taxonomy_fields_from_row( array $headers, array $data ): $taxonomy_name = substr( $header_name_normalized, 4 ); // Remove 'tax_'. $taxonomies[ $taxonomy_name ] = $terms; - if ( taxonomy_exists( $taxonomy_name ) ) { - $term_ids = []; - foreach ( $terms as $term_name ) { - if ( ! empty( $term_name ) ) { - $term = get_term_by( 'name', $term_name, $taxonomy_name ); - if ( $term ) { - $term_ids[] = $term->term_id; - } - } - } - if ( ! empty( $term_ids ) ) { - $taxonomy_term_ids[ $taxonomy_name ] = $term_ids; - } - } } return [ @@ -410,6 +473,242 @@ public function apply_taxonomies_for_post( $this->taxonomy_writer->apply_taxonomies_for_post( $wpdb, $post_id, $taxonomies, $context, $dry_run_log ); } + /** + * Apply prepared taxonomies for a batch. + * + * @since 0.9.0 + * @param wpdb $wpdb WordPress DB instance. + * @param array $items Prepared batch items. + * @param array $context Context values for row processing. + * @param array $dry_run_log Dry run log. + * @param array|null $profile Import profile counters. + * @return void + */ + private function apply_prepared_taxonomies_for_batch( + wpdb $wpdb, + array $items, + array $context, + array &$dry_run_log, + ?array &$profile = null + ): void { + if ( null === $profile ) { + $profile = []; + } + + $dry_run = (bool) ( $context['dry_run'] ?? false ); + $taxonomy_format = (string) ( $context['taxonomy_format'] ?? 'name' ); + $taxonomy_format_validation = isset( $context['taxonomy_format_validation'] ) && is_array( $context['taxonomy_format_validation'] ) ? $context['taxonomy_format_validation'] : []; + $term_id_cache = []; + $existing_term_map = $dry_run ? [] : $this->get_existing_term_ids_for_batch( $wpdb, $items ); + + foreach ( $items as $item ) { + if ( ! is_array( $item ) ) { + continue; + } + + $post_id = (int) ( $item['post_id'] ?? 0 ); + $taxonomies = isset( $item['taxonomies'] ) && is_array( $item['taxonomies'] ) ? $item['taxonomies'] : []; + if ( empty( $taxonomies ) || ( $post_id <= 0 && ! $dry_run ) ) { + continue; + } + + foreach ( $taxonomies as $taxonomy => $terms ) { + if ( ! is_string( $taxonomy ) || ! is_array( $terms ) || empty( $terms ) ) { + continue; + } + + $resolve_started_at = microtime( true ); + $term_ids = $this->resolve_taxonomy_term_ids_with_cache( + $taxonomy, + $terms, + $taxonomy_format, + $taxonomy_format_validation, + $term_id_cache + ); + $profile['tax_resolve'] = (float) ( $profile['tax_resolve'] ?? 0.0 ) + microtime( true ) - $resolve_started_at; + + if ( ! $dry_run && $this->are_taxonomy_terms_unchanged( $existing_term_map, $post_id, $taxonomy, $term_ids ) ) { + $profile['tax_set_terms_skipped'] = (int) ( $profile['tax_set_terms_skipped'] ?? 0 ) + 1; + continue; + } + + $set_terms_started_at = microtime( true ); + $this->apply_taxonomy_terms_to_post( $post_id, $taxonomy, $term_ids, $dry_run, $dry_run_log ); + $profile['tax_set_terms'] = (float) ( $profile['tax_set_terms'] ?? 0.0 ) + microtime( true ) - $set_terms_started_at; + $profile['tax_set_terms_calls'] = (int) ( $profile['tax_set_terms_calls'] ?? 0 ) + 1; + } + } + } + + /** + * Get existing taxonomy term IDs for all post/taxonomy pairs in a batch. + * + * @since 0.9.9.5 + * @param wpdb $wpdb WordPress DB instance. + * @param array $items Prepared batch items. + * @return array>> + */ + private function get_existing_term_ids_for_batch( wpdb $wpdb, array $items ): array { + $post_ids = []; + $taxonomies = []; + + foreach ( $items as $item ) { + if ( ! is_array( $item ) ) { + continue; + } + + $post_id = (int) ( $item['post_id'] ?? 0 ); + if ( $post_id <= 0 ) { + continue; + } + + $item_taxonomies = isset( $item['taxonomies'] ) && is_array( $item['taxonomies'] ) ? $item['taxonomies'] : []; + foreach ( $item_taxonomies as $taxonomy => $terms ) { + if ( ! is_string( $taxonomy ) || ! is_array( $terms ) || empty( $terms ) ) { + continue; + } + + $post_ids[] = $post_id; + $taxonomies[] = $taxonomy; + } + } + + $post_ids = array_values( array_unique( array_map( 'intval', $post_ids ) ) ); + $taxonomies = array_values( array_unique( array_map( 'strval', $taxonomies ) ) ); + if ( empty( $post_ids ) || empty( $taxonomies ) ) { + return []; + } + + $post_placeholders = implode( ',', array_fill( 0, count( $post_ids ), '%d' ) ); + $taxonomy_placeholders = implode( ',', array_fill( 0, count( $taxonomies ), '%s' ) ); + $query_args = array_merge( $post_ids, $taxonomies ); + + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $sql = "SELECT tr.object_id, tt.taxonomy, tt.term_id + FROM {$wpdb->term_relationships} tr + INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id + WHERE tr.object_id IN ({$post_placeholders}) + AND tt.taxonomy IN ({$taxonomy_placeholders})"; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared + $rows = $wpdb->get_results( $wpdb->prepare( $sql, $query_args ), ARRAY_A ); + if ( empty( $rows ) ) { + return []; + } + + $existing_term_map = []; + foreach ( $rows as $row ) { + $post_id = (int) ( $row['object_id'] ?? 0 ); + $taxonomy = (string) ( $row['taxonomy'] ?? '' ); + $term_id = (int) ( $row['term_id'] ?? 0 ); + if ( $post_id <= 0 || '' === $taxonomy || $term_id <= 0 ) { + continue; + } + + if ( ! isset( $existing_term_map[ $post_id ] ) ) { + $existing_term_map[ $post_id ] = []; + } + if ( ! isset( $existing_term_map[ $post_id ][ $taxonomy ] ) ) { + $existing_term_map[ $post_id ][ $taxonomy ] = []; + } + + $existing_term_map[ $post_id ][ $taxonomy ][] = $term_id; + } + + foreach ( $existing_term_map as $post_id => $taxonomy_terms ) { + foreach ( $taxonomy_terms as $taxonomy => $term_ids ) { + $existing_term_map[ $post_id ][ $taxonomy ] = $this->normalize_term_ids_for_comparison( $term_ids ); + } + } + + return $existing_term_map; + } + + /** + * Check whether taxonomy terms are unchanged for a post. + * + * @since 0.9.9.5 + * @param array $existing_term_map Existing term IDs by post and taxonomy. + * @param int $post_id Post ID. + * @param string $taxonomy Taxonomy name. + * @param array $term_ids Resolved term IDs. + * @return bool Whether the terms are unchanged. + */ + private function are_taxonomy_terms_unchanged( array $existing_term_map, int $post_id, string $taxonomy, array $term_ids ): bool { + $existing_term_ids = isset( $existing_term_map[ $post_id ][ $taxonomy ] ) && is_array( $existing_term_map[ $post_id ][ $taxonomy ] ) + ? $existing_term_map[ $post_id ][ $taxonomy ] + : []; + $new_term_ids = $this->normalize_term_ids_for_comparison( $term_ids ); + + return $existing_term_ids === $new_term_ids; + } + + /** + * Normalize term IDs for deterministic comparison. + * + * @since 0.9.9.5 + * @param array $term_ids Term IDs. + * @return array + */ + private function normalize_term_ids_for_comparison( array $term_ids ): array { + $term_ids = array_values( + array_unique( + array_filter( + array_map( 'intval', $term_ids ), + static function ( int $term_id ): bool { + return $term_id > 0; + } + ) + ) + ); + sort( $term_ids, SORT_NUMERIC ); + + return $term_ids; + } + + /** + * Resolve taxonomy term IDs using a batch-local cache. + * + * @since 0.9.0 + * @param string $taxonomy Taxonomy name. + * @param array $terms Term values. + * @param string $taxonomy_format Taxonomy format. + * @param array $taxonomy_format_validation Taxonomy format validation. + * @param array $term_id_cache Resolved term ID cache. + * @return array + */ + private function resolve_taxonomy_term_ids_with_cache( + string $taxonomy, + array $terms, + string $taxonomy_format, + array $taxonomy_format_validation, + array &$term_id_cache + ): array { + $term_ids = []; + foreach ( $terms as $term_value ) { + $term_value = trim( (string) $term_value ); + if ( '' === $term_value ) { + continue; + } + + $cache_key = $taxonomy . "\n" . $taxonomy_format . "\n" . $term_value; + if ( ! array_key_exists( $cache_key, $term_id_cache ) ) { + $term_id_cache[ $cache_key ] = $this->resolve_term_ids_for_term_value( + $taxonomy, + $term_value, + $taxonomy_format, + $taxonomy_format_validation + ); + } + + foreach ( $term_id_cache[ $cache_key ] as $resolved_term_id ) { + $term_ids[] = (int) $resolved_term_id; + } + } + + return array_values( array_unique( $term_ids ) ); + } + /** * Resolve term IDs from a term value. * @@ -552,6 +851,150 @@ public function apply_meta_fields_for_post( } } + /** + * Apply prepared meta fields for a batch. + * + * @since 0.9.0 + * @param wpdb $wpdb WordPress DB instance. + * @param array $items Prepared batch items. + * @param array $context Context values for row processing. + * @param array $dry_run_log Dry run log. + * @return void + */ + private function apply_prepared_meta_fields_for_batch( + wpdb $wpdb, + array $items, + array $context, + array &$dry_run_log + ): void { + $dry_run = (bool) ( $context['dry_run'] ?? false ); + $delete_keys = []; + $insert_rows = []; + + foreach ( $items as $item ) { + if ( ! is_array( $item ) ) { + continue; + } + + $post_id = (int) ( $item['post_id'] ?? 0 ); + $meta_fields = isset( $item['meta_fields'] ) && is_array( $item['meta_fields'] ) ? $item['meta_fields'] : []; + if ( empty( $meta_fields ) || ( $post_id <= 0 && ! $dry_run ) ) { + continue; + } + + foreach ( $meta_fields as $key => $value ) { + if ( '' === $value || null === $value ) { + continue; + } + + if ( ! is_string( $value ) ) { + $value = maybe_serialize( $value ); + } + + if ( $dry_run ) { + $this->append_meta_dry_run_log( (string) $key, (string) $value, $dry_run_log ); + continue; + } + + $delete_keys[ $post_id . "\n" . $key ] = [ + 'post_id' => $post_id, + 'meta_key' => (string) $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + ]; + + $values = $this->get_csv_util()->split_pipe_separated_values( $value ); + foreach ( array_map( 'trim', $values ) as $single_value ) { + if ( '' === $single_value ) { + continue; + } + $insert_rows[] = [ + 'post_id' => $post_id, + 'meta_key' => (string) $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => $single_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ]; + } + } + } + + if ( $dry_run || empty( $delete_keys ) ) { + return; + } + + foreach ( array_chunk( array_values( $delete_keys ), 200 ) as $delete_chunk ) { + $where_parts = []; + $params = []; + foreach ( $delete_chunk as $delete_item ) { + $where_parts[] = '(post_id = %d AND meta_key = %s)'; + $params[] = (int) $delete_item['post_id']; + $params[] = (string) $delete_item['meta_key']; + } + + $sql = "DELETE FROM {$wpdb->postmeta} WHERE " . implode( ' OR ', $where_parts ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( $wpdb->prepare( $sql, $params ) ); + } + + foreach ( array_chunk( $insert_rows, 500 ) as $insert_chunk ) { + $placeholders = []; + $params = []; + foreach ( $insert_chunk as $insert_row ) { + $placeholders[] = '(%d, %s, %s)'; + $params[] = (int) $insert_row['post_id']; + $params[] = (string) $insert_row['meta_key']; + $params[] = (string) $insert_row['meta_value']; + } + + if ( empty( $placeholders ) ) { + continue; + } + + $sql = "INSERT INTO {$wpdb->postmeta} (post_id, meta_key, meta_value) VALUES " . implode( ', ', $placeholders ); + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared + $wpdb->query( $wpdb->prepare( $sql, $params ) ); + } + } + + /** + * Append meta dry run log. + * + * @since 0.9.0 + * @param string $key Meta key. + * @param string $value Meta value. + * @param array $dry_run_log Dry run log. + * @return void + */ + private function append_meta_dry_run_log( string $key, string $value, array &$dry_run_log ): void { + $dry_run_log_limit = (int) apply_filters( 'fe_csv_import_export_dry_run_log_limit', 50 ); + $values = $this->get_csv_util()->split_pipe_separated_values( $value ); + if ( count( $values ) > 1 ) { + foreach ( array_map( 'trim', $values ) as $single_value ) { + if ( '' === $single_value ) { + continue; + } + $dry_run_log[] = sprintf( + /* translators: 1: field name, 2: field value */ + __( 'Custom field (multi-value): %1$s = %2$s', 'fe-csv-import-export' ), + $key, + $single_value + ); + if ( count( $dry_run_log ) > $dry_run_log_limit ) { + $dry_run_log = array_slice( $dry_run_log, -1 * $dry_run_log_limit ); + } + } + return; + } + + $single_value = trim( (string) ( $values[0] ?? '' ) ); + $dry_run_log[] = sprintf( + /* translators: 1: field name, 2: field value */ + __( 'Custom field: %1$s = %2$s', 'fe-csv-import-export' ), + $key, + $single_value + ); + if ( count( $dry_run_log ) > $dry_run_log_limit ) { + $dry_run_log = array_slice( $dry_run_log, -1 * $dry_run_log_limit ); + } + } + /** * Normalize header/field name. * diff --git a/includes/import/class-fe-csv-import-export-import-wp-compatible.php b/includes/import/class-fe-csv-import-export-import-wp-compatible.php index fd0ea35..fe668ae 100644 --- a/includes/import/class-fe-csv-import-export-import-wp-compatible.php +++ b/includes/import/class-fe-csv-import-export-import-wp-compatible.php @@ -152,6 +152,39 @@ public function import(): void { $this->cleanup_import_session( $import_session ); } + $profile = isset( $counters['import_profile'] ) && is_array( $counters['import_profile'] ) ? $counters['import_profile'] : []; + $profile_details = sprintf( + 'start=%d size=%d processed=%d ensure=%.4fs read=%.4fs process=%.4fs total=%.4fs preload=%.4fs row_context=%.4fs row_processor=%.4fs success_log=%.4fs success_counter_guid=%.4fs prepare_meta_tax=%.4fs compat_hooks=%.4fs meta_apply=%.4fs tax_apply=%.4fs tax_resolve=%.4fs tax_set_terms=%.4fs tax_set_terms_calls=%d tax_set_terms_skipped=%d rows_profiled=%d memory_delta=%d peak=%d', + $start_row, + $batch_size, + (int) $counters['processed'], + $ensure_elapsed, + $read_elapsed, + $process_elapsed, + microtime( true ) - $batch_started_at, + (float) ( $profile['preload'] ?? 0.0 ), + (float) ( $profile['row_context'] ?? 0.0 ), + (float) ( $profile['row_processor'] ?? 0.0 ), + (float) ( $profile['success_log'] ?? 0.0 ), + (float) ( $profile['success_counter_guid'] ?? 0.0 ), + (float) ( $profile['prepare_meta_tax'] ?? 0.0 ), + (float) ( $profile['compat_hooks'] ?? 0.0 ), + (float) ( $profile['meta_apply'] ?? 0.0 ), + (float) ( $profile['tax_apply'] ?? 0.0 ), + (float) ( $profile['tax_resolve'] ?? 0.0 ), + (float) ( $profile['tax_set_terms'] ?? 0.0 ), + (int) ( $profile['tax_set_terms_calls'] ?? 0 ), + (int) ( $profile['tax_set_terms_skipped'] ?? 0 ), + (int) ( $profile['rows_profiled'] ?? 0 ), + memory_get_usage( true ) - $batch_memory_start, + memory_get_peak_usage( true ) + ); + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + error_log( '[FE CSV Import & Export] Batch profile: ' . $profile_details ); + } + if ( $enable_logs && is_callable( $append_log ) ) { $append_log( [ @@ -159,18 +192,7 @@ public function import(): void { 'status' => 'info', 'row' => $start_row + 1, 'title' => 'Batch profile', - 'details' => sprintf( - 'start=%d size=%d processed=%d ensure=%.4fs read=%.4fs process=%.4fs total=%.4fs memory_delta=%d peak=%d', - $start_row, - $batch_size, - (int) $counters['processed'], - $ensure_elapsed, - $read_elapsed, - $process_elapsed, - microtime( true ) - $batch_started_at, - memory_get_usage( true ) - $batch_memory_start, - memory_get_peak_usage( true ) - ), + 'details' => $profile_details, ] ); } diff --git a/package.json b/package.json index c31a288..4ab9861 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fe-csv-import-export", - "version": "0.9.9.3", + "version": "1.0.0", "description": "FE CSV Import & Export - Simple CSV Management Without the Complexity.", "license": "GPL-2.0+", "engines": { diff --git a/readme.txt b/readme.txt index 256cdce..555adfe 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: csv, import, export, custom post types, custom fields Requires at least: 6.6 Tested up to: 7.0 Requires PHP: 8.1 -Stable tag: 0.9.9.4 +Stable tag: 1.0.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -135,6 +135,14 @@ No. The free version works on its own. A Pro version is available separately for == Changelog == += 1.0.0 = + +* **Taxonomy optimization for unchanged terms** - Import now skips wp_set_post_terms() calls when taxonomy terms are unchanged, significantly reducing processing time for update-heavy imports. +* **Increased default batch size** - Default import batch size increased from 100 to 500 rows, reducing AJAX overhead for large imports. +* **Configurable batch size limit** - New fe_csv_import_export_import_max_batch_size filter allows per-site customization of the maximum batch size. +* **Detailed import profiling** - Added granular profiling for import phases with WP_DEBUG batch profile logging. +* **Fixed JavaScript ES module import syntax** - Removed static ES module imports from main JS file to resolve browser syntax errors. + = 0.9.9.4 = * Bundled JavaScript files to reduce HTTP requests and improve page load performance. diff --git a/test-release.sh b/test-release.sh index 6a81546..1ff7841 100755 --- a/test-release.sh +++ b/test-release.sh @@ -4,7 +4,7 @@ set -euo pipefail BRANCH="${1:-$(git rev-parse --abbrev-ref HEAD)}" RELEASE_DIR="test-release" -ZIP_NAME="fe-csv-import-export-dev.zip" +ZIP_NAME="fe-csv-import-export-1.0.0.zip" GENERATED_MINIFIED_FILES=( "assets/css/fe-csv-import-export-style.min.css" "assets/js/export/fe-csv-import-export/ajax.min.js"