Skip to content

Commit ea843d1

Browse files
authored
Merge pull request #144 from hellausefulsoftware/bugfix/hellauseful1-issue-0-postgres-delete-query-rewrite
Automated: Fix #1: SQL Replacements need to use DB_PREFIX when building the regex replacements
2 parents 333f414 + de1824d commit ea843d1

3 files changed

Lines changed: 100 additions & 7 deletions

File tree

pg4wp/rewriters/DeleteSQLRewriter.php

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,35 +19,79 @@ public function rewrite(): string
1919
$sql = str_replace('LIMIT 1', '', $sql);
2020
$sql = str_replace(' REGEXP ', ' ~ ', $sql);
2121

22+
// Get the WordPress table prefix
23+
$prefix = $wpdb->prefix;
24+
2225
// This handles removal of duplicate entries in table options
2326
if(false !== strpos($sql, 'DELETE o1 FROM ')) {
2427
$sql = "DELETE FROM $wpdb->options WHERE option_id IN " .
2528
"(SELECT o1.option_id FROM $wpdb->options AS o1, $wpdb->options AS o2 " .
2629
"WHERE o1.option_name = o2.option_name " .
2730
"AND o1.option_id < o2.option_id)";
2831
}
29-
// Rewrite _transient_timeout multi-table delete query
30-
elseif(0 === strpos($sql, 'DELETE a, b FROM wp_options a, wp_options b')) {
32+
// Rewrite _transient_timeout multi-table delete query with dynamic prefix for options table
33+
elseif(preg_match('/DELETE a, b FROM ' . preg_quote($prefix, '/') . 'options a, ' . preg_quote($prefix, '/') . 'options b/', $sql)) {
3134
$where = substr($sql, strpos($sql, 'WHERE ') + 6);
3235
$where = rtrim($where, " \t\n\r;");
3336
// Fix string/number comparison by adding check and cast
3437
$where = str_replace('AND b.option_value', 'AND b.option_value ~ \'^[0-9]+$\' AND CAST(b.option_value AS BIGINT)', $where);
3538
// Mirror WHERE clause to delete both sides of self-join.
3639
$where2 = strtr($where, array('a.' => 'b.', 'b.' => 'a.'));
37-
$sql = 'DELETE FROM wp_options a USING wp_options b WHERE ' .
40+
$sql = "DELETE FROM {$wpdb->options} a USING {$wpdb->options} b WHERE " .
3841
'(' . $where . ') OR (' . $where2 . ');';
3942
}
4043

41-
// Rewrite _transient_timeout multi-table delete query
42-
elseif(0 === strpos($sql, 'DELETE a, b FROM wp_sitemeta a, wp_sitemeta b')) {
44+
// Rewrite _transient_timeout multi-table delete query with dynamic prefix for sitemeta table
45+
elseif(preg_match('/DELETE a, b FROM ' . preg_quote($prefix, '/') . 'sitemeta a, ' . preg_quote($prefix, '/') . 'sitemeta b/', $sql)) {
4346
$where = substr($sql, strpos($sql, 'WHERE ') + 6);
4447
$where = rtrim($where, " \t\n\r;");
4548
// Fix string/number comparison by adding check and cast
4649
$where = str_replace('AND b.meta_value', 'AND b.meta_value ~ \'^[0-9]+$\' AND CAST(b.meta_value AS BIGINT)', $where);
4750
// Mirror WHERE clause to delete both sides of self-join.
4851
$where2 = strtr($where, array('a.' => 'b.', 'b.' => 'a.'));
49-
$sql = 'DELETE FROM wp_sitemeta a USING wp_sitemeta b WHERE ' .
50-
'(' . $where . ') OR (' . $where2 . ');';
52+
// Use $wpdb's sitemeta table name which should already have the correct prefix
53+
if(isset($wpdb->sitemeta)) {
54+
$sql = "DELETE FROM {$wpdb->sitemeta} a USING {$wpdb->sitemeta} b WHERE " .
55+
'(' . $where . ') OR (' . $where2 . ');';
56+
} else {
57+
// Fallback if $wpdb->sitemeta is not available
58+
$sql = "DELETE FROM {$prefix}sitemeta a USING {$prefix}sitemeta b WHERE " .
59+
'(' . $where . ') OR (' . $where2 . ');';
60+
}
61+
}
62+
63+
// Add a more general pattern to handle multi-table DELETE with aliases and dynamic table names
64+
elseif(preg_match('/DELETE\s+([a-zA-Z0-9_]+),\s*([a-zA-Z0-9_]+)\s+FROM\s+([a-zA-Z0-9_' . preg_quote($prefix, '/') . ']+)\s+([a-zA-Z0-9_]+),\s*([a-zA-Z0-9_' . preg_quote($prefix, '/') . ']+)\s+([a-zA-Z0-9_]+)\s+WHERE/i', $sql, $matches)) {
65+
// Extract aliases and table names
66+
$firstAlias = $matches[1];
67+
$secondAlias = $matches[2];
68+
$firstTable = $matches[3];
69+
$firstTableAlias = $matches[4];
70+
$secondTable = $matches[5];
71+
$secondTableAlias = $matches[6];
72+
73+
// Extract WHERE clause
74+
$where = substr($sql, strpos($sql, 'WHERE ') + 6);
75+
$where = rtrim($where, " \t\n\r;");
76+
77+
// Check if the table names are known WordPress tables and replace with dynamic property references
78+
foreach([$firstTable, $secondTable] as $index => $tableName) {
79+
// Strip prefix if it exists to get the base table name
80+
$baseTableName = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $tableName);
81+
82+
// Check if $wpdb has a property for this table
83+
if(isset($wpdb->$baseTableName)) {
84+
// Replace the hardcoded table name with the dynamic property
85+
if($index === 0) {
86+
$firstTable = $wpdb->$baseTableName;
87+
} else {
88+
$secondTable = $wpdb->$baseTableName;
89+
}
90+
}
91+
}
92+
93+
// Generate PostgreSQL DELETE...USING syntax
94+
$sql = "DELETE FROM $firstTable $firstTableAlias USING $secondTable $secondTableAlias WHERE $where;";
5195
}
5296

5397
// Akismet sometimes doesn't write 'comment_ID' with 'ID' in capitals where needed ...

readme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ PG4WP is provided "as-is" with no warranty in the hope it can be useful.
8181

8282
PG4WP is licensed under the [GNU GPL](http://www.gnu.org/licenses/gpl.html "GNU GPL") v2 or any newer version at your choice.
8383

84+
### Changelog
85+
86+
#### Latest Changes
87+
- Fixed issue with SQL DELETE query rewriting to use DB_PREFIX consistently, which previously caused PostgreSQL syntax errors due to hardcoded table prefixes
88+
8489
### Contributors
8590
Code originally by Hawk__ (http://www.hawkix.net/)
8691
Modifications by @kevinoid and @mattbucci

tests/rewriteTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,50 @@ protected function setUp(): void
854854
public $comments = "wp_comments";
855855
public $prefix = "wp_";
856856
public $options = "wp_options";
857+
public $sitemeta = "wp_sitemeta";
857858
};
858859
}
860+
861+
public function test_it_properly_uses_dynamic_table_prefix_for_delete_queries()
862+
{
863+
global $wpdb;
864+
865+
// Change the prefix to a custom one
866+
$wpdb->prefix = "custom_";
867+
$wpdb->options = "custom_options";
868+
$wpdb->sitemeta = "custom_sitemeta";
869+
$wpdb->posts = "custom_posts";
870+
$wpdb->postmeta = "custom_postmeta";
871+
872+
// Test DELETE with options table
873+
$sql = "DELETE a, b FROM custom_options a, custom_options b WHERE a.option_name = '_transient_timeout_something' AND b.option_name = '_transient_something' AND b.option_value < 12345678";
874+
$postgresql = pg4wp_rewrite($sql);
875+
$this->assertStringContainsString("DELETE FROM custom_options a USING custom_options b", $postgresql);
876+
$this->assertStringNotContainsString("wp_options", $postgresql);
877+
878+
// Test DELETE with sitemeta table
879+
$sql = "DELETE a, b FROM custom_sitemeta a, custom_sitemeta b WHERE a.meta_key = '_site_transient_timeout_something' AND b.meta_key = '_site_transient_something' AND b.meta_value < 12345678";
880+
$postgresql = pg4wp_rewrite($sql);
881+
$this->assertStringContainsString("DELETE FROM custom_sitemeta a USING custom_sitemeta b", $postgresql);
882+
$this->assertStringNotContainsString("wp_sitemeta", $postgresql);
883+
884+
// Test general pattern DELETE with any tables
885+
$sql = "DELETE p, pm FROM custom_posts p, custom_postmeta pm WHERE p.ID = pm.post_id AND p.post_type = 'revision'";
886+
$postgresql = pg4wp_rewrite($sql);
887+
$this->assertStringContainsString("DELETE FROM custom_posts p USING custom_postmeta pm", $postgresql);
888+
$this->assertStringNotContainsString("wp_posts", $postgresql);
889+
$this->assertStringNotContainsString("wp_postmeta", $postgresql);
890+
891+
// Test with tables that don't exist as $wpdb properties
892+
$sql = "DELETE a, b FROM custom_mytable a, custom_anothertable b WHERE a.id = b.ref_id";
893+
$postgresql = pg4wp_rewrite($sql);
894+
$this->assertStringContainsString("DELETE FROM custom_mytable a USING custom_anothertable b", $postgresql);
895+
896+
// Restore the original prefix for other tests
897+
$wpdb->prefix = "wp_";
898+
$wpdb->options = "wp_options";
899+
$wpdb->sitemeta = "wp_sitemeta";
900+
$wpdb->posts = "wp_posts";
901+
$wpdb->postmeta = "wp_postmeta";
902+
}
859903
}

0 commit comments

Comments
 (0)