From effeac96a484fe3f3154081837b316b792c22b11 Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Tue, 16 Jul 2024 22:43:31 +0700 Subject: [PATCH 01/22] Add support for importing into SQLite and exporting from SQLite --- src/DB_Command.php | 18 +++++++- src/WP_SQLite_Base.php | 67 ++++++++++++++++++++++++++++ src/WP_SQLite_Export.php | 72 ++++++++++++++++++++++++++++++ src/WP_SQLite_Import.php | 94 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 src/WP_SQLite_Base.php create mode 100644 src/WP_SQLite_Export.php create mode 100644 src/WP_SQLite_Import.php diff --git a/src/DB_Command.php b/src/DB_Command.php index 17d1c00e..27b98f64 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -149,7 +149,7 @@ public function drop( $_, $assoc_args ) { * Success: Database reset. */ public function reset( $_, $assoc_args ) { - WP_CLI::confirm( "Are you sure you want to reset the '" . DB_NAME . "' database?", $assoc_args ); + WP_CLI::confirm( "TEST HELLO! Are you sure you want to reset the '" . DB_NAME . "' database?", $assoc_args ); $this->run_query( sprintf( 'DROP DATABASE IF EXISTS `%s`', DB_NAME ), $assoc_args ); $this->run_query( self::get_create_query(), $assoc_args ); @@ -586,6 +586,7 @@ public function query( $args, $assoc_args ) { * ... * * @alias dump + * @when after_wp_load */ public function export( $args, $assoc_args ) { if ( ! empty( $args[0] ) ) { @@ -596,6 +597,13 @@ public function export( $args, $assoc_args ) { $result_file = sprintf( '%s-%s-%s.sql', DB_NAME, date( 'Y-m-d' ), $hash ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } + + if ( WP_SQLite_Export::get_sqlite_version() ) { + $export = new WP_SQLite_Export(); + $export->run(); + return; + } + $stdout = ( '-' === $result_file ); $porcelain = Utils\get_flag_value( $assoc_args, 'porcelain' ); @@ -762,6 +770,13 @@ public function import( $args, $assoc_args ) { $result_file = sprintf( '%s.sql', DB_NAME ); } + // We need to detect if the site is using SQLite + if( WP_SQLite_Import::get_sqlite_version() ) { + $importer = new WP_SQLite_Import(); + $importer->run( $args[0] ); + return; + } + // Process options to MySQL. $mysql_args = array_merge( [ 'database' => DB_NAME ], @@ -843,7 +858,6 @@ public function import( $args, $assoc_args ) { * $ wp db export --tables=$(wp db tables --url=sub.example.com --format=csv) * Success: Exported to wordpress_dbase.sql * - * @when after_wp_load */ public function tables( $args, $assoc_args ) { diff --git a/src/WP_SQLite_Base.php b/src/WP_SQLite_Base.php new file mode 100644 index 00000000..59117d5c --- /dev/null +++ b/src/WP_SQLite_Base.php @@ -0,0 +1,67 @@ +get_plugin_directory(); + if ( ! $plugin_directory ) { + throw new Exception( 'Could not locate the SQLite integration plugin.' ); + } + + // Load the translator class from the plugin. + if( ! defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { + define( 'SQLITE_DB_DROPIN_VERSION', self::get_sqlite_version() ); + } + + # A hack to add the do_action and apply_filters functions to the global namespace. + # This is necessary because during the import WP has not been loaded, and we + # need to define these functions to avoid fatal errors. + if( ! function_exists( 'do_action') ) { + function do_action(){}; + function apply_filters(){}; + } + + // We also need to selectively load the necessary classes from the plugin. + require_once $plugin_directory . '/php-polyfills.php'; + require_once $plugin_directory . '/constants.php'; + require_once $plugin_directory . '/wp-includes/sqlite/class-wp-sqlite-lexer.php'; + require_once $plugin_directory . '/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php'; + require_once $plugin_directory . '/wp-includes/sqlite/class-wp-sqlite-translator.php'; + require_once $plugin_directory . '/wp-includes/sqlite/class-wp-sqlite-token.php'; + require_once $plugin_directory . '/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php'; + } + + /** + * @return string|null + */ + protected function get_plugin_directory() { + $plugin_folders = [ + ABSPATH . '/wp-content/plugins/sqlite-database-integration', + ABSPATH . '/wp-content/mu-plugins/sqlite-database-integration', + ]; + + foreach ( $plugin_folders as $folder ) { + if ( file_exists( $folder ) && is_dir( $folder ) ) { + return $folder; + } + } + + return null; + } +} diff --git a/src/WP_SQLite_Export.php b/src/WP_SQLite_Export.php new file mode 100644 index 00000000..01263eaf --- /dev/null +++ b/src/WP_SQLite_Export.php @@ -0,0 +1,72 @@ +load_dependencies(); + WP_CLI::line( 'Exporting database...' ); + + $translator = new WP_SQLite_Translator(); + + $result = $translator->query('SHOW TABLES '); + $pdo = $translator->get_pdo(); + + // Stream into a file + $handle = fopen( 'export.sql', 'w' ); + + foreach ( $result as $table ) { + + $ignore_tables = [ + '_mysql_data_types_cache', + 'sqlite_master', + 'sqlite_sequence', + ]; + + if ( in_array( $table->name, $ignore_tables ) ) { + continue; + } + + $create = $translator->query('SHOW CREATE TABLE ' . $table->name ); + var_dump($create); + fwrite($handle, "DROP TABLE IF EXISTS `" . $table->name ."`;\n"); + fwrite( $handle, $create[0]->{'Create Table'} . "\n" ); + + $stmt = $pdo->prepare('SELECT * FROM ' . $table->name ); + $stmt->execute(); + while ( $row = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT) ) { + // Process the row here + // Rows are fetched in batches from the server + $insert_statement = sprintf("INSERT INTO `%1s` VALUES (%2s);", $table->name, $this->escape_values( $pdo, $row )); + + fwrite( $handle, $insert_statement . "\n" ); + } + } + + fclose( $handle ); + } + + /** + * Escape values for insert statement + * + * @param PDO $pdo + * @param $values + * + * @return string + */ + protected function escape_values( PDO $pdo, $values ) { + // Get a mysql PDO instance + $escaped_values = []; + foreach( $values as $value ) { + if( is_null( $value ) ) { + $escaped_values[] = 'NULL'; + } elseif ( is_numeric( $value ) ) { + $escaped_values[] = $value; + } else { + $escaped_values[] = $pdo->quote( $value ); + } + } + return implode(",", $escaped_values ); + } + +} diff --git a/src/WP_SQLite_Import.php b/src/WP_SQLite_Import.php new file mode 100644 index 00000000..fc13bc46 --- /dev/null +++ b/src/WP_SQLite_Import.php @@ -0,0 +1,94 @@ +load_dependencies(); + $translator = new WP_SQLite_Translator(); + foreach ( $this->parse_statements( $sql_file_path ) as $statement ) { + $result = $translator->query($statement); + if ( $result === false ) { + echo "Error: could not execute statement\n"; + echo "Statement: $statement\n"; + echo "\n\n"; + } + } + } + + /** + * Parse SQL statements from an SQL dump file. + * @param string $sql_file_path The path to the SQL dump file. + * + * @return Generator A generator that yields SQL statements. + * @throws Exception + */ + public function parse_statements( $sql_file_path ) { + $handle = fopen($sql_file_path, 'r'); + if (!$handle) { + throw new Exception("Unable to open file: $sql_file_path"); + } + + $single_quotes = 0; + $double_quotes = 0; + $in_comment = false; + $buffer = ''; + + while (($line = fgets($handle)) !== false) { + $line = trim($line); + + // Skip empty lines and comments + if (empty($line) || strpos($line, '--') === 0 || strpos($line, '#') === 0) { + continue; + } + + // Handle multi-line comments + if (!$in_comment && strpos($line, '/*') === 0) { + $in_comment = true; + } + if ($in_comment) { + if (strpos($line, '*/') !== false) { + $in_comment = false; + } + continue; + } + + for ($i = 0; $i < strlen($line); $i++) { + $ch = $line[$i]; + + // Handle escaped characters + if ($i > 0 && $line[$i - 1] == '\\') { + $buffer .= $ch; + continue; + } + + // Handle quotes + if ($ch == "'" && $double_quotes == 0) { + $single_quotes = 1 - $single_quotes; + } + if ($ch == '"' && $single_quotes == 0) { + $double_quotes = 1 - $double_quotes; + } + + // Process statement end + if ($ch == ';' && $single_quotes == 0 && $double_quotes == 0) { + yield trim($buffer); + $buffer = ''; + } else { + $buffer .= $ch; + } + } + } + + // Handle any remaining buffer content + if (!empty($buffer)) { + yield trim($buffer); + } + + fclose($handle); + } + + +} From 648f5c06cb47572e910ebe286dbef1507189c57d Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Tue, 16 Jul 2024 22:58:53 +0700 Subject: [PATCH 02/22] Remove test --- src/DB_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 27b98f64..06e6c61c 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -149,7 +149,7 @@ public function drop( $_, $assoc_args ) { * Success: Database reset. */ public function reset( $_, $assoc_args ) { - WP_CLI::confirm( "TEST HELLO! Are you sure you want to reset the '" . DB_NAME . "' database?", $assoc_args ); + WP_CLI::confirm( "Are you sure you want to reset the '" . DB_NAME . "' database?", $assoc_args ); $this->run_query( sprintf( 'DROP DATABASE IF EXISTS `%s`', DB_NAME ), $assoc_args ); $this->run_query( self::get_create_query(), $assoc_args ); From 6543dd9d30defb21ba35db4cd476130e700a104d Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Tue, 16 Jul 2024 22:59:30 +0700 Subject: [PATCH 03/22] Remove after load --- src/DB_Command.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 06e6c61c..ba1f2324 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -586,7 +586,6 @@ public function query( $args, $assoc_args ) { * ... * * @alias dump - * @when after_wp_load */ public function export( $args, $assoc_args ) { if ( ! empty( $args[0] ) ) { From 0c8160538be28e67adc00000c92c573b1b5d688a Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Fri, 19 Jul 2024 17:53:33 +0700 Subject: [PATCH 04/22] Refactoring changes --- src/DB_Command.php | 2 +- src/WP_SQLite_Export.php | 72 +++++++++++++++++++++++++++++----------- 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index ba1f2324..8c6b7d3d 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -599,7 +599,7 @@ public function export( $args, $assoc_args ) { if ( WP_SQLite_Export::get_sqlite_version() ) { $export = new WP_SQLite_Export(); - $export->run(); + $export->run( $result_file, $assoc_args ); return; } diff --git a/src/WP_SQLite_Export.php b/src/WP_SQLite_Export.php index 01263eaf..1a260385 100644 --- a/src/WP_SQLite_Export.php +++ b/src/WP_SQLite_Export.php @@ -3,19 +3,42 @@ class WP_SQLite_Export extends WP_SQLite_Base { - public function run() { - $this->load_dependencies(); - WP_CLI::line( 'Exporting database...' ); + private $unsupported_arguments = [ + 'fields', + 'include-tablespaces', + 'defaults', + 'db_user', + 'db_pass', + 'tables', + 'exclude-tables' + ]; - $translator = new WP_SQLite_Translator(); + /** + * Run the export command. + * + * @param string $result_file The file to write the exported data to. + * @param array $args The arguments passed to the command. + * + * @return void + * @throws Exception + */ + public function run( $result_file, $args ) { - $result = $translator->query('SHOW TABLES '); - $pdo = $translator->get_pdo(); + if ( array_intersect_key( $args, array_flip( $this->unsupported_arguments ) ) ) { + WP_CLI::error( + sprintf( + 'The following arguments are not supported by SQLite exports: %s', + implode( ', ', $this->unsupported_arguments ) + ) + ); + return; + } - // Stream into a file - $handle = fopen( 'export.sql', 'w' ); + $this->load_dependencies(); + $translator = new WP_SQLite_Translator(); + $handle = fopen( 'export.sql', 'w' ); - foreach ( $result as $table ) { + foreach ( $translator->query('SHOW TABLES') as $table ) { $ignore_tables = [ '_mysql_data_types_cache', @@ -27,25 +50,36 @@ public function run() { continue; } - $create = $translator->query('SHOW CREATE TABLE ' . $table->name ); - var_dump($create); fwrite($handle, "DROP TABLE IF EXISTS `" . $table->name ."`;\n"); - fwrite( $handle, $create[0]->{'Create Table'} . "\n" ); - - $stmt = $pdo->prepare('SELECT * FROM ' . $table->name ); - $stmt->execute(); - while ( $row = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT) ) { - // Process the row here - // Rows are fetched in batches from the server - $insert_statement = sprintf("INSERT INTO `%1s` VALUES (%2s);", $table->name, $this->escape_values( $pdo, $row )); + fwrite( $handle, $this->get_create_statement( $table, $translator ) . "\n" ); + foreach( $this->get_insert_statements( $table, $translator->get_pdo() ) as $insert_statement ) { fwrite( $handle, $insert_statement . "\n" ); } } + if ( $args['porcelain'] ) { + WP_CLI::line( $result_file ); + } else { + WP_CLI::line( 'Export complete. File written to ' . $result_file ); + } + fclose( $handle ); } + protected function get_create_statement( $table, $translator ) { + $create = $translator->query('SHOW CREATE TABLE ' . $table->name ); + return $create[0]->{'Create Table'}; + } + + protected function get_insert_statements( $table, $pdo ) { + $stmt = $pdo->prepare('SELECT * FROM ' . $table->name ); + $stmt->execute(); + while ( $row = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT) ) { + yield sprintf("INSERT INTO `%1s` VALUES (%2s);", $table->name, $this->escape_values( $pdo, $row )); + } + } + /** * Escape values for insert statement * From 05714cb8fa1ac9b46e358120b397a22c5edaee50 Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Fri, 19 Jul 2024 22:00:10 +0700 Subject: [PATCH 05/22] Refactoring --- src/DB_Command.php | 5 +++-- src/WP_SQLite_Base.php | 13 ++++++++++++ src/WP_SQLite_Export.php | 45 +++++++++++++++++++--------------------- src/WP_SQLite_Import.php | 21 ++++++++++++------- 4 files changed, 51 insertions(+), 33 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 8c6b7d3d..41245877 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -597,6 +597,7 @@ public function export( $args, $assoc_args ) { } + // Check if SQLite is enabled and use it if it is. if ( WP_SQLite_Export::get_sqlite_version() ) { $export = new WP_SQLite_Export(); $export->run( $result_file, $assoc_args ); @@ -769,10 +770,10 @@ public function import( $args, $assoc_args ) { $result_file = sprintf( '%s.sql', DB_NAME ); } - // We need to detect if the site is using SQLite + // Check if SQLite is enabled and use it if it is. if( WP_SQLite_Import::get_sqlite_version() ) { $importer = new WP_SQLite_Import(); - $importer->run( $args[0] ); + $importer->run( $args[0], $args ); return; } diff --git a/src/WP_SQLite_Base.php b/src/WP_SQLite_Base.php index 59117d5c..23db5666 100644 --- a/src/WP_SQLite_Base.php +++ b/src/WP_SQLite_Base.php @@ -2,6 +2,8 @@ class WP_SQLite_Base { + protected $unsupported_arguments = []; + /** * Tries to determine if the current site is using SQL by checking * for an active sqlite integration plugin. @@ -64,4 +66,15 @@ protected function get_plugin_directory() { return null; } + + protected function check_arguments( $args ) { + if ( array_intersect_key( $args, array_flip( $this->unsupported_arguments ) ) ) { + WP_CLI::error( + sprintf( + 'The following arguments are not supported by SQLite exports: %s', + implode( ', ', $this->unsupported_arguments ) + ) + ); + } + } } diff --git a/src/WP_SQLite_Export.php b/src/WP_SQLite_Export.php index 1a260385..55f55216 100644 --- a/src/WP_SQLite_Export.php +++ b/src/WP_SQLite_Export.php @@ -2,15 +2,12 @@ class WP_SQLite_Export extends WP_SQLite_Base { - - private $unsupported_arguments = [ + protected $unsupported_arguments = [ 'fields', 'include-tablespaces', 'defaults', - 'db_user', - 'db_pass', - 'tables', - 'exclude-tables' + 'dbuser', + 'dbpass', ]; /** @@ -24,29 +21,30 @@ class WP_SQLite_Export extends WP_SQLite_Base { */ public function run( $result_file, $args ) { - if ( array_intersect_key( $args, array_flip( $this->unsupported_arguments ) ) ) { - WP_CLI::error( - sprintf( - 'The following arguments are not supported by SQLite exports: %s', - implode( ', ', $this->unsupported_arguments ) - ) - ); - return; - } - + $this->check_arguments( $args ); $this->load_dependencies(); + + $exclude_tables = isset( $args['exclude_tables'] ) ? explode( ',', $args['exclude_tables'] ) : []; + $exclude_tables = array_merge($exclude_tables , [ + '_mysql_data_types_cache', + 'sqlite_master', + 'sqlite_sequence', + ] ); + + $include_tables = isset( $args['tables'] ) ? explode( ',', $args['tables'] ) : []; + $translator = new WP_SQLite_Translator(); $handle = fopen( 'export.sql', 'w' ); foreach ( $translator->query('SHOW TABLES') as $table ) { - $ignore_tables = [ - '_mysql_data_types_cache', - 'sqlite_master', - 'sqlite_sequence', - ]; + // Skip tables that are not in the include_tables list if the list is defined + if ( ! empty( $include_tables ) && ! in_array( $table->name, $include_tables ) ) { + continue; + } - if ( in_array( $table->name, $ignore_tables ) ) { + // Skip tables that are in the exclude_tables list + if ( in_array( $table->name, $exclude_tables ) ) { continue; } @@ -58,7 +56,7 @@ public function run( $result_file, $args ) { } } - if ( $args['porcelain'] ) { + if ( isset( $args['porcelain'] ) ) { WP_CLI::line( $result_file ); } else { WP_CLI::line( 'Export complete. File written to ' . $result_file ); @@ -102,5 +100,4 @@ protected function escape_values( PDO $pdo, $values ) { } return implode(",", $escaped_values ); } - } diff --git a/src/WP_SQLite_Import.php b/src/WP_SQLite_Import.php index fc13bc46..8c568564 100644 --- a/src/WP_SQLite_Import.php +++ b/src/WP_SQLite_Import.php @@ -2,18 +2,27 @@ class WP_SQLite_Import extends WP_SQLite_Base { + protected $unsupported_arguments = [ + 'skip-optimization', + 'defaults', + 'fields', + 'dbuser', + 'dbpass', + ]; + /** + * Execute the import command for SQLite. + * * @throws Exception */ - public function run( $sql_file_path ) { + public function run( $sql_file_path, $args ) { + $this->check_arguments( $args ); $this->load_dependencies(); $translator = new WP_SQLite_Translator(); foreach ( $this->parse_statements( $sql_file_path ) as $statement ) { $result = $translator->query($statement); if ( $result === false ) { - echo "Error: could not execute statement\n"; - echo "Statement: $statement\n"; - echo "\n\n"; + WP_CLI::warning( "Could not execute statement: " . $statement ); } } } @@ -45,7 +54,7 @@ public function parse_statements( $sql_file_path ) { } // Handle multi-line comments - if (!$in_comment && strpos($line, '/*') === 0) { + if (! $in_comment && strpos($line, '/*') === 0) { $in_comment = true; } if ($in_comment) { @@ -89,6 +98,4 @@ public function parse_statements( $sql_file_path ) { fclose($handle); } - - } From f6b2da68d17f16b8ae0785e864a767d2bf93dc94 Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Fri, 19 Jul 2024 22:17:32 +0700 Subject: [PATCH 06/22] Some code standard fixes --- src/DB_Command.php | 2 +- src/WP_SQLite_Base.php | 12 +++++----- src/WP_SQLite_Export.php | 37 ++++++++++++++++-------------- src/WP_SQLite_Import.php | 49 ++++++++++++++++++++-------------------- 4 files changed, 52 insertions(+), 48 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 41245877..70a291d9 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -771,7 +771,7 @@ public function import( $args, $assoc_args ) { } // Check if SQLite is enabled and use it if it is. - if( WP_SQLite_Import::get_sqlite_version() ) { + if ( WP_SQLite_Import::get_sqlite_version() ) { $importer = new WP_SQLite_Import(); $importer->run( $args[0], $args ); return; diff --git a/src/WP_SQLite_Base.php b/src/WP_SQLite_Base.php index 23db5666..ff4e5d16 100644 --- a/src/WP_SQLite_Base.php +++ b/src/WP_SQLite_Base.php @@ -11,7 +11,7 @@ class WP_SQLite_Base { */ public static function get_sqlite_version() { // Check if there is a db.php file in the wp-content directory. - if ( ! file_exists( ABSPATH . '/wp-content/db.php') ) { + if ( ! file_exists( ABSPATH . '/wp-content/db.php' ) ) { return false; } // If the file is found, we need to check that it is the sqlite integration plugin. @@ -27,20 +27,20 @@ protected function load_dependencies() { } // Load the translator class from the plugin. - if( ! defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { + if ( ! defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { define( 'SQLITE_DB_DROPIN_VERSION', self::get_sqlite_version() ); } # A hack to add the do_action and apply_filters functions to the global namespace. # This is necessary because during the import WP has not been loaded, and we # need to define these functions to avoid fatal errors. - if( ! function_exists( 'do_action') ) { - function do_action(){}; - function apply_filters(){}; + if ( ! function_exists( 'do_action' ) ) { + function do_action() {} + function apply_filters() {} } // We also need to selectively load the necessary classes from the plugin. - require_once $plugin_directory . '/php-polyfills.php'; + require_once $plugin_directory . '/php-polyfills.php'; require_once $plugin_directory . '/constants.php'; require_once $plugin_directory . '/wp-includes/sqlite/class-wp-sqlite-lexer.php'; require_once $plugin_directory . '/wp-includes/sqlite/class-wp-sqlite-query-rewriter.php'; diff --git a/src/WP_SQLite_Export.php b/src/WP_SQLite_Export.php index 55f55216..d46b6eab 100644 --- a/src/WP_SQLite_Export.php +++ b/src/WP_SQLite_Export.php @@ -25,33 +25,36 @@ public function run( $result_file, $args ) { $this->load_dependencies(); $exclude_tables = isset( $args['exclude_tables'] ) ? explode( ',', $args['exclude_tables'] ) : []; - $exclude_tables = array_merge($exclude_tables , [ - '_mysql_data_types_cache', - 'sqlite_master', - 'sqlite_sequence', - ] ); + $exclude_tables = array_merge( + $exclude_tables, + [ + '_mysql_data_types_cache', + 'sqlite_master', + 'sqlite_sequence', + ] + ); $include_tables = isset( $args['tables'] ) ? explode( ',', $args['tables'] ) : []; $translator = new WP_SQLite_Translator(); $handle = fopen( 'export.sql', 'w' ); - foreach ( $translator->query('SHOW TABLES') as $table ) { + foreach ( $translator->query( 'SHOW TABLES' ) as $table ) { // Skip tables that are not in the include_tables list if the list is defined - if ( ! empty( $include_tables ) && ! in_array( $table->name, $include_tables ) ) { + if ( ! empty( $include_tables ) && ! in_array( $table->name, $include_tables, true ) ) { continue; } // Skip tables that are in the exclude_tables list - if ( in_array( $table->name, $exclude_tables ) ) { + if ( in_array( $table->name, $exclude_tables, true ) ) { continue; } - fwrite($handle, "DROP TABLE IF EXISTS `" . $table->name ."`;\n"); + fwrite( $handle, 'DROP TABLE IF EXISTS `' . $table->name . "`;\n" ); fwrite( $handle, $this->get_create_statement( $table, $translator ) . "\n" ); - foreach( $this->get_insert_statements( $table, $translator->get_pdo() ) as $insert_statement ) { + foreach ( $this->get_insert_statements( $table, $translator->get_pdo() ) as $insert_statement ) { fwrite( $handle, $insert_statement . "\n" ); } } @@ -66,15 +69,15 @@ public function run( $result_file, $args ) { } protected function get_create_statement( $table, $translator ) { - $create = $translator->query('SHOW CREATE TABLE ' . $table->name ); + $create = $translator->query( 'SHOW CREATE TABLE ' . $table->name ); return $create[0]->{'Create Table'}; } protected function get_insert_statements( $table, $pdo ) { - $stmt = $pdo->prepare('SELECT * FROM ' . $table->name ); + $stmt = $pdo->prepare( 'SELECT * FROM ' . $table->name ); $stmt->execute(); - while ( $row = $stmt->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT) ) { - yield sprintf("INSERT INTO `%1s` VALUES (%2s);", $table->name, $this->escape_values( $pdo, $row )); + while ( $row = $stmt->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT ) ) { + yield sprintf( 'INSERT INTO `%1s` VALUES (%2s);', $table->name, $this->escape_values( $pdo, $row ) ); } } @@ -89,8 +92,8 @@ protected function get_insert_statements( $table, $pdo ) { protected function escape_values( PDO $pdo, $values ) { // Get a mysql PDO instance $escaped_values = []; - foreach( $values as $value ) { - if( is_null( $value ) ) { + foreach ( $values as $value ) { + if ( is_null( $value ) ) { $escaped_values[] = 'NULL'; } elseif ( is_numeric( $value ) ) { $escaped_values[] = $value; @@ -98,6 +101,6 @@ protected function escape_values( PDO $pdo, $values ) { $escaped_values[] = $pdo->quote( $value ); } } - return implode(",", $escaped_values ); + return implode( ',', $escaped_values ); } } diff --git a/src/WP_SQLite_Import.php b/src/WP_SQLite_Import.php index 8c568564..e8905f2d 100644 --- a/src/WP_SQLite_Import.php +++ b/src/WP_SQLite_Import.php @@ -20,9 +20,9 @@ public function run( $sql_file_path, $args ) { $this->load_dependencies(); $translator = new WP_SQLite_Translator(); foreach ( $this->parse_statements( $sql_file_path ) as $statement ) { - $result = $translator->query($statement); - if ( $result === false ) { - WP_CLI::warning( "Could not execute statement: " . $statement ); + $result = $translator->query( $statement ); + if ( false === $result ) { + WP_CLI::warning( 'Could not execute statement: ' . $statement ); } } } @@ -35,55 +35,56 @@ public function run( $sql_file_path, $args ) { * @throws Exception */ public function parse_statements( $sql_file_path ) { - $handle = fopen($sql_file_path, 'r'); - if (!$handle) { - throw new Exception("Unable to open file: $sql_file_path"); + $handle = fopen( $sql_file_path, 'r' ); + if ( ! $handle ) { + throw new Exception( "Unable to open file: $sql_file_path" ); } $single_quotes = 0; $double_quotes = 0; - $in_comment = false; - $buffer = ''; + $in_comment = false; + $buffer = ''; - while (($line = fgets($handle)) !== false) { - $line = trim($line); + while ( ( $line = fgets( $handle ) ) !== false ) { + $line = trim( $line ); // Skip empty lines and comments - if (empty($line) || strpos($line, '--') === 0 || strpos($line, '#') === 0) { + if ( empty( $line ) || strpos( $line, '--' ) === 0 || strpos( $line, '#' ) === 0 ) { continue; } // Handle multi-line comments - if (! $in_comment && strpos($line, '/*') === 0) { + if ( ! $in_comment && strpos( $line, '/*' ) === 0 ) { $in_comment = true; } - if ($in_comment) { - if (strpos($line, '*/') !== false) { + if ( $in_comment ) { + if ( strpos( $line, '*/' ) !== false ) { $in_comment = false; } continue; } - for ($i = 0; $i < strlen($line); $i++) { - $ch = $line[$i]; + $strlen = strlen( $line ); + for ( $i = 0; $i < $strlen; $i++ ) { + $ch = $line[ $i ]; // Handle escaped characters - if ($i > 0 && $line[$i - 1] == '\\') { + if ( $i > 0 && '\\' === $line[ $i - 1 ] ) { $buffer .= $ch; continue; } // Handle quotes - if ($ch == "'" && $double_quotes == 0) { + if ( "'" === $ch && 0 === $double_quotes ) { $single_quotes = 1 - $single_quotes; } - if ($ch == '"' && $single_quotes == 0) { + if ( '"' === $ch && 0 === $single_quotes ) { $double_quotes = 1 - $double_quotes; } // Process statement end - if ($ch == ';' && $single_quotes == 0 && $double_quotes == 0) { - yield trim($buffer); + if ( ';' === $ch && 0 === $single_quotes && 0 === $double_quotes ) { + yield trim( $buffer ); $buffer = ''; } else { $buffer .= $ch; @@ -92,10 +93,10 @@ public function parse_statements( $sql_file_path ) { } // Handle any remaining buffer content - if (!empty($buffer)) { - yield trim($buffer); + if ( ! empty( $buffer ) ) { + yield trim( $buffer ); } - fclose($handle); + fclose( $handle ); } } From 963fc04f43df753cc91f531e643e653734c538ce Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Fri, 19 Jul 2024 22:20:40 +0700 Subject: [PATCH 07/22] Ignore some linting issues --- src/WP_SQLite_Base.php | 6 +++--- src/WP_SQLite_Export.php | 1 + src/WP_SQLite_Import.php | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/WP_SQLite_Base.php b/src/WP_SQLite_Base.php index ff4e5d16..c2435dea 100644 --- a/src/WP_SQLite_Base.php +++ b/src/WP_SQLite_Base.php @@ -28,15 +28,15 @@ protected function load_dependencies() { // Load the translator class from the plugin. if ( ! defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { - define( 'SQLITE_DB_DROPIN_VERSION', self::get_sqlite_version() ); + define( 'SQLITE_DB_DROPIN_VERSION', self::get_sqlite_version() ); // phpcs:ignore } # A hack to add the do_action and apply_filters functions to the global namespace. # This is necessary because during the import WP has not been loaded, and we # need to define these functions to avoid fatal errors. if ( ! function_exists( 'do_action' ) ) { - function do_action() {} - function apply_filters() {} + function do_action() {} // phpcs:ignore + function apply_filters() {} // phpcs:ignore } // We also need to selectively load the necessary classes from the plugin. diff --git a/src/WP_SQLite_Export.php b/src/WP_SQLite_Export.php index d46b6eab..c7a451f5 100644 --- a/src/WP_SQLite_Export.php +++ b/src/WP_SQLite_Export.php @@ -76,6 +76,7 @@ protected function get_create_statement( $table, $translator ) { protected function get_insert_statements( $table, $pdo ) { $stmt = $pdo->prepare( 'SELECT * FROM ' . $table->name ); $stmt->execute(); + // phpcs:ignore while ( $row = $stmt->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT ) ) { yield sprintf( 'INSERT INTO `%1s` VALUES (%2s);', $table->name, $this->escape_values( $pdo, $row ) ); } diff --git a/src/WP_SQLite_Import.php b/src/WP_SQLite_Import.php index e8905f2d..27dd537a 100644 --- a/src/WP_SQLite_Import.php +++ b/src/WP_SQLite_Import.php @@ -45,6 +45,7 @@ public function parse_statements( $sql_file_path ) { $in_comment = false; $buffer = ''; + // phpcs:ignore while ( ( $line = fgets( $handle ) ) !== false ) { $line = trim( $line ); From c23bd71ede2f5c55b3f1dee1c0786299c67bc234 Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Tue, 23 Jul 2024 14:07:13 +0700 Subject: [PATCH 08/22] Add namespace to prevent linting error and move noop functions out of the namespace to ensure they are global --- src/DB_Command.php | 2 ++ src/WP_SQLite_Base.php | 17 ++++++++--------- src/WP_SQLite_Export.php | 6 ++++++ src/WP_SQLite_Import.php | 6 ++++++ src/noop.php | 15 +++++++++++++++ 5 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 src/noop.php diff --git a/src/DB_Command.php b/src/DB_Command.php index 70a291d9..7e318db9 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1,5 +1,7 @@ get_plugin_directory(); if ( ! $plugin_directory ) { - throw new Exception( 'Could not locate the SQLite integration plugin.' ); + throw new \Exception( 'Could not locate the SQLite integration plugin.' ); } // Load the translator class from the plugin. @@ -31,13 +32,11 @@ protected function load_dependencies() { define( 'SQLITE_DB_DROPIN_VERSION', self::get_sqlite_version() ); // phpcs:ignore } - # A hack to add the do_action and apply_filters functions to the global namespace. - # This is necessary because during the import WP has not been loaded, and we - # need to define these functions to avoid fatal errors. - if ( ! function_exists( 'do_action' ) ) { - function do_action() {} // phpcs:ignore - function apply_filters() {} // phpcs:ignore - } + # WordPress is not loaded during the execution of the export and import commands. + # The SQLite database integration plugin uses do_action and apply_filters to hook + # into the WordPress core. To prevent a fatal error, we can define these functions + # as no-op functions. + require_once __DIR__ . '/noop.php'; // We also need to selectively load the necessary classes from the plugin. require_once $plugin_directory . '/php-polyfills.php'; @@ -69,7 +68,7 @@ protected function get_plugin_directory() { protected function check_arguments( $args ) { if ( array_intersect_key( $args, array_flip( $this->unsupported_arguments ) ) ) { - WP_CLI::error( + \WP_CLI::error( sprintf( 'The following arguments are not supported by SQLite exports: %s', implode( ', ', $this->unsupported_arguments ) diff --git a/src/WP_SQLite_Export.php b/src/WP_SQLite_Export.php index c7a451f5..b85324d0 100644 --- a/src/WP_SQLite_Export.php +++ b/src/WP_SQLite_Export.php @@ -1,4 +1,10 @@ Date: Tue, 23 Jul 2024 19:49:27 +0700 Subject: [PATCH 09/22] Restore after_wp_load that was mistakenly removed --- src/DB_Command.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 7e318db9..9ae24f1b 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -859,7 +859,7 @@ public function import( $args, $assoc_args ) { * # Export only tables for a single site * $ wp db export --tables=$(wp db tables --url=sub.example.com --format=csv) * Success: Exported to wordpress_dbase.sql - * + * @when after_wp_load */ public function tables( $args, $assoc_args ) { From b10aa9e1ec9072f5b5aa42302580e02a3850424a Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Wed, 24 Jul 2024 14:58:56 +0700 Subject: [PATCH 10/22] Refactoring to ensure behat tests pass --- features/db-import.feature | 22 +++++++++++++++++----- src/DB_Command.php | 2 +- src/WP_SQLite_Export.php | 2 +- src/WP_SQLite_Import.php | 4 ++-- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/features/db-import.feature b/features/db-import.feature index af32c1be..83f0bf3b 100644 --- a/features/db-import.feature +++ b/features/db-import.feature @@ -36,13 +36,25 @@ Feature: Import a WordPress database Success: Imported from 'wp_cli_test.sql'. """ - Scenario: Import from STDIN +# Scenario: Import from STDIN +# Given a WP install +# +# When I run `wp db import -` +# Then STDOUT should be: +# """ +# Success: Imported from 'STDIN'. +# """ + + Scenario: Test plugin Given a WP install - - When I run `wp db import -` - Then STDOUT should be: + And these installed and active plugins: + """ + sqlite-database-integration + """ + When I run `wp plugin list` + Then STDOUT should contain: """ - Success: Imported from 'STDIN'. + sqlite-database-integration """ Scenario: Import from database name path by default and skip speed optimization diff --git a/src/DB_Command.php b/src/DB_Command.php index 9ae24f1b..65149248 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -775,7 +775,7 @@ public function import( $args, $assoc_args ) { // Check if SQLite is enabled and use it if it is. if ( WP_SQLite_Import::get_sqlite_version() ) { $importer = new WP_SQLite_Import(); - $importer->run( $args[0], $args ); + $importer->run( $result_file, $args ); return; } diff --git a/src/WP_SQLite_Export.php b/src/WP_SQLite_Export.php index b85324d0..e77496f2 100644 --- a/src/WP_SQLite_Export.php +++ b/src/WP_SQLite_Export.php @@ -43,7 +43,7 @@ public function run( $result_file, $args ) { $include_tables = isset( $args['tables'] ) ? explode( ',', $args['tables'] ) : []; $translator = new WP_SQLite_Translator(); - $handle = fopen( 'export.sql', 'w' ); + $handle = fopen( $result_file, 'w' ); foreach ( $translator->query( 'SHOW TABLES' ) as $table ) { diff --git a/src/WP_SQLite_Import.php b/src/WP_SQLite_Import.php index 96cccaa9..eddf6b7b 100644 --- a/src/WP_SQLite_Import.php +++ b/src/WP_SQLite_Import.php @@ -12,8 +12,6 @@ class WP_SQLite_Import extends WP_SQLite_Base { 'skip-optimization', 'defaults', 'fields', - 'dbuser', - 'dbpass', ]; /** @@ -31,6 +29,8 @@ public function run( $sql_file_path, $args ) { WP_CLI::warning( 'Could not execute statement: ' . $statement ); } } + + WP_CLI::success( sprintf("Imported from '%s'.", $sql_file_path ) ); } /** From 1d369699f8b08e2f5a12a6f05bf06976db65740d Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Wed, 24 Jul 2024 16:52:28 +0700 Subject: [PATCH 11/22] Fix behat tests and prevent them from freezing --- features/db-export.feature | 4 ++ features/db-import.feature | 38 +++++++++---------- features/db-query.feature | 8 ++-- src/DB_Command.php | 24 ++++++------ src/{WP_SQLite_Base.php => SQLite/Base.php} | 4 +- .../Export.php} | 18 ++++++--- .../Import.php} | 19 +++++++--- src/{ => SQLite}/noop.php | 0 8 files changed, 66 insertions(+), 49 deletions(-) rename src/{WP_SQLite_Base.php => SQLite/Base.php} (98%) rename src/{WP_SQLite_Export.php => SQLite/Export.php} (88%) rename src/{WP_SQLite_Import.php => SQLite/Import.php} (83%) rename src/{ => SQLite}/noop.php (100%) diff --git a/features/db-export.feature b/features/db-export.feature index 932e12ed..11354f75 100644 --- a/features/db-export.feature +++ b/features/db-export.feature @@ -43,6 +43,7 @@ Feature: Export a WordPress database -- Dump completed on """ + @require-mysql Scenario: Export database with mysql defaults to STDOUT Given a WP install @@ -52,6 +53,7 @@ Feature: Export a WordPress database -- Dump completed on """ + @require-mysql Scenario: Export database with mysql --no-defaults to STDOUT Given a WP install @@ -61,6 +63,7 @@ Feature: Export a WordPress database -- Dump completed on """ + @require-mysql Scenario: Export database with passed-in options Given a WP install @@ -78,6 +81,7 @@ Feature: Export a WordPress database """ And STDOUT should be empty + @require-mysql Scenario: MySQL defaults are available as appropriate with --defaults flag Given a WP install diff --git a/features/db-import.feature b/features/db-import.feature index 83f0bf3b..b06344cb 100644 --- a/features/db-import.feature +++ b/features/db-import.feature @@ -12,6 +12,7 @@ Feature: Import a WordPress database Success: Imported from 'wp_cli_test.sql'. """ + @require-mysql Scenario: Import from database name path by default with mysql defaults Given a WP install @@ -24,6 +25,7 @@ Feature: Import a WordPress database Success: Imported from 'wp_cli_test.sql'. """ + @require-mysql Scenario: Import from database name path by default with --no-defaults Given a WP install @@ -36,27 +38,19 @@ Feature: Import a WordPress database Success: Imported from 'wp_cli_test.sql'. """ -# Scenario: Import from STDIN -# Given a WP install -# -# When I run `wp db import -` -# Then STDOUT should be: -# """ -# Success: Imported from 'STDIN'. -# """ + Scenario: Import from STDIN + Given a WP install - Scenario: Test plugin - Given a WP install - And these installed and active plugins: - """ - sqlite-database-integration - """ - When I run `wp plugin list` - Then STDOUT should contain: - """ - sqlite-database-integration - """ + When I run `wp db export wp_cli_test.sql` + Then the wp_cli_test.sql file should exist + When I run `cat wp_cli_test.sql | wp db import -` + Then STDOUT should be: + """ + Success: Imported from 'STDIN'. + """ + + @require-mysql Scenario: Import from database name path by default and skip speed optimization Given a WP install @@ -68,7 +62,7 @@ Feature: Import a WordPress database """ Success: Imported from 'wp_cli_test.sql'. """ - + @require-mysql Scenario: Import from database name path by default with passed-in dbuser/dbpass Given a WP install @@ -103,6 +97,7 @@ Feature: Import a WordPress database Success: Imported from 'debug.sql'. """ + @require-mysql Scenario: Help runs properly at various points of a functional WP install Given an empty directory @@ -140,6 +135,7 @@ Feature: Import a WordPress database """ wp db import """ + @require-mysql Scenario: MySQL defaults are available as appropriate with --defaults flag Given a WP install @@ -164,7 +160,7 @@ Feature: Import a WordPress database Debug (db): Running shell command: /usr/bin/env mysql --no-defaults --no-auto-rehash """ - @require-wp-4.2 + @require-wp-4.2 @require-mysql Scenario: Import db that has emoji in post Given a WP install diff --git a/features/db-query.feature b/features/db-query.feature index 627ef534..f3d6bec9 100644 --- a/features/db-query.feature +++ b/features/db-query.feature @@ -75,23 +75,23 @@ Feature: Query the database with WordPress' MySQL config Scenario: MySQL defaults are available as appropriate with --defaults flag Given a WP install - When I try `wp db query --defaults --debug` + When I try `"select 1" | wp db query --defaults --debug` Then STDERR should contain: """ Debug (db): Running shell command: /usr/bin/env mysql --no-auto-rehash """ - When I try `wp db query --debug` + When I try `"select 1" | wp db query --debug` Then STDERR should contain: """ Debug (db): Running shell command: /usr/bin/env mysql --no-defaults --no-auto-rehash """ - When I try `wp db query --no-defaults --debug` + When I try `"select 1" | wp db query --no-defaults --debug` Then STDERR should contain: """ Debug (db): Running shell command: /usr/bin/env mysql --no-defaults --no-auto-rehash - """ + """ Scenario: SQL modes do not include any of the modes incompatible with WordPress Given a WP install diff --git a/src/DB_Command.php b/src/DB_Command.php index 65149248..9af41d2c 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -1,7 +1,7 @@ run( $result_file, $assoc_args ); - return; - } - $stdout = ( '-' === $result_file ); $porcelain = Utils\get_flag_value( $assoc_args, 'porcelain' ); @@ -614,6 +607,13 @@ public function export( $args, $assoc_args ) { WP_CLI::error( 'Porcelain is not allowed when output mode is STDOUT.' ); } + // Check if SQLite is enabled and use it if it is. + if ( Export::get_sqlite_version() ) { + $export = new Export(); + $export->run( $result_file, $assoc_args ); + return; + } + if ( ! $stdout ) { $assoc_args['result-file'] = $result_file; } @@ -773,9 +773,9 @@ public function import( $args, $assoc_args ) { } // Check if SQLite is enabled and use it if it is. - if ( WP_SQLite_Import::get_sqlite_version() ) { - $importer = new WP_SQLite_Import(); - $importer->run( $result_file, $args ); + if ( Import::get_sqlite_version() ) { + $importer = new Import(); + $importer->run( $result_file, $assoc_args ); return; } diff --git a/src/WP_SQLite_Base.php b/src/SQLite/Base.php similarity index 98% rename from src/WP_SQLite_Base.php rename to src/SQLite/Base.php index 94e725b0..0f249627 100644 --- a/src/WP_SQLite_Base.php +++ b/src/SQLite/Base.php @@ -1,7 +1,7 @@ check_arguments( $args ); $this->load_dependencies(); + $is_stdout = '-' === $result_file; $exclude_tables = isset( $args['exclude_tables'] ) ? explode( ',', $args['exclude_tables'] ) : []; $exclude_tables = array_merge( @@ -43,7 +44,7 @@ public function run( $result_file, $args ) { $include_tables = isset( $args['tables'] ) ? explode( ',', $args['tables'] ) : []; $translator = new WP_SQLite_Translator(); - $handle = fopen( $result_file, 'w' ); + $handle = $is_stdout ? fopen( 'php://stdout', 'w' ) : fopen( $result_file, 'w' ); foreach ( $translator->query( 'SHOW TABLES' ) as $table ) { @@ -65,13 +66,20 @@ public function run( $result_file, $args ) { } } + fwrite( $handle, sprintf( '-- Dump completed on %s', date('c') ) ); + fclose( $handle ); + + if ( $is_stdout ) { + return; + } + if ( isset( $args['porcelain'] ) ) { WP_CLI::line( $result_file ); } else { - WP_CLI::line( 'Export complete. File written to ' . $result_file ); + WP_CLI::success( 'Export complete. File written to ' . $result_file ); } - fclose( $handle ); + } protected function get_create_statement( $table, $translator ) { diff --git a/src/WP_SQLite_Import.php b/src/SQLite/Import.php similarity index 83% rename from src/WP_SQLite_Import.php rename to src/SQLite/Import.php index eddf6b7b..44538158 100644 --- a/src/WP_SQLite_Import.php +++ b/src/SQLite/Import.php @@ -1,17 +1,19 @@ check_arguments( $args ); $this->load_dependencies(); $translator = new WP_SQLite_Translator(); - foreach ( $this->parse_statements( $sql_file_path ) as $statement ) { + + $is_stdin = '-' === $sql_file_path; + $import_file = $is_stdin ? 'php://stdin' : $sql_file_path; + + foreach ( $this->parse_statements( $import_file ) as $statement ) { $result = $translator->query( $statement ); if ( false === $result ) { WP_CLI::warning( 'Could not execute statement: ' . $statement ); } } - WP_CLI::success( sprintf("Imported from '%s'.", $sql_file_path ) ); + $imported_from = $is_stdin ? 'STDIN' : $sql_file_path; + WP_CLI::success( sprintf("Imported from '%s'.", $imported_from ) ); } /** @@ -41,9 +48,11 @@ public function run( $sql_file_path, $args ) { * @throws Exception */ public function parse_statements( $sql_file_path ) { + $handle = fopen( $sql_file_path, 'r' ); + if ( ! $handle ) { - throw new Exception( "Unable to open file: $sql_file_path" ); + WP_CLI::error( "Unable to open file: $sql_file_path" ); } $single_quotes = 0; diff --git a/src/noop.php b/src/SQLite/noop.php similarity index 100% rename from src/noop.php rename to src/SQLite/noop.php From d33bb6651a4817259a141ef0bbde7766b7a5286e Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Wed, 24 Jul 2024 17:14:40 +0700 Subject: [PATCH 12/22] Linter fixes --- src/SQLite/Export.php | 4 +--- src/SQLite/Import.php | 5 +++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/SQLite/Export.php b/src/SQLite/Export.php index 9fd44805..63e1ad59 100644 --- a/src/SQLite/Export.php +++ b/src/SQLite/Export.php @@ -66,7 +66,7 @@ public function run( $result_file, $args ) { } } - fwrite( $handle, sprintf( '-- Dump completed on %s', date('c') ) ); + fwrite( $handle, sprintf( '-- Dump completed on %s', gmdate( 'c' ) ) ); fclose( $handle ); if ( $is_stdout ) { @@ -78,8 +78,6 @@ public function run( $result_file, $args ) { } else { WP_CLI::success( 'Export complete. File written to ' . $result_file ); } - - } protected function get_create_statement( $table, $translator ) { diff --git a/src/SQLite/Import.php b/src/SQLite/Import.php index 44538158..c676046d 100644 --- a/src/SQLite/Import.php +++ b/src/SQLite/Import.php @@ -8,6 +8,7 @@ class Import extends Base { + protected $unsupported_arguments = [ 'skip-optimization', 'defaults', @@ -26,7 +27,7 @@ public function run( $sql_file_path, $args ) { $this->load_dependencies(); $translator = new WP_SQLite_Translator(); - $is_stdin = '-' === $sql_file_path; + $is_stdin = '-' === $sql_file_path; $import_file = $is_stdin ? 'php://stdin' : $sql_file_path; foreach ( $this->parse_statements( $import_file ) as $statement ) { @@ -37,7 +38,7 @@ public function run( $sql_file_path, $args ) { } $imported_from = $is_stdin ? 'STDIN' : $sql_file_path; - WP_CLI::success( sprintf("Imported from '%s'.", $imported_from ) ); + WP_CLI::success( sprintf( "Imported from '%s'.", $imported_from ) ); } /** From 74e4008471a0f921b9942b42f156b752f3a061d5 Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Wed, 24 Jul 2024 18:42:04 +0700 Subject: [PATCH 13/22] Check plugin version --- src/DB_Command.php | 4 +-- src/SQLite/Base.php | 69 +++++++++++++++++++++++++++++---------------- 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/DB_Command.php b/src/DB_Command.php index 9af41d2c..617a1456 100644 --- a/src/DB_Command.php +++ b/src/DB_Command.php @@ -608,7 +608,7 @@ public function export( $args, $assoc_args ) { } // Check if SQLite is enabled and use it if it is. - if ( Export::get_sqlite_version() ) { + if ( Export::get_sqlite_plugin_version() ) { $export = new Export(); $export->run( $result_file, $assoc_args ); return; @@ -773,7 +773,7 @@ public function import( $args, $assoc_args ) { } // Check if SQLite is enabled and use it if it is. - if ( Import::get_sqlite_version() ) { + if ( Import::get_sqlite_plugin_version() ) { $importer = new Import(); $importer->run( $result_file, $assoc_args ); return; diff --git a/src/SQLite/Base.php b/src/SQLite/Base.php index 0f249627..219c83b7 100644 --- a/src/SQLite/Base.php +++ b/src/SQLite/Base.php @@ -10,26 +10,65 @@ class Base { * for an active sqlite integration plugin. * @return false|void */ - public static function get_sqlite_version() { + public static function get_sqlite_plugin_version() { // Check if there is a db.php file in the wp-content directory. if ( ! file_exists( ABSPATH . '/wp-content/db.php' ) ) { return false; } // If the file is found, we need to check that it is the sqlite integration plugin. $plugin_file = file_get_contents( ABSPATH . '/wp-content/db.php' ); - preg_match( '/define\( \'SQLITE_DB_DROPIN_VERSION\', \'([0-9.]+)\' \)/', $plugin_file, $matches ); - return isset( $matches[1] ) ? $matches[1] : false; + if ( ! preg_match( '/define\( \'SQLITE_DB_DROPIN_VERSION\', \'([0-9.]+)\' \)/', $plugin_file ) ) { + return false; + } + + $plugin_path = self::get_plugin_directory(); + if ( ! $plugin_path ) { + return false; + } + + $plugin_file = file_get_contents( $plugin_path . '/readme.txt' ); + + preg_match( '/^Stable tag:\s*?(.+)$/m', $plugin_file, $matches ); + + return isset( $matches[1] ) ? trim( $matches[1] ) : false; + } + + /** + * @return string|null + */ + protected static function get_plugin_directory() { + $plugin_folders = [ + ABSPATH . '/wp-content/plugins/sqlite-database-integration', + ABSPATH . '/wp-content/mu-plugins/sqlite-database-integration', + ]; + + foreach ( $plugin_folders as $folder ) { + if ( file_exists( $folder ) && is_dir( $folder ) ) { + return $folder; + } + } + + return null; } protected function load_dependencies() { - $plugin_directory = $this->get_plugin_directory(); + $plugin_directory = self::get_plugin_directory(); if ( ! $plugin_directory ) { - throw new \Exception( 'Could not locate the SQLite integration plugin.' ); + \WP_CLI::error( 'Could not locate the SQLite integration plugin.' ); + } + + $sqlite_plugin_version = self::get_sqlite_plugin_version(); + if ( ! $sqlite_plugin_version ) { + \WP_CLI::error( 'Could not determine the version of the SQLite integration plugin.' ); + } + + if ( version_compare( $sqlite_plugin_version, '2.1.11', '<' ) ) { + \WP_CLI::error( 'The SQLite integration plugin must be version 2.1.11 or higher.' ); } // Load the translator class from the plugin. if ( ! defined( 'SQLITE_DB_DROPIN_VERSION' ) ) { - define( 'SQLITE_DB_DROPIN_VERSION', self::get_sqlite_version() ); // phpcs:ignore + define( 'SQLITE_DB_DROPIN_VERSION', $sqlite_plugin_version ); // phpcs:ignore } # WordPress is not loaded during the execution of the export and import commands. @@ -48,24 +87,6 @@ protected function load_dependencies() { require_once $plugin_directory . '/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php'; } - /** - * @return string|null - */ - protected function get_plugin_directory() { - $plugin_folders = [ - ABSPATH . '/wp-content/plugins/sqlite-database-integration', - ABSPATH . '/wp-content/mu-plugins/sqlite-database-integration', - ]; - - foreach ( $plugin_folders as $folder ) { - if ( file_exists( $folder ) && is_dir( $folder ) ) { - return $folder; - } - } - - return null; - } - protected function check_arguments( $args ) { if ( array_intersect_key( $args, array_flip( $this->unsupported_arguments ) ) ) { \WP_CLI::error( From 73fb1d307f34a99d4bbfb42c2e9dcea3437d5f50 Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Thu, 25 Jul 2024 15:06:28 +0700 Subject: [PATCH 14/22] Import WP_CLI --- src/SQLite/Base.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/SQLite/Base.php b/src/SQLite/Base.php index 219c83b7..aec6cdf2 100644 --- a/src/SQLite/Base.php +++ b/src/SQLite/Base.php @@ -1,6 +1,8 @@ unsupported_arguments ) ) ) { - \WP_CLI::error( + WP_CLI::error( sprintf( 'The following arguments are not supported by SQLite exports: %s', implode( ', ', $this->unsupported_arguments ) From 377a4508d2f59f8f8edc5ff65b30f171257f70ac Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Thu, 25 Jul 2024 16:06:20 +0700 Subject: [PATCH 15/22] refactoring --- src/SQLite/Base.php | 27 +++++-- src/SQLite/Export.php | 164 ++++++++++++++++++++++++++++++++++-------- src/SQLite/Import.php | 5 +- 3 files changed, 160 insertions(+), 36 deletions(-) diff --git a/src/SQLite/Base.php b/src/SQLite/Base.php index aec6cdf2..5c866348 100644 --- a/src/SQLite/Base.php +++ b/src/SQLite/Base.php @@ -8,15 +8,17 @@ class Base { protected $unsupported_arguments = []; /** - * Tries to determine if the current site is using SQL by checking - * for an active sqlite integration plugin. - * @return false|void + * Get the version of the SQLite integration plugin if it is installed + * and activated. + * + * @return false|string The version of the SQLite integration plugin or false if not found/activated. */ public static function get_sqlite_plugin_version() { // Check if there is a db.php file in the wp-content directory. if ( ! file_exists( ABSPATH . '/wp-content/db.php' ) ) { return false; } + // If the file is found, we need to check that it is the sqlite integration plugin. $plugin_file = file_get_contents( ABSPATH . '/wp-content/db.php' ); if ( ! preg_match( '/define\( \'SQLITE_DB_DROPIN_VERSION\', \'([0-9.]+)\' \)/', $plugin_file ) ) { @@ -28,6 +30,7 @@ public static function get_sqlite_plugin_version() { return false; } + // Try to get the version number from readme.txt $plugin_file = file_get_contents( $plugin_path . '/readme.txt' ); preg_match( '/^Stable tag:\s*?(.+)$/m', $plugin_file, $matches ); @@ -36,7 +39,9 @@ public static function get_sqlite_plugin_version() { } /** - * @return string|null + * Find the directory where the SQLite integration plugin is installed. + * + * @return string|null The directory where the SQLite integration plugin is installed or null if not found. */ protected static function get_plugin_directory() { $plugin_folders = [ @@ -53,6 +58,12 @@ protected static function get_plugin_directory() { return null; } + /** + * Load the necessary classes from the SQLite integration plugin. + * + * @return void + * @throws WP_CLI\ExitException + */ protected function load_dependencies() { $plugin_directory = self::get_plugin_directory(); if ( ! $plugin_directory ) { @@ -89,6 +100,14 @@ protected function load_dependencies() { require_once $plugin_directory . '/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php'; } + /** + * Check if the arguments passed to the command are supported. + * + * @param $args + * + * @return void + * @throws WP_CLI\ExitException + */ protected function check_arguments( $args ) { if ( array_intersect_key( $args, array_flip( $this->unsupported_arguments ) ) ) { WP_CLI::error( diff --git a/src/SQLite/Export.php b/src/SQLite/Export.php index 63e1ad59..4abc15e4 100644 --- a/src/SQLite/Export.php +++ b/src/SQLite/Export.php @@ -8,6 +8,10 @@ class Export extends Base { + /** + * List of arguments that are not supported by the export command. + * @var string[] + */ protected $unsupported_arguments = [ 'fields', 'include-tablespaces', @@ -16,6 +20,15 @@ class Export extends Base { 'dbpass', ]; + protected $translator; + protected $args = array(); + protected $is_stdout = false; + + public function __construct() { + $this->load_dependencies(); + $this->translator = new WP_SQLite_Translator(); + } + /** * Run the export command. * @@ -26,26 +39,74 @@ class Export extends Base { * @throws Exception */ public function run( $result_file, $args ) { - + $this->args = $args; $this->check_arguments( $args ); - $this->load_dependencies(); - $is_stdout = '-' === $result_file; - $exclude_tables = isset( $args['exclude_tables'] ) ? explode( ',', $args['exclude_tables'] ) : []; - $exclude_tables = array_merge( - $exclude_tables, - [ - '_mysql_data_types_cache', - 'sqlite_master', - 'sqlite_sequence', - ] - ); + $handle = $this->open_output_stream( $result_file ); - $include_tables = isset( $args['tables'] ) ? explode( ',', $args['tables'] ) : []; + $this->write_sql_statements( $handle ); + $this->close_output_stream( $handle ); - $translator = new WP_SQLite_Translator(); - $handle = $is_stdout ? fopen( 'php://stdout', 'w' ) : fopen( $result_file, 'w' ); + $this->display_result_message( $result_file ); + } + + /** + * Get output stream for the export. + * + * @param $result_file + * + * @return false|resource + * @throws WP_CLI\ExitException + */ + protected function open_output_stream( $result_file ) { + $this->is_stdout = '-' === $result_file; + $handle = $this->is_stdout ? fopen( 'php://stdout', 'w' ) : fopen( $result_file, 'w' ); + if ( ! $handle ) { + WP_CLI::error( "Unable to open file: $result_file" ); + } + return $handle; + } + /** + * Close the output stream. + * + * @param $handle + * + * @return void + * @throws WP_CLI\ExitException + */ + protected function close_output_stream( $handle ) { + if ( ! fclose( $handle ) ) { + WP_CLI::error( 'Error closing output stream.' ); + } + } + + /** + * Write SQL statements to the output stream. + * + * @param resource $handle The output stream. + * + * @return void + * @throws Exception + */ + protected function write_sql_statements( $handle ) { + foreach ( $this->get_sql_statements() as $statement ) { + fwrite( $handle, $statement . PHP_EOL ); + } + + fwrite( $handle, sprintf( '-- Dump completed on %s', gmdate( 'c' ) ) ); + } + + /** + * Get SQL statements for the dump. + * + * @return \Generator + * @throws Exception + */ + protected function get_sql_statements() { + $include_tables = $this->get_include_tables(); + $exclude_tables = $this->get_exclude_tables(); + $translator = $this->translator; foreach ( $translator->query( 'SHOW TABLES' ) as $table ) { // Skip tables that are not in the include_tables list if the list is defined @@ -58,33 +119,36 @@ public function run( $result_file, $args ) { continue; } - fwrite( $handle, 'DROP TABLE IF EXISTS `' . $table->name . "`;\n" ); - fwrite( $handle, $this->get_create_statement( $table, $translator ) . "\n" ); + yield sprintf( 'DROP TABLE IF EXISTS `%s`;', $table->name ); + yield $this->get_create_statement( $table, $translator ); foreach ( $this->get_insert_statements( $table, $translator->get_pdo() ) as $insert_statement ) { - fwrite( $handle, $insert_statement . "\n" ); + yield $insert_statement; } } - - fwrite( $handle, sprintf( '-- Dump completed on %s', gmdate( 'c' ) ) ); - fclose( $handle ); - - if ( $is_stdout ) { - return; - } - - if ( isset( $args['porcelain'] ) ) { - WP_CLI::line( $result_file ); - } else { - WP_CLI::success( 'Export complete. File written to ' . $result_file ); - } } + /** + * Get the CREATE TABLE statement for a table. + * + * @param $table + * @param $translator + * + * @return mixed + */ protected function get_create_statement( $table, $translator ) { $create = $translator->query( 'SHOW CREATE TABLE ' . $table->name ); return $create[0]->{'Create Table'}; } + /** + * Get the INSERT statements for a table. + * + * @param $table + * @param $pdo + * + * @return \Generator + */ protected function get_insert_statements( $table, $pdo ) { $stmt = $pdo->prepare( 'SELECT * FROM ' . $table->name ); $stmt->execute(); @@ -94,6 +158,44 @@ protected function get_insert_statements( $table, $pdo ) { } } + /** + * Get the tables to exclude from the export. + * + * @return array|false|string[] + */ + protected function get_exclude_tables() { + $exclude_tables = isset( $this->args['exclude_tables'] ) ? explode( ',', $this->args['exclude_tables'] ) : []; + return array_merge( + $exclude_tables, + [ + '_mysql_data_types_cache', + 'sqlite_master', + 'sqlite_sequence', + ] + ); + } + + protected function display_result_message( $result_file ) { + if ( $this->is_stdout ) { + return; + } + + if ( isset( $this->args['porcelain'] ) ) { + WP_CLI::line( $result_file ); + } else { + WP_CLI::success( 'Export complete. File written to ' . $result_file ); + } + } + + /** + * Get the tables to include in the export. + * + * @return array|false|string[] + */ + protected function get_include_tables() { + return isset( $this->args['tables'] ) ? explode( ',', $this->args['tables'] ) : []; + } + /** * Escape values for insert statement * diff --git a/src/SQLite/Import.php b/src/SQLite/Import.php index c676046d..edfa803c 100644 --- a/src/SQLite/Import.php +++ b/src/SQLite/Import.php @@ -8,7 +8,6 @@ class Import extends Base { - protected $unsupported_arguments = [ 'skip-optimization', 'defaults', @@ -17,12 +16,16 @@ class Import extends Base { 'dbpass', ]; + protected $translator; + protected $args; + /** * Execute the import command for SQLite. * * @throws Exception */ public function run( $sql_file_path, $args ) { + $this->args = $args; $this->check_arguments( $args ); $this->load_dependencies(); $translator = new WP_SQLite_Translator(); From 46863b86fcc312eb74ec08c9307997d4fc0fe061 Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Thu, 25 Jul 2024 16:49:31 +0700 Subject: [PATCH 16/22] Refactoring --- src/SQLite/Import.php | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/SQLite/Import.php b/src/SQLite/Import.php index edfa803c..33e26f73 100644 --- a/src/SQLite/Import.php +++ b/src/SQLite/Import.php @@ -19,29 +19,47 @@ class Import extends Base { protected $translator; protected $args; + public function __construct() { + $this->load_dependencies(); + $this->translator = new WP_SQLite_Translator(); + } + /** * Execute the import command for SQLite. * - * @throws Exception + * @param string $sql_file_path The path to the SQL dump file. + * @param array $args The arguments passed to the command. + * + * @return void */ public function run( $sql_file_path, $args ) { $this->args = $args; $this->check_arguments( $args ); - $this->load_dependencies(); - $translator = new WP_SQLite_Translator(); $is_stdin = '-' === $sql_file_path; $import_file = $is_stdin ? 'php://stdin' : $sql_file_path; + $this->execute_statements( $import_file ); + + $imported_from = $is_stdin ? 'STDIN' : $sql_file_path; + WP_CLI::success( sprintf( "Imported from '%s'.", $imported_from ) ); + } + + /** + * Execute SQL statements from an SQL dump file. + * + * @param $import_file + * + * @return void + * @throws Exception + */ + protected function execute_statements( $import_file ) { foreach ( $this->parse_statements( $import_file ) as $statement ) { - $result = $translator->query( $statement ); + $result = $this->translator->query( $statement ); if ( false === $result ) { WP_CLI::warning( 'Could not execute statement: ' . $statement ); } } - - $imported_from = $is_stdin ? 'STDIN' : $sql_file_path; - WP_CLI::success( sprintf( "Imported from '%s'.", $imported_from ) ); } /** @@ -49,7 +67,6 @@ public function run( $sql_file_path, $args ) { * @param string $sql_file_path The path to the SQL dump file. * * @return Generator A generator that yields SQL statements. - * @throws Exception */ public function parse_statements( $sql_file_path ) { From 2753c7947cf9e0c56364ab0c88cb9d41e21d381a Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Mon, 29 Jul 2024 21:37:29 +0700 Subject: [PATCH 17/22] Ensure that insert statements appear on one line --- src/SQLite/Export.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SQLite/Export.php b/src/SQLite/Export.php index 4abc15e4..a459f51a 100644 --- a/src/SQLite/Export.php +++ b/src/SQLite/Export.php @@ -213,7 +213,8 @@ protected function escape_values( PDO $pdo, $values ) { } elseif ( is_numeric( $value ) ) { $escaped_values[] = $value; } else { - $escaped_values[] = $pdo->quote( $value ); + // Quote the values and escape encode the newlines so the insert statement appears on a single line. + $escaped_values[] = str_replace( "\n", "\\n", $pdo->quote( $value ) ); } } return implode( ',', $escaped_values ); From 9085b5e679a35ca6ce22c846ee48cfbc0c9669c4 Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Mon, 29 Jul 2024 21:58:22 +0700 Subject: [PATCH 18/22] Add comments to the dump --- src/SQLite/Export.php | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/SQLite/Export.php b/src/SQLite/Export.php index a459f51a..c8e489cb 100644 --- a/src/SQLite/Export.php +++ b/src/SQLite/Export.php @@ -119,12 +119,21 @@ protected function get_sql_statements() { continue; } + // Create table statement + yield $this->get_dump_comment( sprintf( 'Table structure for table `%s`', $table->name ) ) . "\n"; yield sprintf( 'DROP TABLE IF EXISTS `%s`;', $table->name ); yield $this->get_create_statement( $table, $translator ); + // Insert statements + if ( ! $this->table_has_rows( $table ) ) { + continue; + } + + yield $this->get_dump_comment( sprintf( 'Dumping data for table `%s`', $table->name ) ) . "\n"; foreach ( $this->get_insert_statements( $table, $translator->get_pdo() ) as $insert_statement ) { yield $insert_statement; } + yield "\n"; } } @@ -138,7 +147,7 @@ protected function get_sql_statements() { */ protected function get_create_statement( $table, $translator ) { $create = $translator->query( 'SHOW CREATE TABLE ' . $table->name ); - return $create[0]->{'Create Table'}; + return $create[0]->{'Create Table'} . "\n"; } /** @@ -219,4 +228,25 @@ protected function escape_values( PDO $pdo, $values ) { } return implode( ',', $escaped_values ); } + + /** + * Get a comment for the dump. + * + * @param $comment + * + * @return string + */ + protected function get_dump_comment( $comment ) { + return implode( + "\n", + array( '--', sprintf( '-- %s', $comment ), '--' ) + ); + } + + protected function table_has_rows( $table ) { + $pdo = $this->translator->get_pdo(); + $stmt = $pdo->prepare( 'SELECT COUNT(*) FROM ' . $table->name ); + $stmt->execute(); + return $stmt->fetchColumn() > 0; + } } From 69dfb06b70306aaac6550e807ebb809fba928cec Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Tue, 30 Jul 2024 12:09:03 +0700 Subject: [PATCH 19/22] Remove noop version of apply_filters and do_action --- src/SQLite/Base.php | 6 ------ src/SQLite/noop.php | 15 --------------- 2 files changed, 21 deletions(-) delete mode 100644 src/SQLite/noop.php diff --git a/src/SQLite/Base.php b/src/SQLite/Base.php index 5c866348..0df48832 100644 --- a/src/SQLite/Base.php +++ b/src/SQLite/Base.php @@ -84,12 +84,6 @@ protected function load_dependencies() { define( 'SQLITE_DB_DROPIN_VERSION', $sqlite_plugin_version ); // phpcs:ignore } - # WordPress is not loaded during the execution of the export and import commands. - # The SQLite database integration plugin uses do_action and apply_filters to hook - # into the WordPress core. To prevent a fatal error, we can define these functions - # as no-op functions. - require_once __DIR__ . '/noop.php'; - // We also need to selectively load the necessary classes from the plugin. require_once $plugin_directory . '/php-polyfills.php'; require_once $plugin_directory . '/constants.php'; diff --git a/src/SQLite/noop.php b/src/SQLite/noop.php deleted file mode 100644 index 3d15ddce..00000000 --- a/src/SQLite/noop.php +++ /dev/null @@ -1,15 +0,0 @@ - Date: Tue, 30 Jul 2024 14:08:21 +0700 Subject: [PATCH 20/22] Refactor export --- src/SQLite/Export.php | 100 +++++++++++++++++++++++++----------------- 1 file changed, 59 insertions(+), 41 deletions(-) diff --git a/src/SQLite/Export.php b/src/SQLite/Export.php index c8e489cb..9ccba0c6 100644 --- a/src/SQLite/Export.php +++ b/src/SQLite/Export.php @@ -90,25 +90,9 @@ protected function close_output_stream( $handle ) { * @throws Exception */ protected function write_sql_statements( $handle ) { - foreach ( $this->get_sql_statements() as $statement ) { - fwrite( $handle, $statement . PHP_EOL ); - } - - fwrite( $handle, sprintf( '-- Dump completed on %s', gmdate( 'c' ) ) ); - } - - /** - * Get SQL statements for the dump. - * - * @return \Generator - * @throws Exception - */ - protected function get_sql_statements() { $include_tables = $this->get_include_tables(); $exclude_tables = $this->get_exclude_tables(); - $translator = $this->translator; - foreach ( $translator->query( 'SHOW TABLES' ) as $table ) { - + foreach ( $this->translator->query( 'SHOW TABLES' ) as $table ) { // Skip tables that are not in the include_tables list if the list is defined if ( ! empty( $include_tables ) && ! in_array( $table->name, $include_tables, true ) ) { continue; @@ -119,51 +103,78 @@ protected function get_sql_statements() { continue; } - // Create table statement - yield $this->get_dump_comment( sprintf( 'Table structure for table `%s`', $table->name ) ) . "\n"; - yield sprintf( 'DROP TABLE IF EXISTS `%s`;', $table->name ); - yield $this->get_create_statement( $table, $translator ); + $this->write_create_table_statement( $handle, $table->name ); + $this->write_insert_statements( $handle, $table->name ); + } - // Insert statements - if ( ! $this->table_has_rows( $table ) ) { - continue; - } + fwrite( $handle, sprintf( '-- Dump completed on %s', gmdate( 'c' ) ) ); + } - yield $this->get_dump_comment( sprintf( 'Dumping data for table `%s`', $table->name ) ) . "\n"; - foreach ( $this->get_insert_statements( $table, $translator->get_pdo() ) as $insert_statement ) { - yield $insert_statement; - } - yield "\n"; + /** + * Write the create statement for a table to the output stream. + * + * @param resource $handle + * @param string $table_name + * + * @throws Exception + */ + protected function write_create_table_statement( $handle, $table_name ) { + $comment = $this->get_dump_comment( sprintf( 'Table structure for table `%s`', $table_name ) ); + fwrite( $handle, $comment . PHP_EOL . PHP_EOL ); + fwrite( $handle, sprintf( 'DROP TABLE IF EXISTS `%s`;', $table_name ) . PHP_EOL ); + fwrite( $handle, $this->get_create_statement( $table_name ) . PHP_EOL ); + } + + /** + * Write the insert statements for a table to the output stream. + * + * @param $handle + * @param $table_name + * + * @return void + */ + protected function write_insert_statements( $handle, $table_name ) { + + if ( ! $this->table_has_records( $table_name ) ) { + return; + } + + $comment = $this->get_dump_comment( sprintf( 'Dumping data for table `%s`', $table_name ) ); + fwrite( $handle, $comment . PHP_EOL . PHP_EOL ); + foreach ( $this->get_insert_statements( $table_name ) as $insert_statement ) { + fwrite( $handle, $insert_statement . PHP_EOL ); } + + fwrite( $handle, PHP_EOL ); } /** * Get the CREATE TABLE statement for a table. * - * @param $table - * @param $translator + * @param string $table_name * * @return mixed + * @throws Exception */ - protected function get_create_statement( $table, $translator ) { - $create = $translator->query( 'SHOW CREATE TABLE ' . $table->name ); + protected function get_create_statement( $table_name ) { + $create = $this->translator->query( 'SHOW CREATE TABLE ' . $table_name ); return $create[0]->{'Create Table'} . "\n"; } /** * Get the INSERT statements for a table. * - * @param $table - * @param $pdo + * @param string $table_name * * @return \Generator */ - protected function get_insert_statements( $table, $pdo ) { - $stmt = $pdo->prepare( 'SELECT * FROM ' . $table->name ); + protected function get_insert_statements( $table_name ) { + $pdo = $this->translator->get_pdo(); + $stmt = $pdo->prepare( 'SELECT * FROM ' . $table_name ); $stmt->execute(); // phpcs:ignore while ( $row = $stmt->fetch( PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT ) ) { - yield sprintf( 'INSERT INTO `%1s` VALUES (%2s);', $table->name, $this->escape_values( $pdo, $row ) ); + yield sprintf( 'INSERT INTO `%1s` VALUES (%2s);', $table_name, $this->escape_values( $pdo, $row ) ); } } @@ -243,9 +254,16 @@ protected function get_dump_comment( $comment ) { ); } - protected function table_has_rows( $table ) { + /** + * Check if the given table has records. + * + * @param string $table_name + * + * @return bool + */ + protected function table_has_records( $table_name ) { $pdo = $this->translator->get_pdo(); - $stmt = $pdo->prepare( 'SELECT COUNT(*) FROM ' . $table->name ); + $stmt = $pdo->prepare( 'SELECT COUNT(*) FROM ' . $table_name ); $stmt->execute(); return $stmt->fetchColumn() > 0; } From 241b89c63f7f7c887037e0bb294b4bc08b965981 Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Tue, 30 Jul 2024 17:22:34 +0700 Subject: [PATCH 21/22] Fix outdated behat test --- features/db.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/db.feature b/features/db.feature index a6c32e10..0bdad0ba 100644 --- a/features/db.feature +++ b/features/db.feature @@ -272,7 +272,7 @@ Feature: Perform database operations When I run `cat wp-config.php` Then STDOUT should contain: """ - define( 'DB_CHARSET', '' ); + define( 'DB_CHARSET', 'utf8' ); """ When I run `wp db create` From f9affeec160b80a185312afdb176d1132d5ab4c0 Mon Sep 17 00:00:00 2001 From: Jeroen Pfeil Date: Tue, 30 Jul 2024 20:21:07 +0700 Subject: [PATCH 22/22] Improve escaping of characters --- src/SQLite/Export.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/SQLite/Export.php b/src/SQLite/Export.php index 9ccba0c6..78302c20 100644 --- a/src/SQLite/Export.php +++ b/src/SQLite/Export.php @@ -234,12 +234,24 @@ protected function escape_values( PDO $pdo, $values ) { $escaped_values[] = $value; } else { // Quote the values and escape encode the newlines so the insert statement appears on a single line. - $escaped_values[] = str_replace( "\n", "\\n", $pdo->quote( $value ) ); + $escaped_values[] = $this->escape_string( $value ); } } return implode( ',', $escaped_values ); } + /** + * Escapes a string for use in an insert statement. + * + * @param $value + * + * @return string + */ + protected function escape_string( $value ) { + $pdo = $this->translator->get_pdo(); + return addcslashes( $pdo->quote( $value ), "\\\n" ); + } + /** * Get a comment for the dump. *