diff --git a/pg4wp/driver_pgsql.php b/pg4wp/driver_pgsql.php index cea215a..ac83d76 100644 --- a/pg4wp/driver_pgsql.php +++ b/pg4wp/driver_pgsql.php @@ -16,6 +16,7 @@ $GLOBALS['pg4wp_numrows_query'] = ''; $GLOBALS['pg4wp_ins_table'] = ''; $GLOBALS['pg4wp_ins_field'] = ''; +$GLOBALS['pg4wp_ins_id'] = ''; $GLOBALS['pg4wp_last_insert'] = ''; $GLOBALS['pg4wp_connstr'] = ''; $GLOBALS['pg4wp_conn'] = false; @@ -465,6 +466,35 @@ function wpsqli_rollback(&$connection, $flags = 0, $name = null) pg_query($connection, "ROLLBACK"); } +function get_primary_key_for_table(&$connection, $table) +{ + $query = <<term_relationships) { + if($GLOBALS['pg4wp_ins_id']) { + return $GLOBALS['pg4wp_ins_id']; + } elseif(empty($sql)) { $sql = 'NO QUERY'; $data = 0; - } elseif ('post_author' == $ins_field && false !== strpos($lastq, 'ID')) { - // No PostgreSQL specific operation here. - $sql = 'ID was in query '; - $pattern = '/.+\'(\d+).+$/'; - preg_match($pattern, $lastq, $matches); - $data = $matches[1]; - - // PostgreSQL: Setting the value of the sequence based on the latest inserted ID. - $GLOBALS['pg4wp_queued_query'] = "SELECT SETVAL('$seq',(SELECT MAX(\"ID\") FROM $table)+1);"; } else { - // PostgreSQL: Using CURRVAL() to get the current value of the sequence. + $seq = wpsqli_get_primary_sequence_for_table($connection, $table); + $lastq = $GLOBALS['pg4wp_last_insert']; // Double quoting is needed to prevent seq from being lowercased automatically $sql = "SELECT CURRVAL('\"$seq\"')"; $res = pg_query($connection, $sql); diff --git a/pg4wp/rewriters/AlterTableSQLRewriter.php b/pg4wp/rewriters/AlterTableSQLRewriter.php index ac3d639..727b661 100644 --- a/pg4wp/rewriters/AlterTableSQLRewriter.php +++ b/pg4wp/rewriters/AlterTableSQLRewriter.php @@ -36,7 +36,6 @@ public function rewrite(): string $sql = $this->rewriteAddIndex($sql); return $sql; } - if (str_contains($sql, 'CHANGE COLUMN')) { $sql = $this->rewriteChangeColumn($sql); return $sql; @@ -65,7 +64,7 @@ public function rewrite(): string return $sql; } - private function rewriteAddIndex(string $sql): string + private function rewriteAddIndex(string $sql): string { $pattern = '/ALTER TABLE\s+(\w+)\s+ADD (UNIQUE |)INDEX\s+([^\s]+)\s+\(((?:[^\(\)]+|\([^\(\)]+\))+)\)/'; @@ -74,18 +73,18 @@ private function rewriteAddIndex(string $sql): string $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; } @@ -218,15 +217,16 @@ private function rewriteDropPrimaryKey(string $sql): string return $sql; } - private function rewrite_numeric_type($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 + + // Execute type find & replace $sql = preg_replace_callback($pattern, function ($matches) { return $matches[1]; }, $sql); @@ -260,7 +260,7 @@ private function rewrite_numeric_type($sql){ $sql = preg_replace($pattern, 'serial', $sql); } } - + return $sql; } diff --git a/pg4wp/rewriters/CreateTableSQLRewriter.php b/pg4wp/rewriters/CreateTableSQLRewriter.php index 948c9ce..0e26ba4 100644 --- a/pg4wp/rewriters/CreateTableSQLRewriter.php +++ b/pg4wp/rewriters/CreateTableSQLRewriter.php @@ -89,15 +89,16 @@ public function rewrite(): string return $sql; } - private function rewrite_numeric_type($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 + + // Execute type find & replace $sql = preg_replace_callback($pattern, function ($matches) { return $matches[1]; }, $sql); @@ -131,7 +132,7 @@ private function rewrite_numeric_type($sql){ $sql = preg_replace($pattern, 'serial', $sql); } } - + return $sql; } diff --git a/pg4wp/rewriters/InsertSQLRewriter.php b/pg4wp/rewriters/InsertSQLRewriter.php index b401e28..89d28a3 100644 --- a/pg4wp/rewriters/InsertSQLRewriter.php +++ b/pg4wp/rewriters/InsertSQLRewriter.php @@ -110,6 +110,42 @@ public function rewrite(): string $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; + } } diff --git a/pg4wp/rewriters/ReplaceIntoSQLRewriter.php b/pg4wp/rewriters/ReplaceIntoSQLRewriter.php index a47aab5..7e91948 100644 --- a/pg4wp/rewriters/ReplaceIntoSQLRewriter.php +++ b/pg4wp/rewriters/ReplaceIntoSQLRewriter.php @@ -61,12 +61,12 @@ public function rewrite(): string // 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)); + $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); + return trim($col); }, $updateCols); // Choose a primary key for ON CONFLICT @@ -91,11 +91,26 @@ public function rewrite(): string } // trim any preceding commas - $updateSection = ltrim($updateSection,", "); + $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; } @@ -104,4 +119,25 @@ public function rewrite(): string 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; + } } diff --git a/pg4wp/rewriters/SelectSQLRewriter.php b/pg4wp/rewriters/SelectSQLRewriter.php index 129dbb1..62d2a2a 100644 --- a/pg4wp/rewriters/SelectSQLRewriter.php +++ b/pg4wp/rewriters/SelectSQLRewriter.php @@ -37,7 +37,7 @@ public function rewrite(): string 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(); + $sql = $this->postgresTableSizeRewrite(); return $sql; } @@ -360,7 +360,7 @@ protected function convertToPostgresLimitSyntax($sql) } // This method is specifically to handle should_suggest_persistent_object_cache in wp site health - protected function postgresTableSizeRewrite($schema = 'public') + protected function postgresTableSizeRewrite($schema = 'public') { $sql = <<original(); - return $this->generatePostgresShowTableStatus(); + return $this->generatePostgresShowTableStatus(); } diff --git a/pg4wp/rewriters/ShowVariablesSQLRewriter.php b/pg4wp/rewriters/ShowVariablesSQLRewriter.php index 1eb45e8..8e944a5 100644 --- a/pg4wp/rewriters/ShowVariablesSQLRewriter.php +++ b/pg4wp/rewriters/ShowVariablesSQLRewriter.php @@ -38,7 +38,7 @@ public function generatePostgres($sql, $variableName) } 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 + // 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\";"; } diff --git a/tests/rewriteTest.php b/tests/rewriteTest.php index d9172f2..2bbeeaf 100644 --- a/tests/rewriteTest.php +++ b/tests/rewriteTest.php @@ -415,65 +415,36 @@ public function test_it_will_handle_found_rows_on_queries_with_order_by_case() $this->assertSame(trim($expected), trim($postgresql)); } - - public function test_it_can_handle_replacement_sql() + public function test_it_will_append_returning_id_to_insert_statements() { - $sql = "REPLACE INTO test2 (column1, column2, column3) VALUES (1, 'Old', '2014-08-20 18:47:00')"; - $expected = "INSERT INTO test2 (column1, column2, column3) VALUES (1, 'Old', '2014-08-20 18:47:00') ON CONFLICT (column1) DO UPDATE SET column2 = EXCLUDED.column2, column3 = EXCLUDED.column3"; + $sql = <<assertSame(trim($expected), trim($postgresql)); } - - public function test_it_can_handle_insert_sql_containing_nested_parathesis_with_numbers() + public function test_it_can_handle_replacement_sql() { - $sql = <<