Skip to content

Commit de096da

Browse files
committed
Fix SQLite compatibility for search-replace command
Address all MySQL-specific SQL queries that prevent search-replace from working with the WordPress SQLite database integration plugin. Changes: - DESCRIBE → PRAGMA table_info() for column metadata on SQLite - REGEXP serialized data detection → fall back to PHP processing on SQLite since it lacks native REGEXP support - LIKE BINARY → plain LIKE on SQLite via a like_operator() helper - SHOW CREATE TABLE → sqlite_master query on SQLite for export Remove @skip-sqlite tags from the 5 regex search-replace test scenarios that were previously skipped due to these incompatibilities. Fixes #190
1 parent 20351f6 commit de096da

2 files changed

Lines changed: 65 additions & 27 deletions

File tree

features/search-replace.feature

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,6 @@ Feature: Do global search/replace
222222
"""
223223

224224
# See https://github.com/wp-cli/search-replace-command/issues/190
225-
@skip-sqlite
226225
Scenario: Regex search/replace
227226
Given a WP install
228227
When I run `wp search-replace '(Hello)\s(world)' '$2, $1' --regex`
@@ -962,7 +961,6 @@ Feature: Do global search/replace
962961
And STDERR should be empty
963962

964963
# See https://github.com/wp-cli/search-replace-command/issues/190
965-
@skip-sqlite
966964
Scenario: Logging with regex replace
967965
Given a WP install
968966

@@ -1326,7 +1324,6 @@ Feature: Do global search/replace
13261324
"""
13271325

13281326
# See https://github.com/wp-cli/search-replace-command/issues/190
1329-
@skip-sqlite
13301327
Scenario: Regex search/replace with `--regex-limit=1` option
13311328
Given a WP install
13321329
And I run `wp post create --post_content="I have a pen, I have an apple. Pen, pine-apple, apple-pen."`
@@ -1338,7 +1335,6 @@ Feature: Do global search/replace
13381335
"""
13391336

13401337
# See https://github.com/wp-cli/search-replace-command/issues/190
1341-
@skip-sqlite
13421338
Scenario: Regex search/replace with `--regex-limit=2` option
13431339
Given a WP install
13441340
And I run `wp post create --post_content="I have a pen, I have an apple. Pen, pine-apple, apple-pen."`
@@ -1350,7 +1346,6 @@ Feature: Do global search/replace
13501346
"""
13511347

13521348
# See https://github.com/wp-cli/search-replace-command/issues/190
1353-
@skip-sqlite
13541349
Scenario: Regex search/replace with incorrect or default `--regex-limit`
13551350
Given a WP install
13561351
When I try `wp search-replace '(Hello)\s(world)' '$2, $1' --regex --regex-limit=asdf`

src/Search_Replace_Command.php

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -504,10 +504,17 @@ public function __invoke( $args, $assoc_args ) {
504504
if ( $this->export_handle ) {
505505
fwrite( $this->export_handle, "\nDROP TABLE IF EXISTS $table_sql;\n" );
506506

507-
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
508-
$row = $wpdb->get_row( "SHOW CREATE TABLE $table_sql", ARRAY_N );
507+
if ( 'sqlite' === Utils\get_db_type() ) {
508+
// sqlite_master returns a single `sql` column with the CREATE TABLE statement.
509+
$row = $wpdb->get_row( $wpdb->prepare( "SELECT sql FROM sqlite_master WHERE type='table' AND name = %s", $table ), ARRAY_N );
510+
$create_table = $row[0];
511+
} else {
512+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
513+
$row = $wpdb->get_row( "SHOW CREATE TABLE $table_sql", ARRAY_N );
514+
$create_table = $row[1];
515+
}
509516

510-
fwrite( $this->export_handle, $row[1] . ";\n" );
517+
fwrite( $this->export_handle, $create_table . ";\n" );
511518
list( $table_report, $total_rows ) = $this->php_export_table( $table, $old, $new );
512519
if ( $this->report ) {
513520
$report = array_merge( $report, $table_report );
@@ -555,12 +562,17 @@ public function __invoke( $args, $assoc_args ) {
555562
$col_sql = self::esc_sql_ident( $col );
556563
$wpdb->last_error = '';
557564

558-
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
559-
$serial_row = $wpdb->get_row( "SELECT * FROM $table_sql WHERE $col_sql REGEXP '^[aiO]:[1-9]' LIMIT 1" );
560-
561-
// When the regex triggers an error, we should fall back to PHP
562-
if ( false !== strpos( $wpdb->last_error, 'ERROR 1139' ) ) {
565+
if ( 'sqlite' === Utils\get_db_type() ) {
566+
// SQLite does not support REGEXP by default, fall back to PHP processing.
563567
$serial_row = true;
568+
} else {
569+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
570+
$serial_row = $wpdb->get_row( "SELECT * FROM $table_sql WHERE $col_sql REGEXP '^[aiO]:[1-9]' LIMIT 1" );
571+
572+
// When the regex triggers an error, we should fall back to PHP
573+
if ( false !== strpos( $wpdb->last_error, 'ERROR 1139' ) ) {
574+
$serial_row = true;
575+
}
564576
}
565577
}
566578

@@ -695,11 +707,13 @@ private function sql_handle_col( $col, $primary_keys, $table, $old, $new ) {
695707
}
696708
} elseif ( $has_json ) {
697709
// Single query with OR to avoid counting rows that match both forms twice.
710+
$like = self::like_operator();
698711
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
699-
$count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT($col_sql) FROM $table_sql WHERE $col_sql LIKE BINARY %s OR $col_sql LIKE BINARY %s;", '%' . self::esc_like( $old ) . '%', '%' . self::esc_like( $old_json ) . '%' ) );
712+
$count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT($col_sql) FROM $table_sql WHERE $col_sql $like %s OR $col_sql $like %s;", '%' . self::esc_like( $old ) . '%', '%' . self::esc_like( $old_json ) . '%' ) );
700713
} else {
714+
$like = self::like_operator();
701715
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
702-
$count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT($col_sql) FROM $table_sql WHERE $col_sql LIKE BINARY %s;", '%' . self::esc_like( $old ) . '%' ) );
716+
$count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT($col_sql) FROM $table_sql WHERE $col_sql $like %s;", '%' . self::esc_like( $old ) . '%' ) );
703717
}
704718
} else {
705719
if ( $this->log_handle ) {
@@ -740,10 +754,11 @@ private function php_handle_col( $col, $primary_keys, $table, $old, $new ) {
740754
$base_key_condition = '';
741755
$where_key = '';
742756
if ( ! $this->regex ) {
757+
$like = self::like_operator();
743758
$old_json = self::json_encode_strip_quotes( $old );
744-
$base_key_condition = "$col_sql" . $wpdb->prepare( ' LIKE BINARY %s', '%' . self::esc_like( $old ) . '%' );
759+
$base_key_condition = "$col_sql" . $wpdb->prepare( " $like %s", '%' . self::esc_like( $old ) . '%' );
745760
if ( $old_json !== $old ) {
746-
$base_key_condition = "( $base_key_condition OR $col_sql" . $wpdb->prepare( ' LIKE BINARY %s', '%' . self::esc_like( $old_json ) . '%' ) . ' )';
761+
$base_key_condition = "( $base_key_condition OR $col_sql" . $wpdb->prepare( " $like %s", '%' . self::esc_like( $old_json ) . '%' ) . ' )';
747762
}
748763
$where_key = "WHERE $base_key_condition";
749764
}
@@ -953,20 +968,38 @@ private static function get_columns( $table ) {
953968
$all_columns = array();
954969
$suppress_errors = $wpdb->suppress_errors();
955970

956-
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
957-
$results = $wpdb->get_results( "DESCRIBE $table_sql" );
971+
if ( 'sqlite' === Utils\get_db_type() ) {
972+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
973+
$results = $wpdb->get_results( "PRAGMA table_info($table_sql)" );
958974

959-
if ( ! empty( $results ) ) {
960-
foreach ( $results as $col ) {
961-
if ( 'PRI' === $col->Key ) {
962-
$primary_keys[] = $col->Field;
975+
if ( ! empty( $results ) ) {
976+
foreach ( $results as $col ) {
977+
if ( $col->pk > 0 ) {
978+
$primary_keys[] = $col->name;
979+
}
980+
if ( self::is_text_col( $col->type ) ) {
981+
$text_columns[] = $col->name;
982+
}
983+
$all_columns[] = $col->name;
963984
}
964-
if ( self::is_text_col( $col->Type ) ) {
965-
$text_columns[] = $col->Field;
985+
}
986+
} else {
987+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
988+
$results = $wpdb->get_results( "DESCRIBE $table_sql" );
989+
990+
if ( ! empty( $results ) ) {
991+
foreach ( $results as $col ) {
992+
if ( 'PRI' === $col->Key ) {
993+
$primary_keys[] = $col->Field;
994+
}
995+
if ( self::is_text_col( $col->Type ) ) {
996+
$text_columns[] = $col->Field;
997+
}
998+
$all_columns[] = $col->Field;
966999
}
967-
$all_columns[] = $col->Field;
9681000
}
9691001
}
1002+
9701003
$wpdb->suppress_errors( $suppress_errors );
9711004
return array( $primary_keys, $text_columns, $all_columns );
9721005
}
@@ -997,6 +1030,15 @@ private static function esc_like( $old ) {
9971030
return $old;
9981031
}
9991032

1033+
/**
1034+
* Returns the SQL LIKE operator, using LIKE BINARY for MySQL and plain LIKE for SQLite.
1035+
*
1036+
* @return string The LIKE operator appropriate for the current database.
1037+
*/
1038+
private static function like_operator() {
1039+
return 'sqlite' === Utils\get_db_type() ? 'LIKE' : 'LIKE BINARY';
1040+
}
1041+
10001042
/**
10011043
* Returns the JSON-encoded representation of a string with the surrounding quotes stripped.
10021044
* This is used to also handle values stored as raw JSON in the database (e.g. WordPress font data).
@@ -1110,9 +1152,10 @@ private function log_sql_diff( $col, $primary_keys, $table, $old, $new ) {
11101152

11111153
$table_sql = self::esc_sql_ident( $table );
11121154
$col_sql = self::esc_sql_ident( $col );
1155+
$like = self::like_operator();
11131156

11141157
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- escaped through self::esc_sql_ident
1115-
$results = $wpdb->get_results( $wpdb->prepare( "SELECT {$primary_keys_sql}{$col_sql} FROM {$table_sql} WHERE {$col_sql} LIKE BINARY %s", '%' . self::esc_like( $old ) . '%' ), ARRAY_N );
1158+
$results = $wpdb->get_results( $wpdb->prepare( "SELECT {$primary_keys_sql}{$col_sql} FROM {$table_sql} WHERE {$col_sql} $like %s", '%' . self::esc_like( $old ) . '%' ), ARRAY_N );
11161159

11171160
if ( empty( $results ) ) {
11181161
return 0;

0 commit comments

Comments
 (0)