From 421e08dffef55240fec5f0ebb379cdcef43fef6a Mon Sep 17 00:00:00 2001 From: Torsten Landsiedel Date: Tue, 23 Jan 2024 11:36:11 +0100 Subject: [PATCH 1/9] First draft of adding regexp_replace function to SQLite Fixes #47 --- ...s-wp-sqlite-pdo-user-defined-functions.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index 7d4ac5ee..646c3c4d 100644 --- a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -68,6 +68,7 @@ public function __construct( $pdo ) { 'isnull' => 'isnull', 'if' => '_if', 'regexp' => 'regexp', + 'regexp_replace' => 'regexp_replace', 'field' => 'field', 'log' => 'log', 'least' => 'least', @@ -492,6 +493,43 @@ public function regexp( $pattern, $field ) { return preg_match( $pattern, $field ); } + /** + * Method to emulate MySQL REGEXP_REPLACE() function. + * + * @param string|array $pattern Regular expression to search for (or array of strings). + * @param string|array $replacement The string or an array with strings to replace. + * @param string|array $field Haystack. + * + * @return Array if the field parameter is an array, or a string otherwise. + */ + public function regexp_replace( $pattern, $replacement, $field ) { + /* + * If the original query says REGEXP BINARY + * the comparison is byte-by-byte and letter casing now + * matters since lower- and upper-case letters have different + * byte codes. + * + * The REGEXP function can't be easily made to accept two + * parameters, so we'll have to use a hack to get around this. + * + * If the first character of the pattern is a null byte, we'll + * remove it and make the comparison case-sensitive. This should + * be reasonably safe since PHP does not allow null bytes in + * regular expressions anyway. + */ + if ( "\x00" === $pattern[0] ) { + $pattern = substr( $pattern, 1 ); + $flags = ''; + } else { + // Otherwise, the search is case-insensitive. + $flags = 'i'; + } + $pattern = str_replace( '/', '\/', $pattern ); + $pattern = '/' . $pattern . '/' . $flags; + + return preg_replace( $pattern, $replacement, $field ); + } + /** * Method to emulate MySQL FIELD() function. * From ddc87fa7e707c93e3860121c15f6c3d7eb05f969 Mon Sep 17 00:00:00 2001 From: Torsten Landsiedel Date: Thu, 13 Jun 2024 12:01:06 +0200 Subject: [PATCH 2/9] Add first stub for unit test --- tests/WP_SQLite_Translator_Tests.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index 260689c1..9dd1eeae 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -117,6 +117,18 @@ public function regexpOperators() { ); } + public function testRegexpReplace() { + $this->assertQuery( + "INSERT INTO _options (option_name, option_value) VALUES ('first', 'test');" + ); + $this->assertQuery( + "INSERT INTO _options (option_name, option_value) VALUES ('second', 'ignore test');" + ); + + $this->assertQuery( "SELECT FROM _options WHERE REGEXP_REPLACE(option_value, 'ignore ', '') = 'test'" ); + $this->assertCount( 2, $this->engine->get_query_results() ); + } + public function testInsertDateNow() { $this->assertQuery( "INSERT INTO _dates (option_name, option_value) VALUES ('first', now());" From 7ff503563cf5708b29569ea5c0afbdafac012e43 Mon Sep 17 00:00:00 2001 From: Torsten Landsiedel Date: Thu, 13 Jun 2024 14:49:48 +0200 Subject: [PATCH 3/9] Fix unit test - 2nd attempt --- .../sqlite/class-wp-sqlite-pdo-user-defined-functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index e11653a4..50987d29 100644 --- a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -503,7 +503,7 @@ public function regexp( $pattern, $field ) { * * @return Array if the field parameter is an array, or a string otherwise. */ - public function regexp_replace( $pattern, $replacement, $field ) { + public function regexp_replace( $field, $pattern, $replacement ) { /* * If the original query says REGEXP BINARY * the comparison is byte-by-byte and letter casing now From 05d20b2fce80301772ef86d8b3f44ad3f761ba4f Mon Sep 17 00:00:00 2001 From: Torsten Landsiedel Date: Thu, 13 Jun 2024 15:09:06 +0200 Subject: [PATCH 4/9] Fixing unit test - third attempt --- tests/WP_SQLite_Translator_Tests.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index bb3b362a..43a68d13 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -120,13 +120,13 @@ public static function regexpOperators() { public function testRegexpReplace() { $this->assertQuery( - "INSERT INTO _options (option_name, option_value) VALUES ('first', 'test');" + "INSERT INTO _options (option_name, option_value) VALUES ('test-ignore', '1');" ); $this->assertQuery( - "INSERT INTO _options (option_name, option_value) VALUES ('second', 'ignore test');" + "INSERT INTO _options (option_name, option_value) VALUES ('test-remove', '2');" ); - $this->assertQuery( "SELECT FROM _options WHERE REGEXP_REPLACE(option_value, 'ignore ', '') = 'test'" ); + $this->assertQuery( "SELECT * FROM _options WHERE REGEXP_REPLACE(option_name, '(-ignore|-remove)', '') = 'test'" ); $this->assertCount( 2, $this->engine->get_query_results() ); } From 28d2b76c9bdf8a890379c6451fb61ade2c9b9e4a Mon Sep 17 00:00:00 2001 From: Torsten Landsiedel Date: Thu, 13 Jun 2024 15:19:14 +0200 Subject: [PATCH 5/9] Add additional tests to show the value is not modified --- tests/WP_SQLite_Translator_Tests.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index 43a68d13..f7821d4b 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -128,6 +128,11 @@ public function testRegexpReplace() { $this->assertQuery( "SELECT * FROM _options WHERE REGEXP_REPLACE(option_name, '(-ignore|-remove)', '') = 'test'" ); $this->assertCount( 2, $this->engine->get_query_results() ); + + $result1 = $this->engine->query( "SELECT option_value FROM _options WHERE option_name='test-ignore';" ); + $result2 = $this->engine->query( "SELECT option_value FROM _options WHERE option_name='test-remove';" ); + $this->assertEquals( '1', $result1[0]->option_value ); + $this->assertEquals( '2', $result2[0]->option_value ); } public function testInsertDateNow() { From f521f5afd339855a05bff3e46f31c12e12b5f027 Mon Sep 17 00:00:00 2001 From: Torsten Landsiedel Date: Thu, 13 Jun 2024 15:24:28 +0200 Subject: [PATCH 6/9] Update tests/WP_SQLite_Translator_Tests.php --- tests/WP_SQLite_Translator_Tests.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index f7821d4b..43a68d13 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -128,11 +128,6 @@ public function testRegexpReplace() { $this->assertQuery( "SELECT * FROM _options WHERE REGEXP_REPLACE(option_name, '(-ignore|-remove)', '') = 'test'" ); $this->assertCount( 2, $this->engine->get_query_results() ); - - $result1 = $this->engine->query( "SELECT option_value FROM _options WHERE option_name='test-ignore';" ); - $result2 = $this->engine->query( "SELECT option_value FROM _options WHERE option_name='test-remove';" ); - $this->assertEquals( '1', $result1[0]->option_value ); - $this->assertEquals( '2', $result2[0]->option_value ); } public function testInsertDateNow() { From cfb123c8678e52a10d9c003cd6b6a1821306e865 Mon Sep 17 00:00:00 2001 From: Torsten Landsiedel Date: Thu, 13 Jun 2024 16:07:19 +0200 Subject: [PATCH 7/9] Add additional check to copy MySQL behavior --- tests/WP_SQLite_Translator_Tests.php | 12 ++++++++++++ .../class-wp-sqlite-pdo-user-defined-functions.php | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index 43a68d13..21118fba 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -128,6 +128,18 @@ public function testRegexpReplace() { $this->assertQuery( "SELECT * FROM _options WHERE REGEXP_REPLACE(option_name, '(-ignore|-remove)', '') = 'test'" ); $this->assertCount( 2, $this->engine->get_query_results() ); + + $this->assertQuery( 'SELECT REGEXP_REPLACE( null, 'a', 'x') as result' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( null, $results[0]->result ); + + $this->assertQuery( 'SELECT REGEXP_REPLACE( 'abc', null, 'x') as result' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( null, $results[0]->result ); + + $this->assertQuery( 'SELECT REGEXP_REPLACE( 'abc', 'a', null) as result' ); + $results = $this->engine->get_query_results(); + $this->assertEquals( null, $results[0]->result ); } public function testInsertDateNow() { diff --git a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index 50987d29..9030d81e 100644 --- a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -518,6 +518,12 @@ public function regexp_replace( $field, $pattern, $replacement ) { * be reasonably safe since PHP does not allow null bytes in * regular expressions anyway. */ + + /* Return null if one of the required parameter is null */ + if ( is_null( $field ) || is_null( $pattern ) || is_null( $replacement ) ) { + return null; + } + if ( "\x00" === $pattern[0] ) { $pattern = substr( $pattern, 1 ); $flags = ''; From 0ade800be33baba09b232dc3f35be5e2b2a5aa7c Mon Sep 17 00:00:00 2001 From: Torsten Landsiedel Date: Thu, 13 Jun 2024 16:10:18 +0200 Subject: [PATCH 8/9] Fix quotes and coding standards issue --- tests/WP_SQLite_Translator_Tests.php | 6 +++--- .../sqlite/class-wp-sqlite-pdo-user-defined-functions.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index 21118fba..f9899723 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -129,15 +129,15 @@ public function testRegexpReplace() { $this->assertQuery( "SELECT * FROM _options WHERE REGEXP_REPLACE(option_name, '(-ignore|-remove)', '') = 'test'" ); $this->assertCount( 2, $this->engine->get_query_results() ); - $this->assertQuery( 'SELECT REGEXP_REPLACE( null, 'a', 'x') as result' ); + $this->assertQuery( "SELECT REGEXP_REPLACE( null, 'a', 'x') as result" ); $results = $this->engine->get_query_results(); $this->assertEquals( null, $results[0]->result ); - $this->assertQuery( 'SELECT REGEXP_REPLACE( 'abc', null, 'x') as result' ); + $this->assertQuery( "SELECT REGEXP_REPLACE( 'abc', null, 'x') as result" ); $results = $this->engine->get_query_results(); $this->assertEquals( null, $results[0]->result ); - $this->assertQuery( 'SELECT REGEXP_REPLACE( 'abc', 'a', null) as result' ); + $this->assertQuery( "SELECT REGEXP_REPLACE( 'abc', 'a', null) as result" ); $results = $this->engine->get_query_results(); $this->assertEquals( null, $results[0]->result ); } diff --git a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index 9030d81e..e8c00a04 100644 --- a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -518,7 +518,7 @@ public function regexp_replace( $field, $pattern, $replacement ) { * be reasonably safe since PHP does not allow null bytes in * regular expressions anyway. */ - + /* Return null if one of the required parameter is null */ if ( is_null( $field ) || is_null( $pattern ) || is_null( $replacement ) ) { return null; From 380e5412d158028312b8e1ea53295162aea4b685 Mon Sep 17 00:00:00 2001 From: Torsten Landsiedel Date: Thu, 13 Jun 2024 17:04:07 +0200 Subject: [PATCH 9/9] Adding more inline comments and add a check for an empty pattern --- tests/WP_SQLite_Translator_Tests.php | 8 +++++++- .../sqlite/class-wp-sqlite-pdo-user-defined-functions.php | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/WP_SQLite_Translator_Tests.php b/tests/WP_SQLite_Translator_Tests.php index f9899723..71070eca 100644 --- a/tests/WP_SQLite_Translator_Tests.php +++ b/tests/WP_SQLite_Translator_Tests.php @@ -119,16 +119,17 @@ public static function regexpOperators() { } public function testRegexpReplace() { + /* Testing if an actual replacment works correctly */ $this->assertQuery( "INSERT INTO _options (option_name, option_value) VALUES ('test-ignore', '1');" ); $this->assertQuery( "INSERT INTO _options (option_name, option_value) VALUES ('test-remove', '2');" ); - $this->assertQuery( "SELECT * FROM _options WHERE REGEXP_REPLACE(option_name, '(-ignore|-remove)', '') = 'test'" ); $this->assertCount( 2, $this->engine->get_query_results() ); + /* If one of the required parameters is null, the return value is null, copying the MYSQL/MariaDB behavior */ $this->assertQuery( "SELECT REGEXP_REPLACE( null, 'a', 'x') as result" ); $results = $this->engine->get_query_results(); $this->assertEquals( null, $results[0]->result ); @@ -140,6 +141,11 @@ public function testRegexpReplace() { $this->assertQuery( "SELECT REGEXP_REPLACE( 'abc', 'a', null) as result" ); $results = $this->engine->get_query_results(); $this->assertEquals( null, $results[0]->result ); + + /* Providing an empty pattern should produce an error - but we changed that to null to avoid breaking things */ + $this->assertQuery( "SELECT REGEXP_REPLACE( 'abc', '', 'x') as result" ); + $results = $this->engine->get_query_results(); + $this->assertEquals( null, $results[0]->result ); } public function testInsertDateNow() { diff --git a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php index e8c00a04..816ccb86 100644 --- a/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php +++ b/wp-includes/sqlite/class-wp-sqlite-pdo-user-defined-functions.php @@ -524,6 +524,11 @@ public function regexp_replace( $field, $pattern, $replacement ) { return null; } + /* Return null if the pattern is empty - this changes MySQL/MariaDB behavior! */ + if ( empty( $pattern ) ) { + return null; + } + if ( "\x00" === $pattern[0] ) { $pattern = substr( $pattern, 1 ); $flags = '';