diff --git a/pg4wp/core.php b/pg4wp/core.php new file mode 100644 index 0000000..1f62efc --- /dev/null +++ b/pg4wp/core.php @@ -0,0 +1,39 @@ + '// define( ', + 'class wpdb' => 'class wpdb2', + 'new wpdb' => 'new wpdb2', + 'instanceof mysqli_result' => 'instanceof \PgSql\Result', + 'instanceof mysqli' => 'instanceof \PgSql\Connection', + '$this->dbh->connect_errno' => 'wpsqli_connect_error()', + 'mysqli_' => 'wpsqli_', + 'is_resource' => 'wpsqli_is_resource', + ' '', + '?>' => '', +); + +eval(str_replace(array_keys($replaces), array_values($replaces), file_get_contents(ABSPATH . '/wp-includes/class-wpdb.php'))); + +// Create wpdb object if not already done +if (!isset($wpdb) && defined('DB_USER')) { + $wpdb = new wpdb2(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST); +} diff --git a/pg4wp/db.php b/pg4wp/db.php new file mode 100644 index 0000000..b5cca53 --- /dev/null +++ b/pg4wp/db.php @@ -0,0 +1,54 @@ +sslkey)) { + $GLOBALS['pg4wp_connstr'] .= ' sslkey=' . $connection->sslkey; + } + + if (!empty($connection->sslcert)) { + $GLOBALS['pg4wp_connstr'] .= ' sslcert=' . $connection->sslcert; + } + + if (!empty($connection->sslca)) { + $GLOBALS['pg4wp_connstr'] .= ' sslrootcert=' . $connection->sslca; + } + + if (!empty($connection->sslcapath)) { + $GLOBALS['pg4wp_connstr'] .= ' sslcapath=' . $connection->sslcapath; + } + + if (!empty($connection->sslcipher)) { + $GLOBALS['pg4wp_connstr'] .= ' sslcipher=' . $connection->sslcipher; + } + + // Must connect to a specific database unlike MySQL + $dbname = defined('DB_NAME') && DB_NAME ? DB_NAME : $database; + $pg_connstr = $GLOBALS['pg4wp_connstr'] . ' dbname=' . $dbname; + $GLOBALS['pg4wp_conn'] = $connection = pg_connect($pg_connstr); + + return $connection; +} + +/** +* Database Operations +*/ + +/** + * Selects the default database for database queries. + * + * This function is a wrapper for the pg_select_db function, which is used to change the default + * database for the connection. This is useful when performing multiple operations across different + * databases without having to establish a new connection for each one. If the function succeeds, + * it will return TRUE, indicating the database was successfully selected, or FALSE if it fails. + * + * @param PgSql\Connection $connection The pg connection resource. + * @param string $database The name of the database to select. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_select_db(&$connection, $database) +{ + $pg_connstr = $GLOBALS['pg4wp_connstr'] . ' dbname=' . $database; + $GLOBALS['pg4wp_conn'] = $connection = pg_connect($pg_connstr); + + // Return FALSE if connection failed + if(!$connection) { + return $connection; + } + + // Get and store PostgreSQL server version + $ver = pg_version($connection); + if(isset($ver['server'])) { + $GLOBALS['pg4wp_version'] = $ver['server']; + } + + // Clear the connection string unless this is a test connection + if(!defined('WP_INSTALLING') || !WP_INSTALLING) { + $GLOBALS['pg4wp_connstr'] = ''; + } + + // Execute any pre-defined SQL commands + if(!empty($GLOBALS['pg4wp_pre_sql'])) { + foreach($GLOBALS['pg4wp_pre_sql'] as $sql2run) { + wpsqli_query($sql2run); + } + } + + return $connection; +} + +/** + * Closes a previously opened database connection. + * + * This function is a wrapper for the pg_close function. + * // mysqli_close => pg_close (resource $connection): bool + * It's important to close connections when they are no longer needed to free up resources on both the web + * server and the PostgreSQL server. The function returns TRUE on success or FALSE on failure. + * + * @param PgSql\Connection $connection The pg connection resource to be closed. + * @return bool Returns TRUE on successful closure, FALSE on failure. + */ +function wpsqli_close(&$connection) +{ + // Closing a connection in PostgreSQL is straightforward. + return pg_close($connection); +} + +/** + * Used to establish secure connections using SSL. + * + * This function sets up variables on the fake pg connection class which are used when + * connecting to the postgres database with pg_connect + * + * @param PgSql\Connection $connection The pg connection resource. + * @param string $key The path to the key file. + * @param string $cert The path to the certificate file. + * @param string $ca The path to the certificate authority file. + * @param string $capath The pathname to a directory that contains trusted SSL CA certificates + * in PEM format. + * @param string $cipher A list of allowable ciphers to use for SSL encryption. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_ssl_set(&$connection, $key, $cert, $ca, $capath, $cipher) +{ + $connection->sslkey = $key; + $connection->sslcert = $cert; + $connection->sslca = $ca; + $connection->sslcapath = $capath; + $connection->sslcipher = $cipher; + return $connection; +} + +/** + * Returns the PostgreSQL client library version as a string. + * + * This function is used to retrieve the version of the client library that is used to compile the pg extension. The function + * does not require any parameters and can be called statically. It is helpful for debugging + * and ensuring that the PHP environment is using the correct version of the PostgreSQL client library, + * which can be important for compatibility and functionality reasons. + * + * @return string The PostgreSQL client library version. + */ +function wpsqli_get_client_info() +{ + // mysqli_get_client_info => No direct equivalent. + // Information can be derived from phpinfo() or phpversion(). + return '8.0.35'; // Just want to fool wordpress ... +} + +/** + * Retrieves the version of the PostgreSQL server. + * + * This function returns a string representing the version of the PostgreSQL server pointed to by the connection resource. This + * information can be used for a variety of purposes, such as conditional behavior for different + * PostgreSQL versions or simply for logging and monitoring. Understanding the server version is + * essential for ensuring compatibility with specific PostgreSQL features and syntax. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return string The version of the PostgreSQL server. + */ +function wpsqli_get_server_info(&$connection) +{ + // mysqli_get_server_info => pg_version (resource $connection): array + // This function retrieves an array that includes server version. + // pg_version($connection); + return '8.0.35'; // Just want to fool wordpress ... +} + +/** + * Returns a string representing the type of connection used. + * + * This function is a wrapper for the pg_get_host_info function. It retrieves information about + * the type of connection that was established to the PostgreSQL server and the host server information. + * This includes the host name and the connection type, such as TCP/IP or a UNIX socket. It's useful + * for debugging and for understanding how PHP is communicating with the PostgreSQL server. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return string A string describing the connection type and server host information. + */ +function wpsqli_host_info(&$connection) +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_get_host_info => No direct equivalent. Host information is part of the connection string in PostgreSQL. +} + +/** + * Pings a server connection, or tries to reconnect if the connection has gone down. + * + * This function is a wrapper for the pg_ping function, which checks whether the + * connection to the server is working. If it has gone down, and the global option + * pg.reconnect is enabled, it will attempt to reconnect. This is useful to ensure + * that a connection is still alive and if not, to re-establish it before proceeding + * with further operations. It returns TRUE if the connection is alive or if it was + * successfully re-established, and FALSE if the connection is not established and + * cannot be re-established. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_ping(&$connection) +{ + return pg_ping($connection); +} + +/** + * Returns the thread ID for the current connection. + * + * This function is a wrapper for the pg_thread_id function. It retrieves the thread ID used by + * the current connection to the PostgreSQL server. This ID can be used as an argument to the KILL + * statement to terminate a connection. It is useful for debugging and managing PostgreSQL connections + * and can be used to uniquely identify the connection within the server's process. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return int The thread ID for the current connection. + */ +function wpsqli_thread_id(&$connection) +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_thread_id => No direct equivalent. PostgreSQL does not provide thread ID in the same manner as MySQL. +} + +/** + * Returns whether the client library is thread-safe. + * + * This function is a wrapper for the pg_thread_safe function. It indicates whether the + * pg client library that PHP is using is thread-safe. This is important information when + * running PHP in a multi-threaded environment such as with the worker MPM in Apache or when + * using multi-threading extensions in PHP. + * + * @return bool Returns TRUE if the client library is thread-safe, FALSE otherwise. + */ +function wpsqli_thread_safe() +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_thread_safe => No direct equivalent. PostgreSQL's thread safety is dependent on PHP's thread safety. +} + +/** + * Gets the current system status of the PostgreSQL server. + * + * This function is a wrapper for the pg_stat function. It returns a string containing + * status information about the PostgreSQL server to which it's connected. The information includes + * uptime, threads, queries, open tables, and flush tables, among other status indicators. + * This can be useful for monitoring the health and performance of the PostgreSQL server, as well + * as for debugging purposes. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return string A string describing the server status or FALSE on failure. + */ +function wpsqli_stat(&$connection) +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_stat => No direct equivalent +} + +/** + * Sets extra connect options and affect behavior for a connection. + * + * This function is a wrapper for the pg_options function. It is used to set extra options + * for a connection resource before establishing a connection using pg_real_connect(). These + * options can be used to control various aspects of the connection's behavior. The function should + * be called after pg_init() and before pg_real_connect(). It returns TRUE on success or + * FALSE on failure. + * + * @param PgSql\Connection $connection The pg connection resource. + * @param int $option The specific option that is to be set. + * @param mixed $value The value for the specified option. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_options(&$connection, $option, $value) +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_options => No direct equivalent. Options are set in the connection string or via set_config in PostgreSQL. +} + +/** + * Returns the error code from the last connection attempt. + * + * This function is a wrapper for the pg_connect_errno function. It returns the error code from + * the last call to pg_connect() or pg_real_connect(). It is useful for error handling after + * attempting to establish a connection to a PostgreSQL server, allowing the script to respond appropriately + * to specific error conditions. The function does not take any parameters and returns an integer error + * code. If no error occurred during the last connection attempt, it will return zero. + * + * @return int The error code from the last connection attempt. + */ +function wpsqli_connect_errno() +{ + throw new \Exception("PG4WP: Not Yet Implemented"); +} + +/** + * Returns a string description of the last connect error. + * + * This function is a wrapper for the pg_connect_error function. It provides a textual description + * of the error from the last connection attempt made by pg_connect() or pg_real_connect(). + * Unlike pg_connect_errno(), which returns an error code, pg_connect_error() returns a string + * describing the error. This is useful for error handling, providing more detailed context about + * connection problems. + * + * @return string|null A string that describes the error from the last connection attempt, or NULL + * if no error occurred. + */ +function wpsqli_connect_error() +{ + if($GLOBALS['pg4wp_conn']) { + return pg_last_error($GLOBALS['pg4wp_conn']); + } + + return ''; +} + +/** +* Transaction Handling +*/ + +/** + * Turns on or off auto-commit mode on queries for the database connection. + * + * This function is a wrapper for the pg_autocommit function. When turned on, each query + * that you execute will automatically commit to the database. When turned off, you will need to + * manually commit transactions using pg_commit() or rollback using pg_rollback(). This + * function is particularly useful for transactions that require multiple steps and you don't want + * to commit until all steps are successful. + * + * @param PgSql\Connection $connection The pg connection resource. + * @param bool $mode Whether to turn on auto-commit mode or not. Pass TRUE to turn on auto-commit + * mode and FALSE to turn it off. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_autocommit(&$connection, $mode) +{ + // mysqli_autocommit => pg_autocommit (resource $connection, bool $mode): bool + // PostgreSQL autocommit behavior is typically managed at the transaction level. + pg_query($connection, "SET AUTOCOMMIT TO ON"); +} + +/** + * Starts a new transaction. + * + * This function is a wrapper for the pg_begin_transaction function. It starts a new transaction + * with the provided connection and with the specified flags. Transactions allow multiple changes to + * be made to the database atomically - they will all be applied, or none will be, which can be controlled + * by committing or rolling back the transaction. This function can also set a name for the transaction, + * which can be used for savepoints. + * + * @param PgSql\Connection $connection The pg connection resource. + * @param int $flags Optional flags for defining transaction characteristics. This should be a bitmask + * of any of the pg_TRANS_START_* constants. + * @param string|null $name Optional name for the transaction, used for savepoint names. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_begin_transaction(&$connection, $flags = 0, $name = null) +{ + // mysqli_begin_transaction => pg_query (resource $connection, string $query): resource + // PostgreSQL uses standard BEGIN or START TRANSACTION queries. + pg_query($connection, "BEGIN"); +} + +/** + * Commits the current transaction. + * + * This function is a wrapper for the pg_commit function. It is used to commit the current transaction + * for the database connection. Committing a transaction means that all the operations performed since the + * start of the transaction are permanently saved to the database. This function can also take optional flags + * and a name, the latter being used if the commit should be associated with a named savepoint. + * + * @param PgSql\Connection $connection The pg connection resource. + * @param int $flags Optional flags for the commit operation. It should be a bitmask of the pg_TRANS_COR_* constants. + * @param string|null $name Optional name for the savepoint that should be committed. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_commit(&$connection, $flags = 0, $name = null) +{ + // mysqli_commit => pg_query (resource $connection, string $query): resource + // Commits are standard SQL in PostgreSQL. + pg_query($connection, "COMMIT"); +} + +/** + * Rolls back the current transaction for the database connection. + * + * This function is a wrapper for the pg_rollback function. It rolls back the current transaction, + * undoing all changes made to the database in the current transaction. This is an essential feature + * for maintaining data integrity, especially in situations where a series of database operations need + * to be treated as an atomic unit. The function can also accept optional flags and a name, which can be + * used to rollback to a named savepoint within the transaction rather than rolling back the entire transaction. + * + * @param PgSql\Connection $connection The pg connection resource. + * @param int $flags Optional flags that define how the rollback operation should be handled. It should be + * a bitmask of the pg_TRANS_COR_* constants. + * @param string|null $name Optional name of the savepoint to which the rollback operation should be directed. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_rollback(&$connection, $flags = 0, $name = null) +{ + // mysqli_rollback => pg_query (resource $connection, string $query): resource + // Rollbacks are standard SQL in PostgreSQL. + pg_query($connection, "ROLLBACK"); +} + +function get_primary_key_for_table(&$connection, $table) +{ + $query = << $err\n---------------------\n", 3, PG4WP_LOG . 'pg4wp_errors.log'); + } + } + + $GLOBALS['pg4wp_conn'] = $connection; + $GLOBALS['pg4wp_result'] = $result; + + if (false !== strpos($sql, "INSERT INTO")) { + $matches = array(); + preg_match("/^INSERT INTO\s+`?([a-z0-9_]+)`?/i", $query, $matches); + $tableName = $matches[1]; + + if (false !== strpos($sql, "RETURNING")) { + $primaryKey = get_primary_key_for_table($connection, $tableName); + $row = pg_fetch_assoc($result); + + $GLOBALS['pg4wp_ins_id'] = $row[$primaryKey]; + } + } + + return $result; +} + +/** + * Executes one or multiple queries which are concatenated by a semicolon. + * + * This function is a wrapper for the pg_multi_query function. It allows execution of + * multiple SQL statements sent to the PostgreSQL server in a single call. This can be useful to + * perform a batch of SQL operations such as an atomic transaction that should either complete + * entirely or not at all. After calling this function, the results of the queries can be + * processed using pg_store_result() and pg_next_result(). It is important to ensure + * that any user input included in the queries is properly sanitized to avoid SQL injection. + * + * @param PgSql\Connection $connection The pg connection resource. + * @param string $query The queries to execute, concatenated by semicolons. + * @return bool Returns TRUE on success or FALSE on the first error that occurred. + * If the first query succeeds, the function will return TRUE even if + * a subsequent query fails. + */ +function wpsqli_multi_query(&$connection, $query) +{ + // Store the initial SQL query + $initial = $query; + // Rewrite the SQL query for compatibility with Postgres + $sql = pg4wp_rewrite($query); + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_multi_query => No direct equivalent. Multiple queries must be executed separately in PostgreSQL. +} + +/** + * Prepares an SQL statement for execution. + * + * This function is a wrapper for the pg_prepare function. It prepares the SQL statement + * and returns a statement object used for further operations on the statement. The statement + * preparation is used to efficiently execute repeated queries with high efficiency and to avoid + * SQL injection vulnerabilities by separating the query structure from its data. It is especially + * useful when the same statement is executed multiple times with different parameters. + * + * @param PgSql\Connection $connection The pg connection resource. + * @param string $query The SQL query to prepare. + * @return class|false Returns a statement object on success or FALSE on failure. + */ +function wpsqli_prepare(&$connection, $query) +{ + // Store the initial SQL query + $initial = $query; + // Rewrite the SQL query for compatibility with Postgres + $sql = pg4wp_rewrite($query); + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_prepare => pg_prepare (resource $connection, string $stmtname, string $query): resource + // pg_prepare($connection, "my_query", "SELECT * FROM my_table WHERE id = $1"); +} + +/** + * Executes a prepared Query. + * + * This function is a wrapper for the pg_stmt_execute function. It is used to execute a statement + * that was previously prepared using the pg_prepare function. The execution will take place with + * the current bound parameters in the statement object. This is commonly used in database operations + * to execute the same statement repeatedly with high efficiency and to mitigate the risk of SQL injection + * by separating SQL logic from the data being input. + * + * @param pg_stmt $stmt The pg_stmt statement object. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_stmt_execute($stmt) +{ + // Store the initial SQL query + $initial = $stmt; + // Rewrite the SQL query for compatibility with Postgres + $sql = pg4wp_rewrite($stmt); + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_stmt_execute => pg_execute (resource $connection, string $stmtname, array $params): resource + // Executes a previously prepared statement. + // pg_execute($connection, "my_query", array("my_id")); +} + +/** + * Binds variables to a prepared statement as parameters. + * + * This function is a wrapper for the pg_stmt_bind_param function. It binds variables to the + * placeholders of a prepared statement, which is represented by the `$stmt` parameter. The `$types` + * parameter is a string that contains one character for each variable in `$vars`, indicating the type + * of the variable. The supported types are 'i' for integer, 'd' for double, 's' for string, and 'b' for + * blob. By using this function, the values of the variables are bound to the statement as it is executed, + * which can be used to safely execute the statement with user-supplied input. + * + * @param pg_stmt $stmt The prepared statement to which the variables are bound. + * @param string $types A string that contains a type specification char for each variable in `$vars`. + * @param mixed ...$vars The variables to bind to the prepared statement. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_stmt_bind_param($stmt, $types, ...$vars) +{ + // Store the initial SQL query + $initial = $stmt; + // Rewrite the SQL query for compatibility with Postgres + $sql = pg4wp_rewrite($stmt); + throw new \Exception("PG4WP: Not Yet Implemented"); + // The remaining mysqli_stmt_* functions do not have direct equivalents in PostgreSQL. Prepared statements work differently. + // PostgreSQL uses pg_prepare() and pg_execute() for prepared statements. Results are then fetched with pg_fetch_* functions. +} + +/** + * Binds variables to a prepared statement for result storage. + * + * This function is a wrapper for the pg_stmt_bind_result function. It binds variables to the + * prepared statement `$stmt` to store the result of the statement once it is executed. The bound + * variables are passed by reference and will be set to the values of the corresponding columns in + * the result set. This function is typically used in conjunction with pg_stmt_fetch(), which + * will populate the variables with data from the next row in the result set each time it is called. + * + * @param pg_stmt $stmt The statement object that executed a query with a result set. + * @param mixed &...$vars The variables to which the result set columns will be bound. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_stmt_bind_result($stmt, &...$vars) +{ + // Store the initial SQL query + $initial = $stmt; + // Rewrite the SQL query for compatibility with Postgres + $sql = pg4wp_rewrite($stmt); + throw new \Exception("PG4WP: Not Yet Implemented"); + // The remaining mysqli_stmt_* functions do not have direct equivalents in PostgreSQL. Prepared statements work differently. + // PostgreSQL uses pg_prepare() and pg_execute() for prepared statements. Results are then fetched with pg_fetch_* functions. +} + +/** + * Fetches results from a prepared statement into the bound variables. + * + * This function is a wrapper for the pg_stmt_fetch function. It is used to fetch the data + * from the executed prepared statement into the variables that were bound using pg_stmt_bind_result(). + * The function will return TRUE for every row fetched successfully. When there are no more rows to fetch, + * it will return NULL, and if there is an error it will return FALSE. + * + * @param pg_stmt $stmt The prepared statement object from which results are to be fetched. + * @return bool|null Returns TRUE on success, NULL if there are no more rows to fetch, or FALSE on error. + */ +function wpsqli_stmt_fetch($stmt) +{ + // Store the initial SQL query + $initial = $query; + // Rewrite the SQL query for compatibility with Postgres + $sql = pg4wp_rewrite($query); + throw new \Exception("PG4WP: Not Yet Implemented"); + // The remaining mysqli_stmt_* functions do not have direct equivalents in PostgreSQL. Prepared statements work differently. + // PostgreSQL uses pg_prepare() and pg_execute() for prepared statements. Results are then fetched with pg_fetch_* functions. +} + +/** + * Initializes a statement and returns an object for use with pg_stmt_prepare. + * + * This function is a wrapper for the pg_stmt_init function. It creates and returns a new statement + * object associated with the specified database connection. This statement object can then be used + * to prepare a SQL statement for execution. It's particularly useful when you need to execute a + * prepared statement multiple times with different parameters, providing benefits such as improved + * query performance and protection against SQL injection attacks. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return class A new statement object or FALSE on failure. + */ +function wpsqli_stmt_init(&$connection) +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_stmt_init => No direct equivalent in PostgreSQL. + // In PostgreSQL, prepared statements are created directly with pg_prepare, not initialized separately. +} + + +/** + * Closes a prepared statement. + * + * This function is a wrapper for the pg_stmt_close function. It deallocates the statement + * and cleans up the memory associated with the statement object. This is an important step in + * resource management, as it frees up server resources and allows other statements to be executed. + * It should always be called after all the results have been fetched and the statement is no longer needed. + * + * @param pg_stmt $stmt The prepared statement object to be closed. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_stmt_close($stmt) +{ + // Store the initial SQL query + $initial = $stmt; + // Rewrite the SQL query for compatibility with Postgres + $sql = pg4wp_rewrite($stmt); + throw new \Exception("PG4WP: Not Yet Implemented"); + // The remaining mysqli_stmt_* functions do not have direct equivalents in PostgreSQL. Prepared statements work differently. + // PostgreSQL uses pg_prepare() and pg_execute() for prepared statements. Results are then fetched with pg_fetch_* functions. +} + +/** + * Returns a string description for the last statement error. + * + * This function is a wrapper for the pg_stmt_error function. It returns a string describing + * the error for the most recent statement operation that generated an error. This is useful for + * debugging and error handling in applications that use prepared statements to interact with the + * PostgreSQL database. It allows developers to output or log a descriptive error message when a PostgreSQL + * operation on a prepared statement fails. + * + * @param pg_stmt $stmt The pg_stmt statement object. + * @return string A string that describes the error. An empty string if no error occurred. + */ +function wpsqli_stmt_error($stmt) +{ + // Store the initial SQL query + $initial = $stmt; + // Rewrite the SQL query for compatibility with Postgres + $sql = pg4wp_rewrite($stmt); + throw new \Exception("PG4WP: Not Yet Implemented"); + // The remaining mysqli_stmt_* functions do not have direct equivalents in PostgreSQL. Prepared statements work differently. + // PostgreSQL uses pg_prepare() and pg_execute() for prepared statements. Results are then fetched with pg_fetch_* functions. +} + +/** + * Returns the error code for the most recent statement call. + * + * This function is a wrapper for the pg_stmt_errno function. It returns the error code from + * the last operation performed on the specified statement. This is useful for error handling, + * particularly in database operations where you need to react differently based on the specific + * error that occurred. It can be used in conjunction with pg_stmt_error() to retrieve both + * the error code and the error message for more detailed debugging and logging. + * + * @param pg_stmt $stmt The pg_stmt statement object. + * @return int An error code value for the last error that occurred, or zero if no error occurred. + */ +function wpsqli_stmt_errno($stmt) +{ + // Store the initial SQL query + $initial = $stmt; + // Rewrite the SQL query for compatibility with Postgres + $sql = pg4wp_rewrite($stmt); + throw new \Exception("PG4WP: Not Yet Implemented"); + // The remaining mysqli_stmt_* functions do not have direct equivalents in PostgreSQL. Prepared statements work differently. + // PostgreSQL uses pg_prepare() and pg_execute() for prepared statements. Results are then fetched with pg_fetch_* functions. +} + +/** +* Result Handling +*/ + +/** + * Fetches a result row as an associative, a numeric array, or both. + * + * This function is a wrapper for the pg_fetch_array function, which is used to fetch a single + * row of data from the result set obtained from executing a SELECT query. The data can be fetched + * as an associative array, a numeric array, or both, depending on the `mode` specified. By default, + * it fetches as both associative and numeric (PGSQL_BOTH). Using PGSQL_ASSOC will fetch as an + * associative array, and PGSQL_NUM will fetch as a numeric array. It returns NULL when there are + * no more rows to fetch. + * + * This function calls the pg_fetch_array() function to fetch the next row from a Postgres result set + * and trims any leading or trailing whitespace from each of the values. + * + * @param resource $result The result resource returned by a Postgres query. + * @param int $mode The type of array that should be produced from the current row data. + * @return array|bool An array of the next row in the result set, or false if there are no more rows. + */ +function wpsqli_fetch_array($result, $mode = PGSQL_BOTH) +{ + // Fetch the next row as an array + $res = pg_fetch_array($result, null, $mode); + + // Check if the result is an array and trim its values + if (is_array($res)) { + foreach ($res as $v => $k) { + $res[$v] = trim($k); + } + } + + // Return the trimmed array or false if there are no more rows + return $res; +} + +/** + * Fetches a result row as an object. + * + * This function is a wrapper for the pg_fetch_object function. It retrieves the current row + * of a result set as an object where the attributes correspond to the fetched row's column names. + * This function can instantiate an object of a specified class, and pass parameters to its constructor, + * allowing for custom objects based on the rows of the result set. If no class is specified, it defaults + * to a stdClass object. If the class does not exist or the specified class's constructor requires more + * arguments than are given, an exception is thrown. + * + * @param \PgSql\Result $result The result set returned by pg_query, pg_store_result + * or pg_use_result. + * @param string $class The name of the class to instantiate, set the properties of which + * correspond to the fetched row's column names. + * @param array $constructor_args An optional array of parameters to pass to the constructor + * for the class name defined by the class parameter. + * @return object|false An instance of the specified class with property names that correspond + * to the column names returned in the result set, or FALSE on failure. + */ +function wpsqli_fetch_object($result, $class = "stdClass", $constructor_args = []) +{ + return pg_fetch_object($result, null, $class, $constructor_args); +} + +/** + * Fetches one row of data from the result set and returns it as an enumerated array. + * Each call to this function will retrieve the next row in the result set, so it's typically + * used in a loop to process multiple rows. + * + * This function is particularly useful when you need to retrieve a row as a simple array + * where each column is accessed by an integer index starting at 0. It does not include + * column names as keys, which can be marginally faster and less memory intensive than + * associative arrays if the column names are not required. + * + * @param \PgSql\Result $result The result set returned by a query against the database. + * + * @return array|null Returns an enumerated array of strings representing the fetched row, + * or NULL if there are no more rows in the result set. + */ +function wpsqli_fetch_row($result): ?array +{ + return pg_fetch_row($result); +} + +/** + * Adjusts the result pointer to an arbitrary row in the result set represented by the + * $result object. This function can be used in conjunction with pg_fetch_row(), + * pg_fetch_assoc(), pg_fetch_array(), or pg_fetch_object() to navigate between + * rows in result sets, especially when using buffered result sets. + * + * This is an important function for situations where you need to access a specific row + * directly without iterating over all preceding rows, which can be useful for pagination + * or when looking up specific rows by row number. + * + * @param \PgSql\Result $result The result set returned by a query against the database. + * @param int $row_number The desired row number to seek to. Row numbers are zero-indexed. + * + * @return bool Returns TRUE on success or FALSE on failure. If the row number is out of range, + * it returns FALSE. + */ +function wpsqli_data_seek($result, int $row_number): bool +{ + return pg_result_seek($result, $row_number); +} + +/** + * Returns the next field in the result set. + * + * This function is a wrapper for the pg_fetch_field function, which retrieves information about + * the next field in the result set represented by the `$result` parameter. It can be used in a loop + * to obtain information about each field in the result set, such as name, table, max length, flags, + * and type. This is useful for dynamically generating table structures or processing query results + * when the structure of the result set is not known in advance or changes. + * + * @param \PgSql\Result $result The result set returned by pg_query, pg_store_result + * or pg_use_result. + * @return object An object which contains field definition information or FALSE if no field information + * is available. + */ +function wpsqli_fetch_field($result) +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_fetch_field => pg_field_table (resource $result, int $field_number, bool $oid_only = false): mixed + // Returns the name or oid of the table of the field. There's no direct function to mimic mysqli_fetch_field completely. + //pg_field_table($result, $field_number); +} + +/** + * Gets the number of fields in a result set. + * + * This function is a wrapper for the pg_num_fields function. It returns the number + * of fields (columns) in a result set. This is particularly useful when you need to + * dynamically process a query result without knowing the schema of the returned data, + * as it allows the script to iterate over all fields in each row of the result set. + * + * @param \PgSql\Result $result The result set returned by pg_query, pg_store_result + * or pg_use_result. + * @return int The number of fields in the specified result set. + */ +function wpsqli_num_fields($result) +{ + // mysqli_num_fields => pg_num_fields (resource $result): int + // Returns the number of fields (columns) in a result. + return pg_num_fields($result); +} + +/** + * Returns the number of columns for the most recent query on the connection. + * + * This function is a wrapper for the pg_field_count function. It retrieves the number of + * columns obtained from the most recent query executed on the given database connection. This + * can be particularly useful when you need to know how many columns will be returned by a + * SELECT statement before fetching data, which can help in dynamically processing result sets. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return int An integer representing the number of fields in the result set. + */ +function wpsqli_field_count(&$connection) +{ + // mysqli_field_count => pg_num_fields (resource $result): int + // Use pg_num_fields to get the number of fields (columns) in a result. + return pg_num_fields($result); +} + +/** + * Transfers a result set from the last query. + * + * This function is a wrapper for the pg_store_result function. It is used to transfer the + * result set from the last query executed on the given connection, which used the pg_STORE_RESULT + * flag. This function must be called after executing a query that returns a result set (like SELECT). + * It allows the complete result set to be transferred to the client and then utilized via the + * pg_fetch_* functions. It's particularly useful when the result set is expected to be accessed + * multiple times. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return \PgSql\Result|false A buffered result object or FALSE if an error occurred. + */ +function wpsqli_store_result(&$connection) +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_store_result => Not needed in PostgreSQL. + // PostgreSQL's pg_query automatically stores the results without a separate call. + //return true; +} + +/** + * Initiates the retrieval of a result set from the last query executed using the pg_USE_RESULT mode. + * + * This function is a wrapper for the pg_use_result function. It is used to initiate the retrieval + * of a result set from the last query executed on the given connection, without storing the entire result + * set in the buffer. This is particularly useful for handling large result sets that could potentially + * exceed the available PHP memory. The data is fetched row-by-row, reducing the immediate memory footprint. + * However, it requires the connection to remain open, and no other operations can be performed on the + * connection until the result set is fully processed. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return \PgSql\Result|false An unbuffered result object or FALSE if an error occurred. + */ +function wpsqli_use_result(&$connection) +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_use_result => Not needed in PostgreSQL. + // PostgreSQL does not differentiate between unbuffered and buffered queries like MySQL does. +} + +/** + * Frees the memory associated with a result. + * + * This function is a wrapper for the pg_free_result function. It's used to free the memory + * allocated for a result set obtained from a query. When the result data is not needed anymore, + * it's a good practice to free the associated resources, especially when dealing with large + * datasets that can consume significant amounts of memory. It is an important aspect of resource + * management and helps to keep the application's memory footprint minimal. + * + * @param \PgSql\Result $result The result set returned by pg_query, pg_store_result + * or pg_use_result. + * @return void This function doesn't return any value. + */ +function wpsqli_free_result($result) +{ + // mysqli_free_result => pg_free_result (resource $result): bool + // Frees memory associated with a result. + return pg_free_result($result); +} + +/** + * Checks if there are any more result sets from a multi query. + * + * mysqli_more_results => No direct equivalent in PostgreSQL. + * PostgreSQL does not support multiple results like MySQL's multi_query function. + * It returns TRUE if one or more result sets are available from the previous calls to + * pg_multi_query(), otherwise FALSE. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return bool Returns TRUE if there are more result sets from previous multi queries and + * FALSE otherwise. + */ +function wpsqli_more_results(&$connection) +{ + // mysqli_more_results => No direct equivalent in PostgreSQL. + // PostgreSQL does not have a built-in function to check for more results from a batch of queries. + return false; +} + +/** + * Moves the internal result pointer to the next result set returned from a multi query. + * + * mysqli_next_result => No direct equivalent in PostgreSQL. + * PostgreSQL does not support multiple results like MySQL's multi_query function. + * FALSE if there are no more result sets, or FALSE with an error if there is a problem moving the result pointer. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return bool Returns TRUE on success or FALSE on failure (no more results or an error occurred). + */ +function wpsqli_next_result(&$connection) +{ + // mysqli_next_result => No direct equivalent in PostgreSQL. + // PostgreSQL does not support multiple results like MySQL's multi_query function. + return false; +} + +/** +* Utility Functions +*/ + +function wpsqli_is_resource($object) +{ + return $object !== false && $object !== null; +} + +/** + * Gets the number of affected rows in the previous PostgreSQL operation. + * + * This function is a wrapper for the pg_affected_rows function, which is used to determine + * the number of rows affected by the last INSERT, UPDATE, REPLACE or DELETE query executed on + * the given connection. It is an important function for understanding the impact of such queries, + * allowing the developer to verify that the expected number of rows were altered. It returns an + * integer indicating the number of rows affected or -1 if the last query failed. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return int The number of affected rows in the previous operation, or -1 if the last operation failed. + */ +function wpsqli_affected_rows(&$connection) +{ + $result = $GLOBALS['pg4wp_result']; + // mysqli_affected_rows => pg_affected_rows (resource $result): int + // Returns the number of rows affected by INSERT, UPDATE, or DELETE query. + return pg_affected_rows($result); +} + +// Gets the list of sequences from postgres +function wpsqli_get_list_of_sequences(&$connection) +{ + $sql = "SELECT sequencename FROM pg_sequences"; + $result = pg_query($connection, $sql); + if(!$result) { + if (PG4WP_DEBUG || PG4WP_LOG) { + $log = "Unable to get list of sequences\n"; + error_log($log, 3, PG4WP_LOG . 'pg4wp_errors.log'); + } + return []; + } + + $data = pg_fetch_all($result); + return array_column($data, 'sequencename'); +} + +// Get the primary sequence for a table +function wpsqli_get_primary_sequence_for_table(&$connection, $table) +{ + // TODO: it should be possible to use a WP transient here for object caching + global $sequence_lookup; + if (empty($sequence_lookup)) { + $sequence_lookup = []; + } + + if (isset($sequence_lookup[$table])) { + return $sequence_lookup[$table]; + } + + $sequences = wpsqli_get_list_of_sequences($connection); + foreach($sequences as $sequence) { + if (strncmp($sequence, $table, strlen($table)) === 0) { + $sequence_lookup[$table] = $sequence; + return $sequence; + } + } + + // we didn't find a sequence for this table. + return null; +} + +/** + * Fetches the ID generated for an AUTO_INCREMENT column by the previous INSERT query. + * + * @param resource|null $connection A PostgreSQL connection resource. Default is `null`. + * + * @return mixed The ID generated for an AUTO_INCREMENT column by the previous INSERT query on success; `false` on failure. + * + * Note: + * 1. In PostgreSQL, this function uses CURRVAL() on the appropriate sequence to get the last inserted ID. + * 2. In MySQL, last inserted ID is generally fetched using mysql_insert_id() or mysqli_insert_id(). + */ +function wpsqli_insert_id(&$connection = null) +{ + global $wpdb; + $data = null; + $ins_field = $GLOBALS['pg4wp_ins_field']; + $table = $GLOBALS['pg4wp_ins_table']; + + if($GLOBALS['pg4wp_ins_id']) { + return $GLOBALS['pg4wp_ins_id']; + } elseif(empty($sql)) { + $sql = 'NO QUERY'; + $data = 0; + } else { + $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); + if (false !== $res) { + $data = pg_fetch_result($res, 0, 0); + } elseif (PG4WP_DEBUG || PG4WP_LOG) { + $log = '[' . microtime(true) . "] wpsqli_insert_id() was called with '$table' and '$ins_field'" . + " and returned the error:\n" . pg_last_error($connection) . + "\nFor the query:\n" . $sql . + "\nThe latest INSERT query was :\n'$lastq'\n"; + error_log($log, 3, PG4WP_LOG . 'pg4wp_errors.log'); + } + } + + if (PG4WP_DEBUG && $sql) { + error_log('[' . microtime(true) . "] Getting inserted ID for '$table' ('$ins_field') : $sql => $data\n", 3, PG4WP_LOG . 'pg4wp_insertid.log'); + } + + $GLOBALS['pg4wp_conn'] = $connection; + + return $data; +} + + + +/** + * Sets the default character set to be used when sending data from and to the database server. + * + * This function is a wrapper for the pg_set_charset function. The pg_set_charset function + * is used to set the character set to be used when sending data from and to the database server. + * This is particularly important to ensure that data is properly encoded and decoded when stored + * and retrieved from the database, avoiding character encoding issues. + * + * @param PgSql\Connection $connection The pg connection resource. + * @param string $charset The desired character set. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_set_charset(&$connection, $charset) +{ + // mysqli_set_charset => pg_set_client_encoding (resource $connection, string $encoding): int + // Sets the client encoding. + return pg_set_client_encoding($connection, "UTF8"); +} + +/** + * Escapes special characters in a string for use in an SQL statement. + * + * This function serves as a wrapper for the pg_real_escape_string function, which is + * utilized to escape potentially dangerous special characters within a string. This is a + * critical security measure to prevent SQL injection vulnerabilities by ensuring that user + * input can be safely used in an SQL query. The function takes a string to be escaped and + * the pg connection resource, and returns the escaped string which is safe to be included + * in SQL statements. + * + * @param PgSql\Connection $connection The pg connection resource. + * @param string $string The string to be escaped. + * @return string Returns the escaped string. + */ +function wpsqli_real_escape_string(&$connection, $string) +{ + // mysqli_real_escape_string => pg_escape_string (resource $connection, string $data): string + // Escapes a string for safe use in database queries. + return pg_escape_string($connection, $string); +} + +/** + * Retrieves the last error description for the most recent pg function call + * that can succeed or fail. + * + * This function is a wrapper for the pg_last_error function, which returns a string + * describing the error from the last PostgreSQL operation associated with the provided + * connection resource. It's an essential function for debugging and error handling + * in PostgreSQL-related operations. When a pg function fails, wpsqli_error can be + * used to fetch the corresponding error message to understand what went wrong. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return string Returns a string with the error message for the most recent function call + * if it has failed, or an empty string if no error has occurred. + */ +function wpsqli_error(&$connection) +{ + return pg_last_error($connection); +} + +/** + * Returns the SQLSTATE error code for the last query executed on the connection. + * + * @param resource $connection The Postgres database connection resource. + * + * @return string|false SQLSTATE error code or false if no error. + * + * Note: + * 1. This function uses `pg_get_result` to get the result resource of the last query. + * 2. `pg_result_status` returns the status of the result. + * 3. `pg_result_error_field` is used to get the SQLSTATE error code. + * 4. In MySQL, you could use `mysqli_errno` to get the error code directly. + */ +function wpsqli_errno(&$connection) +{ + $result = pg_get_result($connection); + if ($result === false) { + return false; + } + + $result_status = pg_result_status($result); + return pg_result_error_field($result_status, PGSQL_DIAG_SQLSTATE); +} + +/** + * Enables or disables internal report functions. + * + * This function is a wrapper for the pg_report function, which is used to set the + * reporting mode of pg errors. This is useful for defining whether errors should be + * reported as exceptions, warnings, or silent (no report). It's important for configuring + * the error reporting behavior of the pg extension to suit the needs of your application, + * particularly in a development environment where more verbose error reporting is beneficial. + * + * @param int $flags A bit-mask constructed from the pg_REPORT_* constants. + * @return bool Returns TRUE on success or FALSE on failure. + */ +function wpsqli_report($flags) +{ + // mysqli_report => No direct equivalent in PostgreSQL. + // MySQL's mysqli_report function is used to set what MySQLi should report, which doesn't have a direct equivalent in PostgreSQL's PHP functions. + return true; +} + +/** + * Retrieves information about the most recently executed query. + * + * This function is a wrapper for the pg_info function. It provides a string containing + * information about the most recently executed query on the given connection resource. This + * can include information such as the number of rows affected by an INSERT, UPDATE, REPLACE, + * or DELETE query, as well as the number of rows matched and changed. It is valuable for + * obtaining detailed insights into the execution of database operations. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return string|null A string representing information about the last query executed, + * or NULL if no information is available. + */ +function wpsqli_info(&$connection) +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_info => No direct equivalent in PostgreSQL. + // This function retrieves information about the most recently executed query, which is not provided by PostgreSQL's PHP functions. +} + + +/** + * Polls connections for results. + * + * This function is a wrapper for the pg_poll function. It can be used to poll multiple + * connections to check if one or more of the connections have results available for client-side + * processing. It is useful when you have multiple asynchronous queries running and need to handle + * them as soon as their results become available. The function takes variable references for read, + * error, and reject arrays, and modifies them to indicate which connections have results, which + * have errors, and which were rejected respectively. + * + * @param array &$read Array of connections to check for outstanding results that can be read. + * @param array &$error Array of connections on which an error occurred. + * @param array &$reject Array of connections rejected because no asynchronous query + * has been run on them. + * @param int $sec Number of seconds to wait, must be non-negative. + * @param int $usec Number of microseconds to wait, must be non-negative. + * @return int|false Number of ready connections upon success, FALSE otherwise. + */ +function wpsqli_poll(&...$args) +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_poll => No direct equivalent in PostgreSQL. + // Polling for result availability is not a concept that is directly exposed in PostgreSQL's PHP functions. + // Asynchronous query handling in PHP with PostgreSQL typically involves using separate processes or coroutines. +} + +/** + * Gets the result from asynchronous PostgreSQL query. + * + * This function is a wrapper for the pg_reap_async_query function. It is used after initiating + * a query with pg_query() on a connection with the pg_ASYNC flag set. It retrieves the result + * from the query once it is complete, which can be used with pg_poll() to manage multiple + * asynchronous queries. It returns a pg_result object for successful SELECT queries, or TRUE for + * other DML queries (INSERT, UPDATE, DELETE, etc.) if the operation was successful, or FALSE on failure. + * + * @param PgSql\Connection $connection The pg connection resource. + * @return \PgSql\Result|bool A pg_result object for successful SELECT queries, TRUE for other + * successful DML queries, or FALSE on failure. + */ +function wpsqli_reap_async_query(&$connection) +{ + throw new \Exception("PG4WP: Not Yet Implemented"); + // mysqli_reap_async_query => No direct equivalent in PostgreSQL. + // Asynchronous queries can be executed in PostgreSQL using pg_send_query and retrieved using pg_get_result. +} diff --git a/pg4wp/driver_pgsql_rewrite.php b/pg4wp/driver_pgsql_rewrite.php new file mode 100644 index 0000000..8bd1a9d --- /dev/null +++ b/pg4wp/driver_pgsql_rewrite.php @@ -0,0 +1,193 @@ +rewrite(); + $logto = strtoupper($rewriter->type()); + switch ($rewriter->type()) { + case 'Update': + // This will avoid modifications to anything following ' SET ' + list($sql, $end) = explode(' SET ', $sql, 2); + $end = ' SET ' . $end; + break; + case 'Insert': + // This will avoid modifications to anything following ' VALUES' + list($sql, $end) = explode(' VALUES', $sql, 2); + $end = ' VALUES' . $end; + + // When installing, the sequence for table terms has to be updated + if(defined('WP_INSTALLING') && WP_INSTALLING && false !== strpos($sql, 'INSERT INTO `' . $wpdb->terms . '`')) { + $end .= ';SELECT setval(\'' . $wpdb->terms . '_term_id_seq\', (SELECT MAX(term_id) FROM ' . $wpdb->terms . ')+1);'; + } + break; + case 'Insert': + break; + default: + } + + $sql = correctMetaValue($sql); + $sql = handleInterval($sql); + $sql = cleanAndCapitalize($sql); + $sql = correctEmptyInStatements($sql); + $sql = correctQuoting($sql); + + // Put back the end of the query if it was separated + $sql .= $end; + + // For insert ID caching + if($logto == 'INSERT') { + $pattern = '/INSERT INTO "?([\w_]+)"? \(([^)]+)\)/i'; + preg_match($pattern, $sql, $matches); + + if (isset($matches[1])) { + $GLOBALS['pg4wp_ins_table'] = $matches[1]; + } + + if (isset($matches[2])) { + $columns_str = $matches[2]; + $columns = explode(',', $columns_str); + $columns = array_map(function ($column) { + return trim(trim($column), '"'); + }, $columns); + if (isset($columns[0])) { + $GLOBALS['pg4wp_ins_field'] = $columns[0]; + } + } + + $GLOBALS['pg4wp_last_insert'] = $sql; + } elseif(isset($GLOBALS['pg4wp_queued_query'])) { + pg_query($GLOBALS['pg4wp_queued_query']); + unset($GLOBALS['pg4wp_queued_query']); + } + + if(PG4WP_DEBUG) { + if($initial != $sql) { + error_log('[' . microtime(true) . "] Converting :\n$initial\n---- to ----\n$sql\n---------------------\n", 3, PG4WP_LOG . 'pg4wp_' . $logto . '.log'); + } else { + error_log('[' . microtime(true) . "] $sql\n---------------------\n", 3, PG4WP_LOG . 'pg4wp_unmodified.log'); + } + } + return $sql; +} + +/** + * Correct the meta_value field for WP 2.9.1 and add type cast. + * + * @param string $sql SQL query string + * @return string Modified SQL query string + */ +function correctMetaValue($sql) +{ + // WP 2.9.1 uses a comparison where text data is not quoted + $sql = preg_replace('/AND meta_value = (-?\d+)/', 'AND meta_value = \'$1\'', $sql); + // Add type cast for meta_value field when it's compared to number + $sql = preg_replace('/AND meta_value < (\d+)/', 'AND meta_value::bigint < $1', $sql); + return $sql; +} + +/** + * Handle interval expressions in SQL query. + * + * @param string $sql SQL query string + * @return string Modified SQL query string + */ +function handleInterval($sql) +{ + // Generic "INTERVAL xx YEAR|MONTH|DAY|HOUR|MINUTE|SECOND" handler + $sql = preg_replace('/INTERVAL[ ]+(\d+)[ ]+(YEAR|MONTH|DAY|HOUR|MINUTE|SECOND)/', "'\$1 \$2'::interval", $sql); + // DATE_SUB handling + $sql = preg_replace('/DATE_SUB[ ]*\(([^,]+),([^\)]+)\)/', '($1::timestamp - $2)', $sql); + return $sql; +} + +/** + * Clean SQL query from illegal characters and handle capitalization. + * + * @param string $sql SQL query string + * @return string Modified SQL query string + */ +function cleanAndCapitalize($sql) +{ + // Remove illegal characters + $sql = str_replace('`', '', $sql); + // Field names with CAPITALS need special handling + if (false !== strpos($sql, 'ID')) { + $patterns = [ + '/ID([^ ])/' => 'ID $1', + '/ID$/' => 'ID ', + '/\(ID/' => '( ID', + '/,ID/' => ', ID', + '/[0-9a-zA-Z_]+ID/' => '"$0"', + '/\.ID/' => '."ID"', + '/[\s]ID /' => ' "ID" ', + '/"ID "/' => ' "ID" ' + ]; + foreach ($patterns as $pattern => $replacement) { + $sql = preg_replace($pattern, $replacement, $sql); + } + } + return $sql; +} + +/** + * Correct empty IN statements in SQL query. + * + * @param string $sql SQL query string + * @return string Modified SQL query string + */ +function correctEmptyInStatements($sql) +{ + $search = ['IN (\'\')', 'IN ( \'\' )', 'IN ()']; + $replace = 'IN (NULL)'; + $sql = str_replace($search, $replace, $sql); + return $sql; +} + +/** + * Correct quoting for PostgreSQL 9.1+ compatibility. + * + * @param string $sql SQL query string + * @return string Modified SQL query string + */ +function correctQuoting($sql) +{ + $sql = str_replace("\\'", "''", $sql); + $sql = str_replace('\"', '"', $sql); + return $sql; +} diff --git a/pg4wp/rewriters/AbstractSQLRewriter.php b/pg4wp/rewriters/AbstractSQLRewriter.php new file mode 100644 index 0000000..020e72e --- /dev/null +++ b/pg4wp/rewriters/AbstractSQLRewriter.php @@ -0,0 +1,26 @@ +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; + } +} diff --git a/pg4wp/rewriters/AlterTableSQLRewriter.php b/pg4wp/rewriters/AlterTableSQLRewriter.php new file mode 100644 index 0000000..727b661 --- /dev/null +++ b/pg4wp/rewriters/AlterTableSQLRewriter.php @@ -0,0 +1,290 @@ + ' 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; + } +} diff --git a/pg4wp/rewriters/CreateTableSQLRewriter.php b/pg4wp/rewriters/CreateTableSQLRewriter.php new file mode 100644 index 0000000..a19af1a --- /dev/null +++ b/pg4wp/rewriters/CreateTableSQLRewriter.php @@ -0,0 +1,168 @@ + ' 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; + } +} diff --git a/pg4wp/rewriters/DeleteSQLRewriter.php b/pg4wp/rewriters/DeleteSQLRewriter.php new file mode 100644 index 0000000..7d98a10 --- /dev/null +++ b/pg4wp/rewriters/DeleteSQLRewriter.php @@ -0,0 +1,104 @@ +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; + } +} diff --git a/pg4wp/rewriters/DescribeSQLRewriter.php b/pg4wp/rewriters/DescribeSQLRewriter.php new file mode 100644 index 0000000..98dfb8c --- /dev/null +++ b/pg4wp/rewriters/DescribeSQLRewriter.php @@ -0,0 +1,81 @@ +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 = << 0 ORDER BY number + SQL; + + return $sql; + } +} diff --git a/pg4wp/rewriters/DropTableSQLRewriter.php b/pg4wp/rewriters/DropTableSQLRewriter.php new file mode 100644 index 0000000..6473e53 --- /dev/null +++ b/pg4wp/rewriters/DropTableSQLRewriter.php @@ -0,0 +1,17 @@ +original(); + + $pattern = '/DROP TABLE.+ [`]?(\w+)[`]?$/'; + preg_match($pattern, $sql, $matches); + $table = $matches[1]; + $seq = $table . '_seq'; + $sql .= ";\nDROP SEQUENCE IF EXISTS $seq;"; + + return $sql; + } +} diff --git a/pg4wp/rewriters/InsertSQLRewriter.php b/pg4wp/rewriters/InsertSQLRewriter.php new file mode 100644 index 0000000..e323c1e --- /dev/null +++ b/pg4wp/rewriters/InsertSQLRewriter.php @@ -0,0 +1,153 @@ +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; + } +} diff --git a/pg4wp/rewriters/OptimizeTableSQLRewriter.php b/pg4wp/rewriters/OptimizeTableSQLRewriter.php new file mode 100644 index 0000000..1ce2c9e --- /dev/null +++ b/pg4wp/rewriters/OptimizeTableSQLRewriter.php @@ -0,0 +1,10 @@ +original(); + return str_replace('OPTIMIZE TABLE', 'VACUUM', $sql); + } +} diff --git a/pg4wp/rewriters/ReplaceIntoSQLRewriter.php b/pg4wp/rewriters/ReplaceIntoSQLRewriter.php new file mode 100644 index 0000000..7e91948 --- /dev/null +++ b/pg4wp/rewriters/ReplaceIntoSQLRewriter.php @@ -0,0 +1,143 @@ +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; + } +} diff --git a/pg4wp/rewriters/SelectSQLRewriter.php b/pg4wp/rewriters/SelectSQLRewriter.php new file mode 100644 index 0000000..7f8899d --- /dev/null +++ b/pg4wp/rewriters/SelectSQLRewriter.php @@ -0,0 +1,400 @@ +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 = '/ (?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 = <<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 = << 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; + } +} diff --git a/pg4wp/rewriters/ShowIndexSQLRewriter.php b/pg4wp/rewriters/ShowIndexSQLRewriter.php new file mode 100644 index 0000000..a0a7783 --- /dev/null +++ b/pg4wp/rewriters/ShowIndexSQLRewriter.php @@ -0,0 +1,62 @@ +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 = <<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 = <<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';"; + } +} diff --git a/pg4wp/rewriters/UpdateSQLRewriter.php b/pg4wp/rewriters/UpdateSQLRewriter.php new file mode 100644 index 0000000..f1f0784 --- /dev/null +++ b/pg4wp/rewriters/UpdateSQLRewriter.php @@ -0,0 +1,32 @@ +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; + } +} diff --git a/wordpress.yaml b/wordpress.yaml index 41c9b01..02b7530 100644 --- a/wordpress.yaml +++ b/wordpress.yaml @@ -15,15 +15,16 @@ spec: spec: initContainers: - name: init-pg4wp - image: hub.hamdocker.ir/library/busybox:latest + image: hub.hamdocker.ir/library/alpine:latest command: - sh - -c - | - wget -O /tmp/pg4wp.zip https://github.com/kevinoid/postgresql-for-wordpress/archive/refs/heads/master.zip - unzip /tmp/pg4wp.zip -d /var/www/html/wp-content/plugins/ - mv /var/www/html/wp-content/plugins/postgresql-for-wordpress-master /var/www/html/wp-content/plugins/pg4wp - cp /var/www/html/wp-content/plugins/pg4wp/db.php /var/www/html/wp-content/db.php + apk add --no-cache git + git clone http://gitea-http.gitea.svc.cluster.local:3000/gitea_admin/apps.git /tmp/repo + mkdir -p /var/www/html/wp-content/plugins/pg4wp + cp -r /tmp/repo/pg4wp/* /var/www/html/wp-content/plugins/pg4wp/ + cp /tmp/repo/pg4wp/db.php /var/www/html/wp-content/db.php volumeMounts: - name: wordpress-data mountPath: /var/www/html @@ -83,4 +84,4 @@ spec: service: name: wordpress port: - number: 80 \ No newline at end of file + number: 80