Add pg4wp plugin and update wordpress.yaml
This commit is contained in:
1 parent
0a51ac0a31
commit
a4d8c0f418
23 files changed
+4304
-6
No files matched your search
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
abstract class AbstractSQLRewriter
|
||||
{
|
||||
protected string $originalSQL;
|
||||
|
||||
public function __construct(string $sql)
|
||||
{
|
||||
$this->originalSQL = $sql;
|
||||
}
|
||||
|
||||
abstract public function rewrite(): string;
|
||||
|
||||
public function original(): string
|
||||
{
|
||||
return $this->originalSQL;
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
// Get the called class name and remove the "SQLRewriter" suffix to get the SQL type
|
||||
$className = get_called_class();
|
||||
$type = str_replace('SQLRewriter', '', $className);
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
<?php
|
||||
|
||||
class AlterTableSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
private $stringReplacements = [
|
||||
' tinytext' => ' text',
|
||||
' mediumtext' => ' text',
|
||||
' longtext' => ' text',
|
||||
' unsigned' => ' ',
|
||||
'gmt datetime NOT NULL default \'0000-00-00 00:00:00\'' => 'gmt timestamp NOT NULL DEFAULT timezone(\'gmt\'::text, now())',
|
||||
'default \'0000-00-00 00:00:00\'' => 'DEFAULT now()',
|
||||
'\'0000-00-00 00:00:00\'' => 'now()',
|
||||
' datetime' => ' timestamp',
|
||||
' DEFAULT CHARACTER SET utf8mb4' => '',
|
||||
' DEFAULT CHARACTER SET utf8' => '',
|
||||
|
||||
// For WPMU (starting with WP 3.2)
|
||||
" enum('0','1')" => ' smallint',
|
||||
' COLLATE utf8mb4_unicode_520_ci' => '',
|
||||
' COLLATE utf8_general_ci' => '',
|
||||
' CHARACTER SET utf8' => '',
|
||||
' DEFAULT CHARSET=utf8' => '',
|
||||
|
||||
// For flash-album-gallery plugin
|
||||
' tinyint' => ' smallint'
|
||||
];
|
||||
|
||||
public function rewrite(): string
|
||||
{
|
||||
$sql = $this->original();
|
||||
|
||||
$sql = $this->rewrite_numeric_type($sql);
|
||||
$sql = $this->rewrite_columns_with_protected_names($sql);
|
||||
|
||||
if (str_contains($sql, 'ADD INDEX') || str_contains($sql, 'ADD UNIQUE INDEX')) {
|
||||
$sql = $this->rewriteAddIndex($sql);
|
||||
return $sql;
|
||||
}
|
||||
if (str_contains($sql, 'CHANGE COLUMN')) {
|
||||
$sql = $this->rewriteChangeColumn($sql);
|
||||
return $sql;
|
||||
}
|
||||
if (str_contains($sql, 'ALTER COLUMN')) {
|
||||
$sql = $this->rewriteAlterColumn($sql);
|
||||
return $sql;
|
||||
}
|
||||
if (str_contains($sql, 'ADD COLUMN')) {
|
||||
$sql = $this->rewriteAddColumn($sql);
|
||||
return $sql;
|
||||
}
|
||||
if (str_contains($sql, 'ADD KEY') || str_contains($sql, 'ADD UNIQUE KEY')) {
|
||||
$sql = $this->rewriteAddKey($sql);
|
||||
return $sql;
|
||||
}
|
||||
if (str_contains($sql, 'DROP INDEX')) {
|
||||
$sql = $this->rewriteDropIndex($sql);
|
||||
return $sql;
|
||||
}
|
||||
if (str_contains($sql, 'DROP PRIMARY KEY')) {
|
||||
$sql = $this->rewriteDropPrimaryKey($sql);
|
||||
return $sql;
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function rewriteAddIndex(string $sql): string
|
||||
{
|
||||
$pattern = '/ALTER TABLE\s+(\w+)\s+ADD (UNIQUE |)INDEX\s+([^\s]+)\s+\(((?:[^\(\)]+|\([^\(\)]+\))+)\)/';
|
||||
|
||||
if(1 === preg_match($pattern, $sql, $matches)) {
|
||||
$table = $matches[1];
|
||||
$unique = $matches[2];
|
||||
$index = $matches[3];
|
||||
$columns = $matches[4];
|
||||
|
||||
// Remove prefix indexing
|
||||
// Rarely used and apparently unnecessary for current uses
|
||||
$columns = preg_replace('/\([^\)]*\)/', '', $columns);
|
||||
|
||||
// Workaround for index name duplicate
|
||||
$index = $table . '_' . $index;
|
||||
|
||||
// Add backticks around index name and column name, and include IF NOT EXISTS clause
|
||||
$sql = "CREATE {$unique}INDEX IF NOT EXISTS `{$index}` ON `{$table}` (`{$columns}`)";
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function rewriteChangeColumn(string $sql): string
|
||||
{
|
||||
$pattern = '/ALTER TABLE\s+(\w+)\s+CHANGE COLUMN\s+([^\s]+)\s+([^\s]+)\s+([^ ]+)( unsigned|)\s*(NOT NULL|)\s*(default (.+)|)/';
|
||||
|
||||
if(1 === preg_match($pattern, $sql, $matches)) {
|
||||
$table = $matches[1];
|
||||
$col = $matches[2];
|
||||
$newname = $matches[3];
|
||||
$type = strtolower($matches[4]);
|
||||
if(isset($this->stringReplacements[$type])) {
|
||||
$type = $this->stringReplacements[$type];
|
||||
}
|
||||
$unsigned = $matches[5];
|
||||
$notnull = $matches[6];
|
||||
$default = $matches[7];
|
||||
$defval = $matches[8];
|
||||
if(isset($this->stringReplacements[$defval])) {
|
||||
$defval = $this->stringReplacements[$defval];
|
||||
}
|
||||
$newq = "ALTER TABLE $table ALTER COLUMN $col TYPE $type";
|
||||
if(!empty($notnull)) {
|
||||
$newq .= ", ALTER COLUMN $col SET NOT NULL";
|
||||
}
|
||||
if(!empty($default)) {
|
||||
$newq .= ", ALTER COLUMN $col SET DEFAULT $defval";
|
||||
}
|
||||
if($col != $newname) {
|
||||
$newq .= ";ALTER TABLE $table RENAME COLUMN $col TO $newcol;";
|
||||
}
|
||||
$sql = $newq;
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function rewriteAlterColumn(string $sql): string
|
||||
{
|
||||
$pattern = '/ALTER TABLE\s+(\w+)\s+ALTER COLUMN\s+/';
|
||||
|
||||
if(1 === preg_match($pattern, $sql)) {
|
||||
// Translate default values
|
||||
$sql = str_replace(
|
||||
array_keys($this->stringReplacements),
|
||||
array_values($this->stringReplacements),
|
||||
$sql
|
||||
);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function rewriteAddColumn(string $sql): string
|
||||
{
|
||||
$pattern = '/ALTER TABLE\s+(\w+)\s+ADD COLUMN\s+([^\s]+)\s+([^ ]+)( unsigned|)\s+(NOT NULL|)\s*(default (.+)|)/';
|
||||
|
||||
if(1 === preg_match($pattern, $sql, $matches)) {
|
||||
$table = $matches[1];
|
||||
$col = $matches[2];
|
||||
$type = strtolower($matches[3]);
|
||||
if(isset($this->stringReplacements[$type])) {
|
||||
$type = $this->stringReplacements[$type];
|
||||
}
|
||||
$unsigned = $matches[4];
|
||||
$notnull = $matches[5];
|
||||
$default = $matches[6];
|
||||
$defval = $matches[7];
|
||||
if(isset($this->stringReplacements[$defval])) {
|
||||
$defval = $this->stringReplacements[$defval];
|
||||
}
|
||||
$newq = "ALTER TABLE $table ADD COLUMN $col $type";
|
||||
if(!empty($default)) {
|
||||
$newq .= " DEFAULT $defval";
|
||||
}
|
||||
if(!empty($notnull)) {
|
||||
$newq .= " NOT NULL";
|
||||
}
|
||||
$sql = $newq;
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function rewriteAddKey(string $sql): string
|
||||
{
|
||||
$pattern = '/ALTER TABLE\s+(\w+)\s+ADD (UNIQUE |)KEY\s+([^\s]+)\s+\(((?:[^\(\)]+|\([^\(\)]+\))+)\)/';
|
||||
|
||||
if(1 === preg_match($pattern, $sql, $matches)) {
|
||||
$table = $matches[1];
|
||||
$unique = $matches[2];
|
||||
$index = $matches[3];
|
||||
$columns = $matches[4];
|
||||
|
||||
// Remove prefix indexing
|
||||
// Rarely used and apparently unnecessary for current uses
|
||||
$columns = preg_replace('/\([^\)]*\)/', '', $columns);
|
||||
|
||||
// Workaround for index name duplicate
|
||||
$index = $table . '_' . $index;
|
||||
$sql = "CREATE {$unique}INDEX $index ON $table ($columns)";
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function rewriteDropIndex(string $sql): string
|
||||
{
|
||||
$pattern = '/ALTER TABLE\s+(\w+)\s+DROP INDEX\s+([^\s]+)/';
|
||||
|
||||
if(1 === preg_match($pattern, $sql, $matches)) {
|
||||
$table = $matches[1];
|
||||
$index = $matches[2];
|
||||
$sql = "DROP INDEX {$table}_{$index}";
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function rewriteDropPrimaryKey(string $sql): string
|
||||
{
|
||||
$pattern = '/ALTER TABLE\s+(\w+)\s+DROP PRIMARY KEY/';
|
||||
|
||||
if(1 === preg_match($pattern, $sql, $matches)) {
|
||||
$table = $matches[1];
|
||||
$sql = "ALTER TABLE {$table} DROP CONSTRAINT {$table}_pkey";
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function rewrite_numeric_type($sql)
|
||||
{
|
||||
// Numeric types in MySQL which need to be rewritten
|
||||
$numeric_types = ["bigint", "int", "integer", "smallint", "mediumint", "tinyint", "double", "decimal"];
|
||||
$numeric_types_imploded = implode('|', $numeric_types);
|
||||
|
||||
// Prepare regex pattern to match 'type(x)'
|
||||
$pattern = "/(" . $numeric_types_imploded . ")\(\d+\)/";
|
||||
|
||||
// Execute type find & replace
|
||||
$sql = preg_replace_callback($pattern, function ($matches) {
|
||||
return $matches[1];
|
||||
}, $sql);
|
||||
|
||||
// bigint
|
||||
$pattern = '/bigint(\(\d+\))?([ ]*NOT NULL)?[ ]*auto_increment/i';
|
||||
preg_match($pattern, $sql, $matches);
|
||||
if($matches) {
|
||||
$sql = preg_replace($pattern, 'bigserial', $sql);
|
||||
}
|
||||
|
||||
// int
|
||||
$pattern = '/int(\(\d+\))?([ ]*NOT NULL)?[ ]*auto_increment/i';
|
||||
preg_match($pattern, $sql, $matches);
|
||||
if($matches) {
|
||||
$sql = preg_replace($pattern, 'serial', $sql);
|
||||
}
|
||||
|
||||
// smallint
|
||||
$pattern = '/smallint(\(\d+\))?([ ]*NOT NULL)?[ ]*auto_increment/i';
|
||||
preg_match($pattern, $sql, $matches);
|
||||
if($matches) {
|
||||
$sql = preg_replace($pattern, 'smallserial', $sql);
|
||||
}
|
||||
|
||||
// Handle for numeric and decimal -- being replaced with serial
|
||||
$numeric_patterns = ['/numeric(\(\d+\))?([ ]*NOT NULL)?[ ]*auto_increment/i', '/decimal(\(\d+\))?([ ]*NOT NULL)?[ ]*auto_increment/i'];
|
||||
foreach($numeric_patterns as $pattern) {
|
||||
preg_match($pattern, $sql, $matches);
|
||||
if($matches) {
|
||||
$sql = preg_replace($pattern, 'serial', $sql);
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function rewrite_columns_with_protected_names($sql)
|
||||
{
|
||||
// Splitting the SQL statement into parts before "(", inside "(", and after ")"
|
||||
if (preg_match('/^(CREATE TABLE IF NOT EXISTS|CREATE TABLE|ALTER TABLE)\s+([^\s]+)\s*\((.*)\)(.*)$/is', $sql, $matches)) {
|
||||
$prefix = $matches[1] . ' ' . $matches[2] . ' (';
|
||||
$columnsAndKeys = $matches[3];
|
||||
$suffix = ')' . $matches[4];
|
||||
|
||||
$regex = '/(?:^|\s*,\s*)(\b(?:timestamp|date|time|default)\b)\s*(?=\s+\w+)/i';
|
||||
|
||||
// Callback function to add quotes around protected column names
|
||||
$callback = function($matches) {
|
||||
$whitespace = str_replace($matches[1], "", $matches[0]);
|
||||
return $whitespace . '"' . $matches[1] . '"';
|
||||
};
|
||||
|
||||
// Replace protected column names with quoted versions within columns and keys part
|
||||
$columnsAndKeys = preg_replace_callback($regex, $callback, $columnsAndKeys);
|
||||
return $prefix . $columnsAndKeys . $suffix;
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
class CreateTableSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
private $stringReplacements = [
|
||||
' tinytext' => ' text',
|
||||
' mediumtext' => ' text',
|
||||
' longtext' => ' text',
|
||||
' unsigned' => ' ',
|
||||
'gmt datetime NOT NULL default \'0000-00-00 00:00:00\'' => 'gmt timestamp NOT NULL DEFAULT timezone(\'gmt\'::text, now())',
|
||||
'default \'0000-00-00 00:00:00\'' => 'DEFAULT now()',
|
||||
'\'0000-00-00 00:00:00\'' => 'now()',
|
||||
' datetime' => ' timestamp',
|
||||
' DEFAULT CHARACTER SET utf8mb4' => '',
|
||||
' DEFAULT CHARACTER SET utf8' => '',
|
||||
|
||||
// For WPMU (starting with WP 3.2)
|
||||
" enum('0','1')" => ' smallint',
|
||||
' COLLATE utf8mb4_unicode_520_ci' => '',
|
||||
' COLLATE utf8_general_ci' => '',
|
||||
' CHARACTER SET utf8' => '',
|
||||
' DEFAULT CHARSET=utf8' => '',
|
||||
|
||||
' tinyint' => ' smallint',
|
||||
' mediumint' => ' integer'
|
||||
];
|
||||
|
||||
public function rewrite(): string
|
||||
{
|
||||
$sql = $this->original();
|
||||
|
||||
$tableSQL = str_replace('CREATE TABLE IF NOT EXISTS ', 'CREATE TABLE ', $sql);
|
||||
$pattern = '/CREATE TABLE [`]?(\w+)[`]?/';
|
||||
preg_match($pattern, $tableSQL, $matches);
|
||||
$table = $matches[1];
|
||||
|
||||
// change all creates into create if not exists
|
||||
$pattern = "/CREATE TABLE (IF NOT EXISTS )?(\w+)\s*\(/i";
|
||||
$replacement = 'CREATE TABLE IF NOT EXISTS $2 (';
|
||||
$sql = preg_replace($pattern, $replacement, $sql);
|
||||
|
||||
// Remove trailing spaces
|
||||
$sql = trim($sql);
|
||||
|
||||
// Add a slash if needed
|
||||
if (substr($sql, strlen($sql) - 1, 1) != ";") {
|
||||
$sql = $sql . ";";
|
||||
}
|
||||
|
||||
// Translate types and some other replacements
|
||||
$sql = str_ireplace(
|
||||
array_keys($this->stringReplacements),
|
||||
array_values($this->stringReplacements),
|
||||
$sql
|
||||
);
|
||||
|
||||
$sql = $this->rewrite_numeric_type($sql);
|
||||
$sql = $this->rewrite_columns_with_protected_names($sql);
|
||||
|
||||
// Support for UNIQUE INDEX creation
|
||||
$pattern = '/,\s*(UNIQUE |)KEY\s+(`[^`]+`|\w+)\s+\(((?:[^()]|\([^)]*\))*)\)/';
|
||||
if(preg_match_all($pattern, $sql, $matches, PREG_SET_ORDER)) {
|
||||
foreach($matches as $match) {
|
||||
$unique = $match[1];
|
||||
$index = $match[2];
|
||||
$columns = $match[3];
|
||||
|
||||
// Removing backticks from the index names
|
||||
$index = str_replace('`', '', $index);
|
||||
|
||||
// Removing backticks and key length constraints from the columns
|
||||
$columns = preg_replace(["/`/", "/\(\d+\)/"], '', $columns);
|
||||
|
||||
// Creating a unique index name
|
||||
$indexName = $table . '_' . $index;
|
||||
|
||||
// Appending the CREATE INDEX statement to SQL
|
||||
$sql .= "\nCREATE {$unique}INDEX IF NOT EXISTS $indexName ON $table ($columns);";
|
||||
}
|
||||
}
|
||||
// Now remove handled indexes
|
||||
$sql = preg_replace($pattern, '', $sql);
|
||||
|
||||
|
||||
// Rewrite unique keys to just be UNIQUE without a key name
|
||||
$pattern = "/(,\s*)?UNIQUE KEY\s+[a-zA-Z0-9_]+\s+(\([a-zA-Z0-9_,\s]+\))/";
|
||||
$replacement = "$1UNIQUE $2";
|
||||
$sql = preg_replace($pattern, $replacement, $sql);
|
||||
|
||||
// Rewrite Primary keys to not have a key name
|
||||
$pattern = '/PRIMARY KEY\s+\w+\s*\((.*?)\)/';
|
||||
$replacement = 'PRIMARY KEY ($1)';
|
||||
$sql = preg_replace($pattern, $replacement, $sql);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function rewrite_numeric_type($sql)
|
||||
{
|
||||
// Numeric types in MySQL which need to be rewritten
|
||||
$numeric_types = ["bigint", "int", "integer", "smallint", "mediumint", "tinyint", "double", "decimal"];
|
||||
$numeric_types_imploded = implode('|', $numeric_types);
|
||||
|
||||
// Prepare regex pattern to match 'type(x)'
|
||||
$pattern = "/(" . $numeric_types_imploded . ")\(\d+\)/i";
|
||||
|
||||
// Execute type find & replace
|
||||
$sql = preg_replace_callback($pattern, function ($matches) {
|
||||
return $matches[1];
|
||||
}, $sql);
|
||||
|
||||
// bigint
|
||||
$pattern = '/bigint(\(\d+\))?([ ]*NOT NULL)?[ ]*auto_increment/i';
|
||||
preg_match($pattern, $sql, $matches);
|
||||
if($matches) {
|
||||
$sql = preg_replace($pattern, 'bigserial', $sql);
|
||||
}
|
||||
|
||||
// int
|
||||
$pattern = '/int(\(\d+\))?([ ]*NOT NULL)?[ ]*auto_increment/i';
|
||||
preg_match($pattern, $sql, $matches);
|
||||
if($matches) {
|
||||
$sql = preg_replace($pattern, 'serial', $sql);
|
||||
}
|
||||
|
||||
// smallint
|
||||
$pattern = '/smallint(\(\d+\))?([ ]*NOT NULL)?[ ]*auto_increment/i';
|
||||
preg_match($pattern, $sql, $matches);
|
||||
if($matches) {
|
||||
$sql = preg_replace($pattern, 'smallserial', $sql);
|
||||
}
|
||||
|
||||
// Handle for numeric and decimal -- being replaced with serial
|
||||
$numeric_patterns = ['/numeric(\(\d+\))?([ ]*NOT NULL)?[ ]*auto_increment/i', '/decimal(\(\d+\))?([ ]*NOT NULL)?[ ]*auto_increment/i'];
|
||||
foreach($numeric_patterns as $pattern) {
|
||||
preg_match($pattern, $sql, $matches);
|
||||
if($matches) {
|
||||
$sql = preg_replace($pattern, 'serial', $sql);
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
private function rewrite_columns_with_protected_names($sql)
|
||||
{
|
||||
// Splitting the SQL statement into parts before "(", inside "(", and after ")"
|
||||
if (preg_match('/^(CREATE TABLE IF NOT EXISTS|CREATE TABLE|ALTER TABLE)\s+([^\s]+)\s*\((.*)\)(.*)$/is', $sql, $matches)) {
|
||||
$prefix = $matches[1] . ' ' . $matches[2] . ' (';
|
||||
$columnsAndKeys = $matches[3];
|
||||
$suffix = ')' . $matches[4];
|
||||
|
||||
$regex = '/(?:^|\s*,\s*)(\b(?:timestamp|date|time|default|end)\b)\s*(?=\s+\w+)/i';
|
||||
|
||||
// Callback function to add quotes around protected column names
|
||||
$callback = function($matches) {
|
||||
$whitespace = str_replace($matches[1], "", $matches[0]);
|
||||
return $whitespace . '"' . $matches[1] . '"';
|
||||
};
|
||||
|
||||
// Replace protected column names with quoted versions within columns and keys part
|
||||
$columnsAndKeys = preg_replace_callback($regex, $callback, $columnsAndKeys);
|
||||
return $prefix . $columnsAndKeys . $suffix;
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
class DeleteSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$sql = $this->original();
|
||||
|
||||
// ORDER BY is not supported in DELETE queries, and not required
|
||||
// when LIMIT is not present
|
||||
if(false !== strpos($sql, 'ORDER BY') && false === strpos($sql, 'LIMIT')) {
|
||||
$pattern = '/ORDER BY \S+ (ASC|DESC)?/';
|
||||
$sql = preg_replace($pattern, '', $sql);
|
||||
}
|
||||
|
||||
// LIMIT is not allowed in DELETE queries
|
||||
$sql = str_replace('LIMIT 1', '', $sql);
|
||||
$sql = str_replace(' REGEXP ', ' ~ ', $sql);
|
||||
|
||||
// Get the WordPress table prefix
|
||||
$prefix = $wpdb->prefix;
|
||||
|
||||
// This handles removal of duplicate entries in table options
|
||||
if(false !== strpos($sql, 'DELETE o1 FROM ')) {
|
||||
$sql = "DELETE FROM $wpdb->options WHERE option_id IN " .
|
||||
"(SELECT o1.option_id FROM $wpdb->options AS o1, $wpdb->options AS o2 " .
|
||||
"WHERE o1.option_name = o2.option_name " .
|
||||
"AND o1.option_id < o2.option_id)";
|
||||
}
|
||||
// Rewrite _transient_timeout multi-table delete query with dynamic prefix for options table
|
||||
elseif(preg_match('/DELETE a, b FROM ' . preg_quote($prefix, '/') . 'options a, ' . preg_quote($prefix, '/') . 'options b/', $sql)) {
|
||||
$where = substr($sql, strpos($sql, 'WHERE ') + 6);
|
||||
$where = rtrim($where, " \t\n\r;");
|
||||
// Fix string/number comparison by adding check and cast
|
||||
$where = str_replace('AND b.option_value', 'AND b.option_value ~ \'^[0-9]+$\' AND CAST(b.option_value AS BIGINT)', $where);
|
||||
// Mirror WHERE clause to delete both sides of self-join.
|
||||
$where2 = strtr($where, array('a.' => 'b.', 'b.' => 'a.'));
|
||||
$sql = "DELETE FROM {$wpdb->options} a USING {$wpdb->options} b WHERE " .
|
||||
'(' . $where . ') OR (' . $where2 . ');';
|
||||
}
|
||||
|
||||
// Rewrite _transient_timeout multi-table delete query with dynamic prefix for sitemeta table
|
||||
elseif(preg_match('/DELETE a, b FROM ' . preg_quote($prefix, '/') . 'sitemeta a, ' . preg_quote($prefix, '/') . 'sitemeta b/', $sql)) {
|
||||
$where = substr($sql, strpos($sql, 'WHERE ') + 6);
|
||||
$where = rtrim($where, " \t\n\r;");
|
||||
// Fix string/number comparison by adding check and cast
|
||||
$where = str_replace('AND b.meta_value', 'AND b.meta_value ~ \'^[0-9]+$\' AND CAST(b.meta_value AS BIGINT)', $where);
|
||||
// Mirror WHERE clause to delete both sides of self-join.
|
||||
$where2 = strtr($where, array('a.' => 'b.', 'b.' => 'a.'));
|
||||
// Use $wpdb's sitemeta table name which should already have the correct prefix
|
||||
if(isset($wpdb->sitemeta)) {
|
||||
$sql = "DELETE FROM {$wpdb->sitemeta} a USING {$wpdb->sitemeta} b WHERE " .
|
||||
'(' . $where . ') OR (' . $where2 . ');';
|
||||
} else {
|
||||
// Fallback if $wpdb->sitemeta is not available
|
||||
$sql = "DELETE FROM {$prefix}sitemeta a USING {$prefix}sitemeta b WHERE " .
|
||||
'(' . $where . ') OR (' . $where2 . ');';
|
||||
}
|
||||
}
|
||||
|
||||
// Add a more general pattern to handle multi-table DELETE with aliases and dynamic table names
|
||||
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)) {
|
||||
// Extract aliases and table names
|
||||
$firstAlias = $matches[1];
|
||||
$secondAlias = $matches[2];
|
||||
$firstTable = $matches[3];
|
||||
$firstTableAlias = $matches[4];
|
||||
$secondTable = $matches[5];
|
||||
$secondTableAlias = $matches[6];
|
||||
|
||||
// Extract WHERE clause
|
||||
$where = substr($sql, strpos($sql, 'WHERE ') + 6);
|
||||
$where = rtrim($where, " \t\n\r;");
|
||||
|
||||
// Check if the table names are known WordPress tables and replace with dynamic property references
|
||||
foreach([$firstTable, $secondTable] as $index => $tableName) {
|
||||
// Strip prefix if it exists to get the base table name
|
||||
$baseTableName = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $tableName);
|
||||
|
||||
// Check if $wpdb has a property for this table
|
||||
if(isset($wpdb->$baseTableName)) {
|
||||
// Replace the hardcoded table name with the dynamic property
|
||||
if($index === 0) {
|
||||
$firstTable = $wpdb->$baseTableName;
|
||||
} else {
|
||||
$secondTable = $wpdb->$baseTableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate PostgreSQL DELETE...USING syntax
|
||||
$sql = "DELETE FROM $firstTable $firstTableAlias USING $secondTable $secondTableAlias WHERE $where;";
|
||||
}
|
||||
|
||||
// Akismet sometimes doesn't write 'comment_ID' with 'ID' in capitals where needed ...
|
||||
if(false !== strpos($sql, $wpdb->comments)) {
|
||||
$sql = str_replace(' comment_id ', ' comment_ID ', $sql);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
class DescribeSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
$sql = $this->original();
|
||||
$table = $this->extractTableName($sql);
|
||||
return $this->generatePostgresDescribeTable($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts table name from a "DESCRIBE" SQL statement.
|
||||
*
|
||||
* @param string $sql The SQL statement
|
||||
* @return string|null The table name if found, or null otherwise
|
||||
*/
|
||||
protected function extractTableName($sql)
|
||||
{
|
||||
$pattern = "/DESCRIBE ['\"`]?([^'\"`]+)['\"`]?/i";
|
||||
if (preg_match($pattern, $sql, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PostgreSQL-compatible SQL query to mimic MySQL's "DESCRIBE".
|
||||
*
|
||||
* @param string $tableName The table name
|
||||
* @param string $schema The schema name
|
||||
* @return string The generated SQL query
|
||||
*/
|
||||
public function generatePostgresDescribeTable($tableName, $schema = "public")
|
||||
{
|
||||
$sql = <<<SQL
|
||||
SELECT
|
||||
f.attnum AS number,
|
||||
f.attname AS name,
|
||||
f.attnum,
|
||||
f.attnotnull AS notnull,
|
||||
pg_catalog.format_type(f.atttypid,f.atttypmod) AS type,
|
||||
CASE
|
||||
WHEN p.contype = 'p' THEN 't'
|
||||
ELSE 'f'
|
||||
END AS primarykey,
|
||||
CASE
|
||||
WHEN p.contype = 'u' THEN 't'
|
||||
ELSE 'f'
|
||||
END AS uniquekey,
|
||||
CASE
|
||||
WHEN p.contype = 'f' THEN g.relname
|
||||
END AS foreignkey,
|
||||
CASE
|
||||
WHEN p.contype = 'f' THEN p.confkey
|
||||
END AS foreignkey_fieldnum,
|
||||
CASE
|
||||
WHEN p.contype = 'f' THEN g.relname
|
||||
END AS foreignkey,
|
||||
CASE
|
||||
WHEN p.contype = 'f' THEN p.conkey
|
||||
END AS foreignkey_connnum,
|
||||
CASE
|
||||
WHEN f.atthasdef = 't' THEN pg_get_expr(d.adbin, d.adrelid)
|
||||
END AS default
|
||||
FROM pg_attribute f
|
||||
JOIN pg_class c ON c.oid = f.attrelid
|
||||
JOIN pg_type t ON t.oid = f.atttypid
|
||||
LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = f.attnum
|
||||
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
LEFT JOIN pg_constraint p ON p.conrelid = c.oid AND f.attnum = ANY (p.conkey)
|
||||
LEFT JOIN pg_class AS g ON p.confrelid = g.oid
|
||||
WHERE c.relkind = 'r'::char
|
||||
AND n.nspname = '$schema'
|
||||
AND c.relname = '$tableName'
|
||||
AND f.attnum > 0 ORDER BY number
|
||||
SQL;
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
class DropTableSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
$sql = $this->original();
|
||||
|
||||
$pattern = '/DROP TABLE.+ [`]?(\w+)[`]?$/';
|
||||
preg_match($pattern, $sql, $matches);
|
||||
$table = $matches[1];
|
||||
$seq = $table . '_seq';
|
||||
$sql .= ";\nDROP SEQUENCE IF EXISTS $seq;";
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
class InsertSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$sql = $this->original();
|
||||
|
||||
// Those are used when we need to set the date to now() in gmt time
|
||||
$sql = str_replace("'0000-00-00 00:00:00'", 'now() AT TIME ZONE \'gmt\'', $sql);
|
||||
|
||||
$sql = str_replace("utc_timestamp()", "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'", $sql);
|
||||
|
||||
// Multiple values group when calling INSERT INTO don't always work
|
||||
if(false !== strpos($sql, $wpdb->options) && false !== strpos($sql, '), (')) {
|
||||
$pattern = '/INSERT INTO.+VALUES/';
|
||||
preg_match($pattern, $sql, $matches);
|
||||
$insert = $matches[0];
|
||||
$sql = str_replace('), (', ');' . $insert . '(', $sql);
|
||||
}
|
||||
|
||||
// Swap ON DUPLICATE KEY SYNTAX
|
||||
if(false !== $pos = strpos($sql, 'ON DUPLICATE KEY UPDATE')) {
|
||||
$splitStatements = function (string $sql): array {
|
||||
$statements = [];
|
||||
$buffer = '';
|
||||
$quote = null;
|
||||
|
||||
for ($i = 0, $len = strlen($sql); $i < $len; $i++) {
|
||||
$char = $sql[$i];
|
||||
|
||||
if ($quote) {
|
||||
if ($char === $quote && $sql[$i - 1] !== '\\') {
|
||||
$quote = null;
|
||||
}
|
||||
} elseif ($char === '"' || $char === "'") {
|
||||
$quote = $char;
|
||||
} elseif ($char === ';') {
|
||||
$statements[] = $buffer . ';';
|
||||
$buffer = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
$buffer .= $char;
|
||||
}
|
||||
|
||||
if (!empty($buffer)) {
|
||||
$statements[] = $buffer;
|
||||
}
|
||||
|
||||
return $statements;
|
||||
};
|
||||
|
||||
$statements = $splitStatements($sql);
|
||||
|
||||
foreach ($statements as $statement) {
|
||||
$statement = trim($statement);
|
||||
|
||||
// Skip empty statements
|
||||
if (empty($statement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace backticks with double quotes for PostgreSQL compatibility
|
||||
$statement = str_replace('`', '"', $statement);
|
||||
|
||||
// Find index positions for the SQL components
|
||||
$insertIndex = strpos($statement, 'INSERT INTO');
|
||||
$valuesIndex = strpos($statement, 'VALUES');
|
||||
$onDuplicateKeyIndex = strpos($statement, 'ON DUPLICATE KEY UPDATE');
|
||||
|
||||
// Extract SQL components
|
||||
$tableSection = trim(substr($statement, $insertIndex, $valuesIndex - $insertIndex));
|
||||
$valuesSection = trim(substr($statement, $valuesIndex, $onDuplicateKeyIndex - $valuesIndex));
|
||||
$updateSection = trim(str_replace('ON DUPLICATE KEY UPDATE', '', substr($statement, $onDuplicateKeyIndex)));
|
||||
|
||||
// Extract and clean up column names from the update section
|
||||
$updateCols = explode(',', $updateSection);
|
||||
$updateCols = array_map(function ($col) {
|
||||
return trim(explode('=', $col)[0]);
|
||||
}, $updateCols);
|
||||
|
||||
// Choose a primary key for ON CONFLICT
|
||||
$primaryKey = 'option_name';
|
||||
if (!in_array($primaryKey, $updateCols)) {
|
||||
$primaryKey = 'meta_name';
|
||||
if (!in_array($primaryKey, $updateCols)) {
|
||||
$primaryKey = $updateCols[0] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
// Construct the PostgreSQL ON CONFLICT DO UPDATE section
|
||||
$updateSection = implode(', ', array_map(fn ($col) => "$col = EXCLUDED.$col", $updateCols));
|
||||
|
||||
// Construct the PostgreSQL query
|
||||
$postgresSQL = sprintf('%s %s ON CONFLICT (%s) DO UPDATE SET %s', $tableSection, $valuesSection, $primaryKey, $updateSection);
|
||||
|
||||
// Append to the converted statements list
|
||||
$convertedStatements[] = $postgresSQL;
|
||||
}
|
||||
|
||||
$sql = implode('; ', $convertedStatements);
|
||||
} elseif(0 === strpos($sql, 'INSERT IGNORE')) {
|
||||
// Note: Requires PostgreSQL 9.5
|
||||
$sql = 'INSERT' . substr($sql, 13) . ' ON CONFLICT DO NOTHING';
|
||||
}
|
||||
|
||||
// To avoid Encoding errors when inserting data coming from outside
|
||||
if(preg_match('/^.{1}/us', $sql, $ar) != 1) {
|
||||
$sql = utf8_encode($sql);
|
||||
}
|
||||
|
||||
if(false === strpos($sql, 'RETURNING')) {
|
||||
$end_of_statement = $this->findSemicolon($sql);
|
||||
if ($end_of_statement !== false) {
|
||||
// Create the substrings up to and after the semicolon
|
||||
$sql_before_semicolon = substr($sql, 0, $end_of_statement);
|
||||
$sql_after_semicolon = substr($sql, $end_of_statement, strlen($sql));
|
||||
|
||||
// Splice the SQL string together with 'RETURNING *'
|
||||
$sql = $sql_before_semicolon . ' RETURNING *' . $sql_after_semicolon;
|
||||
|
||||
} else {
|
||||
$sql = $sql .= " RETURNING *";
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
// finds semicolons that aren't in variables
|
||||
private function findSemicolon($sql)
|
||||
{
|
||||
$quoteOpened = false;
|
||||
$parenthesisDepth = 0;
|
||||
|
||||
$sqlAsArray = str_split($sql);
|
||||
for($i = 0; $i < count($sqlAsArray); $i++) {
|
||||
if(($sqlAsArray[$i] == '"' || $sqlAsArray[$i] == "'") && ($i == 0 || $sqlAsArray[$i - 1] != '\\')) {
|
||||
$quoteOpened = !$quoteOpened;
|
||||
} elseif($sqlAsArray[$i] == '(' && !$quoteOpened) {
|
||||
$parenthesisDepth++;
|
||||
} elseif($sqlAsArray[$i] == ')' && !$quoteOpened) {
|
||||
$parenthesisDepth--;
|
||||
} elseif($sqlAsArray[$i] == ';' && !$quoteOpened && $parenthesisDepth == 0) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
class OptimizeTableSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
$sql = $this->original();
|
||||
return str_replace('OPTIMIZE TABLE', 'VACUUM', $sql);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
class ReplaceIntoSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$sql = $this->original();
|
||||
|
||||
$splitStatements = function (string $sql): array {
|
||||
$statements = [];
|
||||
$buffer = '';
|
||||
$quote = null;
|
||||
|
||||
for ($i = 0, $len = strlen($sql); $i < $len; $i++) {
|
||||
$char = $sql[$i];
|
||||
|
||||
if ($quote) {
|
||||
if ($char === $quote && $sql[$i - 1] !== '\\') {
|
||||
$quote = null;
|
||||
}
|
||||
} elseif ($char === '"' || $char === "'") {
|
||||
$quote = $char;
|
||||
} elseif ($char === ';') {
|
||||
$statements[] = $buffer . ';';
|
||||
$buffer = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
$buffer .= $char;
|
||||
}
|
||||
|
||||
if (!empty($buffer)) {
|
||||
$statements[] = $buffer;
|
||||
}
|
||||
|
||||
return $statements;
|
||||
};
|
||||
|
||||
$statements = $splitStatements($sql);
|
||||
|
||||
foreach ($statements as $statement) {
|
||||
$statement = trim($statement);
|
||||
|
||||
// Skip empty statements
|
||||
if (empty($statement)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace backticks with double quotes for PostgreSQL compatibility
|
||||
$statement = str_replace('`', '"', $statement);
|
||||
|
||||
// Find index positions for the SQL components
|
||||
$insertIndex = strpos($statement, 'REPLACE INTO');
|
||||
$columnsStartIndex = strpos($statement, "(");
|
||||
$columnsEndIndex = strpos($statement, ")");
|
||||
$valuesIndex = strpos($statement, 'VALUES');
|
||||
$onDuplicateKeyIndex = strpos($statement, 'ON DUPLICATE KEY UPDATE');
|
||||
|
||||
// Extract SQL components
|
||||
$tableSection = trim(substr($statement, $insertIndex, $columnsStartIndex - $insertIndex));
|
||||
$valuesSection = trim(substr($statement, $valuesIndex, strlen($statement) - $valuesIndex));
|
||||
$columnsSection = trim(substr($statement, $columnsStartIndex, $columnsEndIndex - $columnsStartIndex + 1));
|
||||
|
||||
// Extract and clean up column names from the update section
|
||||
$updateCols = explode(',', substr($columnsSection, 1, strlen($columnsSection) - 2));
|
||||
$updateCols = array_map(function ($col) {
|
||||
return trim($col);
|
||||
}, $updateCols);
|
||||
|
||||
// Choose a primary key for ON CONFLICT
|
||||
$primaryKey = 'option_name';
|
||||
if (!in_array($primaryKey, $updateCols)) {
|
||||
$primaryKey = 'meta_name';
|
||||
if (!in_array($primaryKey, $updateCols)) {
|
||||
$primaryKey = $updateCols[0] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
// SWAP REPLACE INTO for INSERT INTO
|
||||
$tableSection = str_replace("REPLACE INTO", "INSERT INTO", $tableSection);
|
||||
|
||||
// Construct the PostgreSQL ON CONFLICT DO UPDATE section
|
||||
$updateSection = "";
|
||||
foreach($updateCols as $col) {
|
||||
if ($col !== $primaryKey) {
|
||||
$updateSection .= ", ";
|
||||
$updateSection .= "$col = EXCLUDED.$col";
|
||||
}
|
||||
}
|
||||
|
||||
// trim any preceding commas
|
||||
$updateSection = ltrim($updateSection, ", ");
|
||||
|
||||
// Construct the PostgreSQL query
|
||||
$postgresSQL = sprintf('%s %s %s ON CONFLICT (%s) DO UPDATE SET %s', $tableSection, $columnsSection, $valuesSection, $primaryKey, $updateSection);
|
||||
|
||||
if(false === strpos($postgresSQL, 'RETURNING')) {
|
||||
$end_of_statement = $this->findSemicolon($postgresSQL);
|
||||
if ($end_of_statement !== false) {
|
||||
// Create the substrings up to and after the semicolon
|
||||
$sql_before_semicolon = substr($postgresSQL, 0, $end_of_statement);
|
||||
$sql_after_semicolon = substr($postgresSQL, $end_of_statement, strlen($postgresSQL));
|
||||
|
||||
// Splice the SQL string together with 'RETURNING *'
|
||||
$postgresSQL = $sql_before_semicolon . ' RETURNING *' . $sql_after_semicolon;
|
||||
|
||||
} else {
|
||||
$postgresSQL = $postgresSQL .= " RETURNING *";
|
||||
}
|
||||
}
|
||||
|
||||
// Append to the converted statements list
|
||||
$convertedStatements[] = $postgresSQL;
|
||||
}
|
||||
|
||||
$sql = implode('; ', $convertedStatements);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
// finds semicolons that aren't in variables
|
||||
private function findSemicolon($sql)
|
||||
{
|
||||
$quoteOpened = false;
|
||||
$parenthesisDepth = 0;
|
||||
|
||||
$sqlAsArray = str_split($sql);
|
||||
for($i = 0; $i < count($sqlAsArray); $i++) {
|
||||
if(($sqlAsArray[$i] == '"' || $sqlAsArray[$i] == "'") && ($i == 0 || $sqlAsArray[$i - 1] != '\\')) {
|
||||
$quoteOpened = !$quoteOpened;
|
||||
} elseif($sqlAsArray[$i] == '(' && !$quoteOpened) {
|
||||
$parenthesisDepth++;
|
||||
} elseif($sqlAsArray[$i] == ')' && !$quoteOpened) {
|
||||
$parenthesisDepth--;
|
||||
} elseif($sqlAsArray[$i] == ';' && !$quoteOpened && $parenthesisDepth == 0) {
|
||||
return $i;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
<?php
|
||||
|
||||
class SelectSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$sql = $this->original();
|
||||
|
||||
// SQL_CALC_FOUND_ROWS doesn't exist in PostgreSQL but it's needed for correct paging
|
||||
if(false !== strpos($sql, 'SQL_CALC_FOUND_ROWS')) {
|
||||
$sql = str_replace('SQL_CALC_FOUND_ROWS', '', $sql);
|
||||
$GLOBALS['pg4wp_numrows_query'] = $sql;
|
||||
if(PG4WP_DEBUG) {
|
||||
error_log('[' . microtime(true) . "] Number of rows required for :\n$sql\n---------------------\n", 3, PG4WP_LOG . 'pg4wp_NUMROWS.log');
|
||||
}
|
||||
}
|
||||
|
||||
if(false !== strpos($sql, 'FOUND_ROWS()')) {
|
||||
// Here we convert the latest query into a COUNT query
|
||||
$sql = $GLOBALS['pg4wp_numrows_query'];
|
||||
|
||||
// Remove the LIMIT clause if it exists
|
||||
$sql = preg_replace('/\s+LIMIT\s+\d+(\s*,\s*\d+)?/i', '', $sql);
|
||||
|
||||
// Remove the ORDER BY containing case / end clause if it exists
|
||||
$sql = preg_replace('/\s+ORDER\s+BY\s+.+END\),[^)]+/is', '', $sql);
|
||||
|
||||
// Remove the ORDER BY clause if it exists
|
||||
$sql = preg_replace('/\s+ORDER\s+BY\s+[^)]+/i', '', $sql);
|
||||
|
||||
// Replace the fields in the SELECT clause with COUNT(*)
|
||||
$sql = preg_replace('/SELECT\s+.*?\s+FROM\s+/is', 'SELECT COUNT(*) FROM ', $sql, 1);
|
||||
}
|
||||
|
||||
if(false !== strpos($sql, 'information_schema')) {
|
||||
// WP Site Health rewrites
|
||||
if (false !== strpos($sql, "SELECT TABLE_NAME AS 'table', TABLE_ROWS AS 'rows', SUM(data_length + index_length)")) {
|
||||
$sql = $this->postgresTableSizeRewrite();
|
||||
return $sql;
|
||||
}
|
||||
|
||||
throw new Exception("Unsupported call to information_schema, this probably won't work correctly and needs to be specifically handled, open a github issue with the SQL");
|
||||
}
|
||||
|
||||
$sql = $this->ensureOrderByInSelect($sql);
|
||||
|
||||
// Handle +0 casting in order by
|
||||
// Regular expression to match the "ORDER BY" pattern
|
||||
$pattern = '/ORDER BY\s+([a-zA-Z0-9_]+)\.meta_value\s*\+\s*0/i';
|
||||
$replacement = 'ORDER BY CAST($1.meta_value AS SIGNED)';
|
||||
$sql = preg_replace($pattern, $replacement, $sql);
|
||||
|
||||
// Convert CONVERT to CAST
|
||||
$pattern = '/CONVERT\(([^()]*(\(((?>[^()]+)|(?-2))*\))?[^()]*),\s*([^\s]+)\)/x';
|
||||
$sql = preg_replace($pattern, 'CAST($1 AS $4)', $sql);
|
||||
|
||||
// Handle CAST( ... AS CHAR)
|
||||
$sql = preg_replace('/CAST\((.+) AS CHAR\)/', 'CAST($1 AS TEXT)', $sql);
|
||||
|
||||
// Handle CAST( ... AS SIGNED)
|
||||
$sql = preg_replace('/CAST\((.+) AS SIGNED\)/', 'CAST($1 AS INTEGER)', $sql);
|
||||
|
||||
// Handle COUNT(*)...ORDER BY...
|
||||
$sql = preg_replace('/COUNT(.+)ORDER BY.+/s', 'COUNT$1', $sql);
|
||||
|
||||
// HANDLE REGEXP
|
||||
$sql = preg_replace('/REGEXP/', '~', $sql);
|
||||
|
||||
$sql = str_replace("utc_timestamp()", "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'", $sql);
|
||||
|
||||
// In order for users counting to work...
|
||||
$matches = array();
|
||||
if(preg_match_all('/COUNT[^C]+\),/', $sql, $matches)) {
|
||||
foreach($matches[0] as $num => $one) {
|
||||
$sub = substr($one, 0, -1);
|
||||
$sql = str_replace($sub, $sub . ' AS count' . $num, $sql);
|
||||
}
|
||||
}
|
||||
|
||||
$sql = $this->convertToPostgresLimitSyntax($sql);
|
||||
$sql = $this->ensureGroupByOrAggregate($sql);
|
||||
|
||||
$pattern = '/DATE_ADD[ ]*\(([^,]+),([^\)]+)\)/';
|
||||
$sql = preg_replace($pattern, '($1 + $2)', $sql);
|
||||
|
||||
// Convert MySQL FIELD function to CASE statement
|
||||
$pattern = '/FIELD[ ]*\(([^\),]+),([^\)]+)\)/';
|
||||
// https://dev.mysql.com/doc/refman/5.7/en/string-functions.html#function_field
|
||||
// Other implementations: https://stackoverflow.com/q/1309624
|
||||
$sql = preg_replace_callback($pattern, function ($matches) {
|
||||
$case = 'CASE ' . trim($matches[1]);
|
||||
$comparands = explode(',', $matches[2]);
|
||||
foreach($comparands as $i => $comparand) {
|
||||
$case .= ' WHEN ' . trim($comparand) . ' THEN ' . ($i + 1);
|
||||
}
|
||||
$case .= ' ELSE 0 END';
|
||||
return $case;
|
||||
}, $sql);
|
||||
|
||||
$pattern = '/GROUP_CONCAT\(([^()]*(\(((?>[^()]+)|(?-2))*\))?[^()]*)\)/x';
|
||||
$sql = preg_replace($pattern, "string_agg($1, ',')", $sql);
|
||||
|
||||
// Convert MySQL RAND function to PostgreSQL RANDOM function
|
||||
$pattern = '/RAND[ ]*\([ ]*\)/';
|
||||
$sql = preg_replace($pattern, 'RANDOM()', $sql);
|
||||
|
||||
// UNIX_TIMESTAMP in MYSQL returns an integer
|
||||
$pattern = '/UNIX_TIMESTAMP\(([^\)]+)\)/';
|
||||
$sql = preg_replace($pattern, 'ROUND(DATE_PART(\'epoch\',$1))', $sql);
|
||||
|
||||
$date_funcs = array(
|
||||
'DAYOFMONTH(' => 'EXTRACT(DAY FROM ',
|
||||
'YEAR(' => 'EXTRACT(YEAR FROM ',
|
||||
'MONTH(' => 'EXTRACT(MONTH FROM ',
|
||||
'DAY(' => 'EXTRACT(DAY FROM ',
|
||||
);
|
||||
|
||||
$sql = str_replace('ORDER BY post_date DESC', 'ORDER BY YEAR(post_date) DESC, MONTH(post_date) DESC', $sql);
|
||||
$sql = str_replace('ORDER BY post_date ASC', 'ORDER BY YEAR(post_date) ASC, MONTH(post_date) ASC', $sql);
|
||||
$sql = str_replace(array_keys($date_funcs), array_values($date_funcs), $sql);
|
||||
$curryear = date('Y');
|
||||
$sql = str_replace('FROM \'' . $curryear, 'FROM TIMESTAMP \'' . $curryear, $sql);
|
||||
|
||||
// MySQL 'IF' conversion - Note : NULLIF doesn't need to be corrected
|
||||
$pattern = '/ (?<!NULL)IF[ ]*\(([^,]+),([^,]+),([^\)]+)\)/';
|
||||
$sql = preg_replace($pattern, ' CASE WHEN $1 THEN $2 ELSE $3 END', $sql);
|
||||
|
||||
// Act like MySQL default configuration, where sql_mode is ""
|
||||
$pattern = '/@@SESSION.sql_mode/';
|
||||
$sql = preg_replace($pattern, "''", $sql);
|
||||
|
||||
|
||||
// TODO: this seems wrong but if we remove it we get failures with XYZ is not part of the group By
|
||||
if(isset($wpdb)) {
|
||||
$sql = str_replace('GROUP BY ' . $wpdb->prefix . 'posts.ID', '', $sql);
|
||||
}
|
||||
|
||||
// MySQL 'LIKE' is case insensitive by default, whereas PostgreSQL 'LIKE' is
|
||||
$sql = str_replace(' LIKE ', ' ILIKE ', $sql);
|
||||
|
||||
// INDEXES are not yet supported
|
||||
if(false !== strpos($sql, 'USE INDEX (comment_date_gmt)')) {
|
||||
$sql = str_replace('USE INDEX (comment_date_gmt)', '', $sql);
|
||||
}
|
||||
|
||||
// HB : timestamp fix for permalinks
|
||||
$sql = str_replace('post_date_gmt > 1970', 'post_date_gmt > to_timestamp (\'1970\')', $sql);
|
||||
|
||||
// Akismet sometimes doesn't write 'comment_ID' with 'ID' in capitals where needed ...
|
||||
if(isset($wpdb) && $wpdb->comments && false !== strpos($sql, $wpdb->comments)) {
|
||||
$sql = str_replace(' comment_id ', ' comment_ID ', $sql);
|
||||
}
|
||||
|
||||
// MySQL treats a HAVING clause without GROUP BY like WHERE
|
||||
if(false !== strpos($sql, 'HAVING') && false === strpos($sql, 'GROUP BY')) {
|
||||
if(false === strpos($sql, 'WHERE')) {
|
||||
$sql = str_replace('HAVING', 'WHERE', $sql);
|
||||
} else {
|
||||
$pattern = '/WHERE\s+(.*?)\s+HAVING\s+(.*?)(\s*(?:ORDER|LIMIT|PROCEDURE|INTO|FOR|LOCK|$))/';
|
||||
$sql = preg_replace($pattern, 'WHERE ($1) AND ($2) $3', $sql);
|
||||
}
|
||||
}
|
||||
|
||||
// MySQL allows integers to be used as boolean expressions
|
||||
// where 0 is false and all other values are true.
|
||||
//
|
||||
// Although this could occur anywhere with any number, so far it
|
||||
// has only been observed as top-level expressions in the WHERE
|
||||
// clause and only with 0. For performance, limit current
|
||||
// replacements to that.
|
||||
$pattern_after_where = '(?:\s*$|\s+(GROUP|HAVING|ORDER|LIMIT|PROCEDURE|INTO|FOR|LOCK))';
|
||||
$pattern = '/(WHERE\s+)0(\s+AND|\s+OR|' . $pattern_after_where . ')/';
|
||||
$sql = preg_replace($pattern, '$1false$2', $sql);
|
||||
|
||||
$pattern = '/(AND\s+|OR\s+)0(' . $pattern_after_where . ')/';
|
||||
$sql = preg_replace($pattern, '$1false$2', $sql);
|
||||
|
||||
// MySQL supports strings as names, PostgreSQL needs identifiers.
|
||||
// Limit to after closing parenthesis to reduce false-positives
|
||||
// Currently only an issue for nextgen-gallery plugin
|
||||
$pattern = '/\) AS \'([^\']+)\'/';
|
||||
$sql = preg_replace($pattern, ') AS "$1"', $sql);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the columns used in the ORDER BY clause are also present in the SELECT clause.
|
||||
*
|
||||
* @param string $sql Original SQL query string.
|
||||
* @return string Modified SQL query string.
|
||||
*/
|
||||
protected function ensureOrderByInSelect(string $sql): string
|
||||
{
|
||||
// Extract the SELECT and ORDER BY clauses
|
||||
preg_match('/SELECT\s+(.*?)\s+FROM/si', $sql, $selectMatches);
|
||||
preg_match('/ORDER BY(.*?)(ASC|DESC|$)/si', $sql, $orderMatches);
|
||||
preg_match('/GROUP BY(.*?)(ASC|DESC|$)/si', $sql, $groupMatches);
|
||||
|
||||
// If the SELECT clause is missing, return the original query
|
||||
if (!$selectMatches) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
// If both ORDER BY and GROUP BY clauses are missing, return the original query
|
||||
if (!$orderMatches && !$groupMatches) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$selectClause = trim($selectMatches[1]);
|
||||
$orderByClause = $orderMatches ? trim($orderMatches[1]) : null;
|
||||
$groupClause = $groupMatches ? trim($groupMatches[1]) : null;
|
||||
$modified = false;
|
||||
|
||||
// Check for wildcard in SELECT
|
||||
if (strpos($selectClause, '*') !== false) {
|
||||
return $sql; // Cannot handle wildcards, return original query
|
||||
}
|
||||
|
||||
// Handle ORDER BY columns
|
||||
if ($orderByClause) {
|
||||
$orderByColumns = explode(',', $orderByClause);
|
||||
foreach ($orderByColumns as $col) {
|
||||
$col = trim($col);
|
||||
if (strpos($selectClause, $col) === false) {
|
||||
$selectClause .= ', ' . $col;
|
||||
$modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle GROUP BY columns
|
||||
if ($groupClause && !$modified) {
|
||||
$groupColumns = explode(',', $groupClause);
|
||||
foreach ($groupColumns as $col) {
|
||||
$col = trim($col);
|
||||
if (strpos($selectClause, $col) === false) {
|
||||
$selectClause .= ', ' . $col;
|
||||
$modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$modified) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
// Find the exact position for the replacement
|
||||
$selectStartPos = strpos($sql, $selectMatches[1]);
|
||||
if ($selectStartPos === false) {
|
||||
return $sql; // If for some reason the exact match is not found, return the original query
|
||||
}
|
||||
$postgresSql = substr_replace($sql, $selectClause, $selectStartPos, strlen($selectMatches[1]));
|
||||
|
||||
return $postgresSql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a given SQL query to include a GROUP BY clause if the SELECT statement has both aggregate
|
||||
* and non-aggregate columns. This function is specifically designed to work with PostgreSQL.
|
||||
*
|
||||
* In PostgreSQL, a query that uses aggregate functions must group by all columns in the SELECT list that
|
||||
* are not part of the aggregate functions. Failing to do so results in a syntax error. This function
|
||||
* automatically adds a GROUP BY clause to meet this PostgreSQL requirement when both aggregate (COUNT, SUM,
|
||||
* AVG, MIN, MAX) and non-aggregate columns are present.
|
||||
*
|
||||
* @param string $sql The SQL query string to be transformed.
|
||||
*
|
||||
* @return string The transformed SQL query string with appropriate GROUP BY clause if required.
|
||||
*
|
||||
* @throws Exception If the SQL query cannot be parsed or modified.
|
||||
*
|
||||
* @example
|
||||
* Input: SELECT COUNT(id), username FROM users;
|
||||
* Output: SELECT COUNT(id), username FROM users GROUP BY username;
|
||||
*
|
||||
*/
|
||||
protected function ensureGroupByOrAggregate(string $sql): string
|
||||
{
|
||||
// Check for system or session variables
|
||||
if (preg_match('/@@[a-zA-Z0-9_]+/', $sql)) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
// Regular expression to capture main SQL components.
|
||||
$regex = '/(SELECT\s+)(.*?)(\s+FROM\s+)([^ ]+)(\s+WHERE\s+.*?(?= ORDER BY | GROUP BY | LIMIT |$))?(ORDER BY.*?(?= LIMIT |$))?(LIMIT.*?$)?/is';
|
||||
|
||||
// Capture main SQL components using regex
|
||||
if (!preg_match($regex, $sql, $matches)) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$selectClause = trim($matches[2] ?? '');
|
||||
$fromClause = trim($matches[4] ?? '');
|
||||
$whereClause = trim($matches[5] ?? '');
|
||||
$orderClause = trim($matches[6] ?? '');
|
||||
$limitClause = trim($matches[7] ?? '');
|
||||
|
||||
if (empty($selectClause) || empty($fromClause)) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
// Regular expression to match commas not within parentheses
|
||||
$pattern = '/,(?![^\(]*\))/';
|
||||
// Split columns using a comma, and then trim each element
|
||||
$columns = array_map('trim', preg_split($pattern, $selectClause));
|
||||
|
||||
$aggregateColumns = [];
|
||||
$nonAggregateColumns = [];
|
||||
|
||||
foreach ($columns as $col) {
|
||||
// Check for aggregate functions in the column
|
||||
if (preg_match('/(COUNT|SUM|AVG|MIN|MAX)\s*?\(/i', $col)) {
|
||||
$aggregateColumns[] = $col;
|
||||
} else {
|
||||
$nonAggregateColumns[] = $col;
|
||||
}
|
||||
}
|
||||
|
||||
// Only add a GROUP BY clause if there are both aggregate and non-aggregate columns in SELECT
|
||||
if (empty($aggregateColumns) || empty($nonAggregateColumns)) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
|
||||
// Assemble new SQL query
|
||||
$postgresSql = "SELECT $selectClause FROM $fromClause";
|
||||
|
||||
if (!empty($whereClause)) {
|
||||
$postgresSql .= ' ' . $whereClause;
|
||||
}
|
||||
|
||||
$groupByClause = "GROUP BY " . implode(", ", $nonAggregateColumns);
|
||||
if (!empty($groupByClause)) {
|
||||
$postgresSql .= ' ' . $groupByClause;
|
||||
}
|
||||
|
||||
if (!empty($orderClause)) {
|
||||
$postgresSql .= ' ' . $orderClause;
|
||||
}
|
||||
|
||||
if (!empty($limitClause)) {
|
||||
$postgresSql .= ' ' . $limitClause;
|
||||
}
|
||||
|
||||
return $postgresSql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert MySQL LIMIT syntax to PostgreSQL LIMIT syntax
|
||||
*
|
||||
* @param string $sql MySQL query string
|
||||
* @return string PostgreSQL query string
|
||||
*/
|
||||
protected function convertToPostgresLimitSyntax($sql)
|
||||
{
|
||||
// Use regex to find "LIMIT m, n" syntax in query
|
||||
if (preg_match('/LIMIT\s+(\d+),\s*(\d+)/i', $sql, $matches)) {
|
||||
$offset = $matches[1];
|
||||
$limit = $matches[2];
|
||||
|
||||
// Replace MySQL LIMIT syntax with PostgreSQL LIMIT syntax
|
||||
$postgresLimitSyntax = "LIMIT $limit OFFSET $offset";
|
||||
$postgresSql = preg_replace('/LIMIT\s+\d+,\s*\d+/i', $postgresLimitSyntax, $sql);
|
||||
|
||||
return $postgresSql;
|
||||
}
|
||||
|
||||
// Return original query if no MySQL LIMIT syntax is found
|
||||
return $sql;
|
||||
}
|
||||
|
||||
// This method is specifically to handle should_suggest_persistent_object_cache in wp site health
|
||||
protected function postgresTableSizeRewrite($schema = 'public')
|
||||
{
|
||||
|
||||
$sql = <<<SQL
|
||||
SELECT
|
||||
C.relname AS "table",
|
||||
S.n_live_tup AS "rows",
|
||||
pg_total_relation_size(C.oid) AS "bytes"
|
||||
FROM
|
||||
pg_class C
|
||||
LEFT JOIN
|
||||
pg_namespace N ON (N.oid = C.relnamespace)
|
||||
INNER JOIN
|
||||
pg_stat_user_tables S ON (S.relid = C.oid)
|
||||
WHERE
|
||||
N.nspname = '$schema' AND
|
||||
C.relname IN ('wp_comments','wp_options','wp_posts','wp_terms','wp_users')
|
||||
GROUP BY
|
||||
C.relname, pg_total_relation_size(C.oid), S.n_live_tup;
|
||||
SQL;
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
class SetNamesSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
return "SET NAMES 'utf8'";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
class ShowFullColumnsSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
$sql = $this->original();
|
||||
$table = $this->extractTableNameFromShowColumns($sql);
|
||||
return $this->generatePostgresShowColumns($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts table name from a "SHOW FULL COLUMNS" SQL statement.
|
||||
*
|
||||
* @param string $sql The SQL statement
|
||||
* @return string|null The table name if found, or null otherwise
|
||||
*/
|
||||
protected function extractTableNameFromShowColumns($sql)
|
||||
{
|
||||
$pattern = "/SHOW FULL COLUMNS FROM ['\"`]?([^'\"`]+)['\"`]?/i";
|
||||
if (preg_match($pattern, $sql, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PostgreSQL-compatible SQL query to mimic MySQL's "SHOW FULL COLUMNS".
|
||||
*
|
||||
* @param string $tableName The table name
|
||||
* @param string $schema The schema name
|
||||
* @return string The generated SQL query
|
||||
*/
|
||||
public function generatePostgresShowColumns($tableName, $schema = "public")
|
||||
{
|
||||
$sql = <<<SQL
|
||||
SELECT
|
||||
a.attname AS "Field",
|
||||
pg_catalog.format_type(a.atttypid, a.atttypmod) AS "Type",
|
||||
(CASE
|
||||
WHEN a.attnotnull THEN 'NO'
|
||||
ELSE 'YES'
|
||||
END) AS "Null",
|
||||
(CASE
|
||||
WHEN i.indisprimary THEN 'PRI'
|
||||
WHEN i.indisunique THEN 'UNI'
|
||||
ELSE ''
|
||||
END) AS "Key",
|
||||
pg_catalog.pg_get_expr(ad.adbin, ad.adrelid) AS "Default",
|
||||
'' AS "Extra",
|
||||
'select,insert,update,references' AS "Privileges",
|
||||
d.description AS "Comment"
|
||||
FROM
|
||||
pg_catalog.pg_attribute a
|
||||
LEFT JOIN pg_catalog.pg_description d ON (a.attrelid = d.objoid AND a.attnum = d.objsubid)
|
||||
LEFT JOIN pg_catalog.pg_attrdef ad ON (a.attrelid = ad.adrelid AND a.attnum = ad.adnum)
|
||||
LEFT JOIN pg_catalog.pg_index i ON (a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey))
|
||||
WHERE
|
||||
a.attnum > 0
|
||||
AND NOT a.attisdropped
|
||||
AND a.attrelid = (
|
||||
SELECT c.oid
|
||||
FROM pg_catalog.pg_class c
|
||||
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relname = '$tableName'
|
||||
AND n.nspname = '$schema'
|
||||
)
|
||||
ORDER BY
|
||||
a.attnum;
|
||||
SQL;
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
class ShowIndexSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
$sql = $this->original();
|
||||
$table = $this->extractTableNameFromShowIndex($sql);
|
||||
return $this->generatePostgresShowIndexFrom($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts table name from a "SHOW FULL COLUMNS" SQL statement.
|
||||
*
|
||||
* @param string $sql The SQL statement
|
||||
* @return string|null The table name if found, or null otherwise
|
||||
*/
|
||||
protected function extractVariableName($sql)
|
||||
{
|
||||
$pattern = "/SHOW INDEX FROM ['\"`]?([^'\"`]+)['\"`]?/i";
|
||||
if (preg_match($pattern, $sql, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PostgreSQL-compatible SQL query to mimic MySQL's "SHOW INDEX FROM".
|
||||
*
|
||||
* @param string $tableName The table name
|
||||
* @return string The generated SQL query
|
||||
*/
|
||||
public function generatePostgresShowIndexFrom($tableName)
|
||||
{
|
||||
$sql = <<<SQL
|
||||
SELECT bc.relname AS "Table",
|
||||
CASE WHEN i.indisunique THEN '0' ELSE '1' END AS "Non_unique",
|
||||
CASE WHEN i.indisprimary THEN 'PRIMARY' WHEN bc.relname LIKE '%usermeta' AND ic.relname = 'umeta_key'
|
||||
THEN 'meta_key' ELSE REPLACE( ic.relname, '' . $table . '_', '') END AS "Key_name",
|
||||
a.attname AS "Column_name",
|
||||
NULL AS "Sub_part"
|
||||
FROM pg_class bc, pg_class ic, pg_index i, pg_attribute a
|
||||
WHERE bc.oid = i.indrelid
|
||||
AND ic.oid = i.indexrelid
|
||||
AND (i.indkey[0] = a.attnum OR i.indkey[1] = a.attnum OR i.indkey[2] = a.attnum OR i.indkey[3] = a.attnum OR i.indkey[4] = a.attnum OR i.indkey[5] = a.attnum OR i.indkey[6] = a.attnum OR i.indkey[7] = a.attnum)
|
||||
AND a.attrelid = bc.oid
|
||||
AND bc.relname = '' . $tableName . ''
|
||||
ORDER BY "Key_name", CASE a.attnum
|
||||
WHEN i.indkey[0] THEN 0
|
||||
WHEN i.indkey[1] THEN 1
|
||||
WHEN i.indkey[2] THEN 2
|
||||
WHEN i.indkey[3] THEN 3
|
||||
WHEN i.indkey[4] THEN 4
|
||||
WHEN i.indkey[5] THEN 5
|
||||
WHEN i.indkey[6] THEN 6
|
||||
WHEN i.indkey[7] THEN 7
|
||||
END
|
||||
SQL;
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
class ShowTableStatusSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
$sql = $this->original();
|
||||
return $this->generatePostgresShowTableStatus();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates a PostgreSQL-compatible SQL query to mimic MySQL's "SHOW TABLE STATUS".
|
||||
*
|
||||
* @return string The generated SQL query
|
||||
*/
|
||||
public function generatePostgresShowTableStatus($schema = "public")
|
||||
{
|
||||
$sql = <<<SQL
|
||||
SELECT
|
||||
'Postgres' AS Engine,
|
||||
cls.relname AS TableName,
|
||||
NULL AS Version,
|
||||
NULL AS Row_format,
|
||||
cls.reltuples AS Rows,
|
||||
NULL AS Avg_row_length,
|
||||
pg_size_pretty(pg_relation_size(cls.oid)) AS Data_length,
|
||||
NULL AS Max_data_length,
|
||||
pg_size_pretty(pg_indexes_size(cls.oid)) AS Index_length,
|
||||
NULL AS Data_free,
|
||||
NULL AS Auto_increment,
|
||||
NULL AS Create_time,
|
||||
NULL AS Update_time,
|
||||
NULL AS Check_time,
|
||||
'UTF8' AS Table_collation,
|
||||
NULL AS Checksum,
|
||||
NULL AS Create_options,
|
||||
obj_description(cls.oid) AS Comment
|
||||
FROM
|
||||
pg_class cls
|
||||
JOIN
|
||||
pg_namespace nsp ON cls.relnamespace = nsp.oid
|
||||
WHERE
|
||||
cls.relkind = 'r'
|
||||
AND nsp.nspname NOT LIKE 'pg_%'
|
||||
AND nsp.nspname != 'information_schema'
|
||||
AND nsp.nspname = '$schema'
|
||||
ORDER BY
|
||||
cls.relname ASC;
|
||||
SQL;
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
class ShowTablesSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
$schema = "public";
|
||||
return 'SELECT tablename FROM pg_tables WHERE schemaname = \'$schema\';';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
class ShowVariablesSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
$sql = $this->original();
|
||||
$variableName = $this->extractVariableName($sql);
|
||||
return $this->generatePostgres($sql, $variableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts Variable name from a "SHOW VARIABLES LIKE " SQL statement.
|
||||
*
|
||||
* @param string $sql The SQL statement
|
||||
* @return string|null The table name if found, or null otherwise
|
||||
*/
|
||||
protected function extractVariableName($sql)
|
||||
{
|
||||
$pattern = "/SHOW VARIABLES LIKE ['\"`]?([^'\"`]+)['\"`]?/i";
|
||||
if (preg_match($pattern, $sql, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a PostgreSQL-compatible SQL query to mimic MySQL's "SHOW VARIABLES".
|
||||
*
|
||||
* @param string $tableName The table name
|
||||
* @return string The generated SQL query
|
||||
*/
|
||||
public function generatePostgres($sql, $variableName)
|
||||
{
|
||||
if ($variableName == "sql_mode") {
|
||||
// Act like MySQL default configuration, where sql_mode is ""
|
||||
return "SELECT '$variableName' AS \"Variable_name\", '' AS \"Value\";";
|
||||
}
|
||||
|
||||
if ($variableName == "max_allowed_packet") {
|
||||
// Act like 1GB packet size, in practice this limit doesn't actually exist for postgres, we just want to fool WP
|
||||
return "SELECT '$variableName' AS \"Variable_name\", '1073741824' AS \"Value\";";
|
||||
}
|
||||
|
||||
return "SELECT name as \"Variable_name\", setting as \"Value\" FROM pg_settings WHERE name = '$variableName';";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
class UpdateSQLRewriter extends AbstractSQLRewriter
|
||||
{
|
||||
public function rewrite(): string
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$sql = $this->original();
|
||||
|
||||
$pattern = '/LIMIT[ ]+\d+/';
|
||||
$sql = preg_replace($pattern, '', $sql);
|
||||
|
||||
// Fix update wp_options
|
||||
$pattern = "/UPDATE `wp_options` SET `option_value` = NULL WHERE `option_name` = '(.+)'/";
|
||||
$match = "UPDATE `wp_options` SET `option_value` = '' WHERE `option_name` = '$1'";
|
||||
$sql = preg_replace($pattern, $match, $sql);
|
||||
|
||||
// For correct bactick removal
|
||||
$pattern = '/[ ]*`([^` ]+)`[ ]*=/';
|
||||
$sql = preg_replace($pattern, ' $1 =', $sql);
|
||||
|
||||
// Those are used when we need to set the date to now() in gmt time
|
||||
$sql = str_replace("'0000-00-00 00:00:00'", 'now() AT TIME ZONE \'gmt\'', $sql);
|
||||
|
||||
// For correct ID quoting
|
||||
$pattern = '/(,|\s)[ ]*([^ \']*ID[^ \']*)[ ]*=/';
|
||||
$sql = preg_replace($pattern, '$1 "$2" =', $sql);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user