Repository: kevinoid/postgresql-for-wordpress Branch: v3 Commit: ea843d1560a4 Files: 543 Total size: 5.1 MB Directory structure: gitextract_y00m6hv_/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── sql-rewriting-issue.md │ ├── pull_request_template.md │ └── workflows/ │ └── php.yml ├── .gitignore ├── license.md ├── pg4wp/ │ ├── core.php │ ├── db.php │ ├── driver_mysql.php │ ├── driver_pgsql.php │ ├── driver_pgsql_rewrite.php │ └── rewriters/ │ ├── AbstractSQLRewriter.php │ ├── AlterTableSQLRewriter.php │ ├── CreateTableSQLRewriter.php │ ├── DeleteSQLRewriter.php │ ├── DescribeSQLRewriter.php │ ├── DropTableSQLRewriter.php │ ├── InsertSQLRewriter.php │ ├── OptimizeTableSQLRewriter.php │ ├── ReplaceIntoSQLRewriter.php │ ├── SelectSQLRewriter.php │ ├── SetNamesSQLRewriter.php │ ├── ShowFullColumnsSQLRewriter.php │ ├── ShowIndexSQLRewriter.php │ ├── ShowTableStatusSQLRewriter.php │ ├── ShowTablesSQLRewriter.php │ ├── ShowVariablesSQLRewriter.php │ └── UpdateSQLRewriter.php ├── phpunit.xml ├── readme.md ├── tests/ │ ├── parseTest.php │ ├── rewriteTest.php │ ├── stubs/ │ │ ├── delete-fro_1698679875.txt │ │ ├── delete-fro_1698679877.txt │ │ ├── delete-fro_1698679878.txt │ │ ├── delete-fro_1698679918.txt │ │ ├── delete-fro_1698679920.txt │ │ ├── delete-fro_1698679954.txt │ │ ├── delete-fro_1698679955.txt │ │ ├── insert-int_1698679874.txt │ │ ├── insert-int_1698679877.txt │ │ ├── insert-int_1698679878.txt │ │ ├── insert-int_1698679884.txt │ │ ├── insert-int_1698679897.txt │ │ ├── insert-int_1698679908.txt │ │ ├── insert-int_1698679917.txt │ │ ├── insert-int_1698679920.txt │ │ ├── insert-int_1698679923.txt │ │ ├── insert-int_1698679936.txt │ │ ├── insert-int_1698679954.txt │ │ ├── insert-int_1698679955.txt │ │ ├── insert-int_1698679965.txt │ │ ├── insert-int_1698679972.txt │ │ ├── select-aut_1698679874.txt │ │ ├── select-aut_1698679884.txt │ │ ├── select-aut_1698679897.txt │ │ ├── select-aut_1698679908.txt │ │ ├── select-aut_1698679917.txt │ │ ├── select-aut_1698679923.txt │ │ ├── select-aut_1698679965.txt │ │ ├── select-aut_1698679972.txt │ │ ├── select-blo_1698679989.txt │ │ ├── select-com_1698679872.txt │ │ ├── select-com_1698679886.txt │ │ ├── select-com_1698679936.txt │ │ ├── select-cou_1698679872.txt │ │ ├── select-cou_1698679874.txt │ │ ├── select-cou_1698679884.txt │ │ ├── select-cou_1698679886.txt │ │ ├── select-cou_1698679889.txt │ │ ├── select-cou_1698679900.txt │ │ ├── select-cou_1698679910.txt │ │ ├── select-cou_1698679917.txt │ │ ├── select-cou_1698679926.txt │ │ ├── select-cou_1698679929.txt │ │ ├── select-cou_1698679931.txt │ │ ├── select-cou_1698679936.txt │ │ ├── select-cou_1698679941.txt │ │ ├── select-cou_1698679949.txt │ │ ├── select-cou_1698679952.txt │ │ ├── select-cou_1698679955.txt │ │ ├── select-cou_1698679965.txt │ │ ├── select-cou_1698679972.txt │ │ ├── select-cou_1698679984.txt │ │ ├── select-dis_1698679872.txt │ │ ├── select-dis_1698679874.txt │ │ ├── select-dis_1698679876.txt │ │ ├── select-dis_1698679884.txt │ │ ├── select-dis_1698679885.txt │ │ ├── select-dis_1698679886.txt │ │ ├── select-dis_1698679910.txt │ │ ├── select-dis_1698679917.txt │ │ ├── select-dis_1698679929.txt │ │ ├── select-dis_1698679931.txt │ │ ├── select-dis_1698679936.txt │ │ ├── select-dis_1698679941.txt │ │ ├── select-dis_1698679949.txt │ │ ├── select-dis_1698679952.txt │ │ ├── select-dis_1698679955.txt │ │ ├── select-dis_1698679965.txt │ │ ├── select-dis_1698679972.txt │ │ ├── select-dis_1698679984.txt │ │ ├── select-fro_1698679872.txt │ │ ├── select-fro_1698679873.txt │ │ ├── select-fro_1698679874.txt │ │ ├── select-fro_1698679875.txt │ │ ├── select-fro_1698679876.txt │ │ ├── select-fro_1698679883.txt │ │ ├── select-fro_1698679884.txt │ │ ├── select-fro_1698679886.txt │ │ ├── select-fro_1698679887.txt │ │ ├── select-fro_1698679889.txt │ │ ├── select-fro_1698679897.txt │ │ ├── select-fro_1698679900.txt │ │ ├── select-fro_1698679907.txt │ │ ├── select-fro_1698679910.txt │ │ ├── select-fro_1698679911.txt │ │ ├── select-fro_1698679917.txt │ │ ├── select-fro_1698679918.txt │ │ ├── select-fro_1698679919.txt │ │ ├── select-fro_1698679922.txt │ │ ├── select-fro_1698679923.txt │ │ ├── select-fro_1698679924.txt │ │ ├── select-fro_1698679926.txt │ │ ├── select-fro_1698679929.txt │ │ ├── select-fro_1698679931.txt │ │ ├── select-fro_1698679935.txt │ │ ├── select-fro_1698679936.txt │ │ ├── select-fro_1698679940.txt │ │ ├── select-fro_1698679941.txt │ │ ├── select-fro_1698679949.txt │ │ ├── select-fro_1698679950.txt │ │ ├── select-fro_1698679952.txt │ │ ├── select-fro_1698679954.txt │ │ ├── select-fro_1698679955.txt │ │ ├── select-fro_1698679956.txt │ │ ├── select-fro_1698679960.txt │ │ ├── select-fro_1698679965.txt │ │ ├── select-fro_1698679972.txt │ │ ├── select-fro_1698679984.txt │ │ ├── select-fro_1698679985.txt │ │ ├── select-fro_1698679987.txt │ │ ├── select-fro_1698679988.txt │ │ ├── select-fro_1698679989.txt │ │ ├── select-fro_1698679991.txt │ │ ├── select-fro_1698679992.txt │ │ ├── select-fro_1698679996.txt │ │ ├── select-fro_1698679999.txt │ │ ├── select-fro_1698680063.txt │ │ ├── select-fro_1698680183.txt │ │ ├── select-fro_1698680305.txt │ │ ├── select-id-_1698679924.txt │ │ ├── select-id-_1698679926.txt │ │ ├── select-met_1698679872.txt │ │ ├── select-met_1698679874.txt │ │ ├── select-met_1698679875.txt │ │ ├── select-met_1698679876.txt │ │ ├── select-met_1698679877.txt │ │ ├── select-met_1698679878.txt │ │ ├── select-met_1698679883.txt │ │ ├── select-met_1698679884.txt │ │ ├── select-met_1698679886.txt │ │ ├── select-met_1698679887.txt │ │ ├── select-met_1698679889.txt │ │ ├── select-met_1698679897.txt │ │ ├── select-met_1698679900.txt │ │ ├── select-met_1698679907.txt │ │ ├── select-met_1698679908.txt │ │ ├── select-met_1698679910.txt │ │ ├── select-met_1698679911.txt │ │ ├── select-met_1698679917.txt │ │ ├── select-met_1698679918.txt │ │ ├── select-met_1698679919.txt │ │ ├── select-met_1698679920.txt │ │ ├── select-met_1698679922.txt │ │ ├── select-met_1698679923.txt │ │ ├── select-met_1698679924.txt │ │ ├── select-met_1698679926.txt │ │ ├── select-met_1698679929.txt │ │ ├── select-met_1698679931.txt │ │ ├── select-met_1698679935.txt │ │ ├── select-met_1698679936.txt │ │ ├── select-met_1698679940.txt │ │ ├── select-met_1698679949.txt │ │ ├── select-met_1698679950.txt │ │ ├── select-met_1698679952.txt │ │ ├── select-met_1698679954.txt │ │ ├── select-met_1698679955.txt │ │ ├── select-met_1698679956.txt │ │ ├── select-met_1698679960.txt │ │ ├── select-met_1698679965.txt │ │ ├── select-met_1698679972.txt │ │ ├── select-met_1698679984.txt │ │ ├── select-met_1698679985.txt │ │ ├── select-met_1698679987.txt │ │ ├── select-met_1698679988.txt │ │ ├── select-met_1698679989.txt │ │ ├── select-met_1698679991.txt │ │ ├── select-met_1698679992.txt │ │ ├── select-met_1698679993.txt │ │ ├── select-met_1698679996.txt │ │ ├── select-met_1698679999.txt │ │ ├── select-met_1698680063.txt │ │ ├── select-met_1698680183.txt │ │ ├── select-met_1698680305.txt │ │ ├── select-opt_1698679872.txt │ │ ├── select-opt_1698679874.txt │ │ ├── select-opt_1698679875.txt │ │ ├── select-opt_1698679876.txt │ │ ├── select-opt_1698679883.txt │ │ ├── select-opt_1698679884.txt │ │ ├── select-opt_1698679886.txt │ │ ├── select-opt_1698679887.txt │ │ ├── select-opt_1698679889.txt │ │ ├── select-opt_1698679897.txt │ │ ├── select-opt_1698679900.txt │ │ ├── select-opt_1698679907.txt │ │ ├── select-opt_1698679908.txt │ │ ├── select-opt_1698679910.txt │ │ ├── select-opt_1698679911.txt │ │ ├── select-opt_1698679917.txt │ │ ├── select-opt_1698679918.txt │ │ ├── select-opt_1698679919.txt │ │ ├── select-opt_1698679922.txt │ │ ├── select-opt_1698679923.txt │ │ ├── select-opt_1698679924.txt │ │ ├── select-opt_1698679926.txt │ │ ├── select-opt_1698679929.txt │ │ ├── select-opt_1698679931.txt │ │ ├── select-opt_1698679935.txt │ │ ├── select-opt_1698679936.txt │ │ ├── select-opt_1698679940.txt │ │ ├── select-opt_1698679941.txt │ │ ├── select-opt_1698679949.txt │ │ ├── select-opt_1698679950.txt │ │ ├── select-opt_1698679952.txt │ │ ├── select-opt_1698679954.txt │ │ ├── select-opt_1698679955.txt │ │ ├── select-opt_1698679956.txt │ │ ├── select-opt_1698679960.txt │ │ ├── select-opt_1698679965.txt │ │ ├── select-opt_1698679972.txt │ │ ├── select-opt_1698679984.txt │ │ ├── select-opt_1698679985.txt │ │ ├── select-opt_1698679987.txt │ │ ├── select-opt_1698679988.txt │ │ ├── select-opt_1698679989.txt │ │ ├── select-opt_1698679991.txt │ │ ├── select-opt_1698679992.txt │ │ ├── select-opt_1698679993.txt │ │ ├── select-opt_1698679996.txt │ │ ├── select-opt_1698679999.txt │ │ ├── select-opt_1698680063.txt │ │ ├── select-opt_1698680183.txt │ │ ├── select-opt_1698680305.txt │ │ ├── select-pos_1698679872.txt │ │ ├── select-pos_1698679873.txt │ │ ├── select-pos_1698679874.txt │ │ ├── select-pos_1698679876.txt │ │ ├── select-pos_1698679884.txt │ │ ├── select-pos_1698679885.txt │ │ ├── select-pos_1698679886.txt │ │ ├── select-pos_1698679887.txt │ │ ├── select-pos_1698679910.txt │ │ ├── select-pos_1698679911.txt │ │ ├── select-pos_1698679917.txt │ │ ├── select-pos_1698679919.txt │ │ ├── select-pos_1698679923.txt │ │ ├── select-pos_1698679924.txt │ │ ├── select-pos_1698679926.txt │ │ ├── select-pos_1698679929.txt │ │ ├── select-pos_1698679931.txt │ │ ├── select-pos_1698679936.txt │ │ ├── select-pos_1698679941.txt │ │ ├── select-pos_1698679949.txt │ │ ├── select-pos_1698679952.txt │ │ ├── select-pos_1698679955.txt │ │ ├── select-pos_1698679956.txt │ │ ├── select-pos_1698679965.txt │ │ ├── select-pos_1698679972.txt │ │ ├── select-pos_1698679984.txt │ │ ├── select-ses_1698679872.txt │ │ ├── select-ses_1698679874.txt │ │ ├── select-ses_1698679875.txt │ │ ├── select-ses_1698679876.txt │ │ ├── select-ses_1698679883.txt │ │ ├── select-ses_1698679884.txt │ │ ├── select-ses_1698679886.txt │ │ ├── select-ses_1698679887.txt │ │ ├── select-ses_1698679889.txt │ │ ├── select-ses_1698679897.txt │ │ ├── select-ses_1698679900.txt │ │ ├── select-ses_1698679907.txt │ │ ├── select-ses_1698679910.txt │ │ ├── select-ses_1698679911.txt │ │ ├── select-ses_1698679917.txt │ │ ├── select-ses_1698679918.txt │ │ ├── select-ses_1698679919.txt │ │ ├── select-ses_1698679922.txt │ │ ├── select-ses_1698679923.txt │ │ ├── select-ses_1698679924.txt │ │ ├── select-ses_1698679926.txt │ │ ├── select-ses_1698679928.txt │ │ ├── select-ses_1698679931.txt │ │ ├── select-ses_1698679935.txt │ │ ├── select-ses_1698679936.txt │ │ ├── select-ses_1698679940.txt │ │ ├── select-ses_1698679949.txt │ │ ├── select-ses_1698679950.txt │ │ ├── select-ses_1698679952.txt │ │ ├── select-ses_1698679953.txt │ │ ├── select-ses_1698679954.txt │ │ ├── select-ses_1698679955.txt │ │ ├── select-ses_1698679956.txt │ │ ├── select-ses_1698679960.txt │ │ ├── select-ses_1698679965.txt │ │ ├── select-ses_1698679972.txt │ │ ├── select-ses_1698679984.txt │ │ ├── select-ses_1698679985.txt │ │ ├── select-ses_1698679987.txt │ │ ├── select-ses_1698679988.txt │ │ ├── select-ses_1698679989.txt │ │ ├── select-ses_1698679991.txt │ │ ├── select-ses_1698679992.txt │ │ ├── select-ses_1698679995.txt │ │ ├── select-ses_1698679999.txt │ │ ├── select-ses_1698680063.txt │ │ ├── select-ses_1698680183.txt │ │ ├── select-ses_1698680304.txt │ │ ├── select-sql_1698679872.txt │ │ ├── select-sql_1698679874.txt │ │ ├── select-sql_1698679875.txt │ │ ├── select-sql_1698679876.txt │ │ ├── select-sql_1698679884.txt │ │ ├── select-sql_1698679886.txt │ │ ├── select-sql_1698679917.txt │ │ ├── select-sql_1698679918.txt │ │ ├── select-sql_1698679919.txt │ │ ├── select-sql_1698679923.txt │ │ ├── select-sql_1698679929.txt │ │ ├── select-sql_1698679931.txt │ │ ├── select-sql_1698679936.txt │ │ ├── select-sql_1698679941.txt │ │ ├── select-sql_1698679952.txt │ │ ├── select-sql_1698679954.txt │ │ ├── select-sql_1698679955.txt │ │ ├── select-sql_1698679956.txt │ │ ├── select-sql_1698679989.txt │ │ ├── select-sql_1698679991.txt │ │ ├── select-t-t_1698679872.txt │ │ ├── select-t-t_1698679874.txt │ │ ├── select-t-t_1698679875.txt │ │ ├── select-t-t_1698679876.txt │ │ ├── select-t-t_1698679883.txt │ │ ├── select-t-t_1698679884.txt │ │ ├── select-t-t_1698679886.txt │ │ ├── select-t-t_1698679887.txt │ │ ├── select-t-t_1698679889.txt │ │ ├── select-t-t_1698679897.txt │ │ ├── select-t-t_1698679900.txt │ │ ├── select-t-t_1698679907.txt │ │ ├── select-t-t_1698679908.txt │ │ ├── select-t-t_1698679910.txt │ │ ├── select-t-t_1698679911.txt │ │ ├── select-t-t_1698679917.txt │ │ ├── select-t-t_1698679918.txt │ │ ├── select-t-t_1698679919.txt │ │ ├── select-t-t_1698679922.txt │ │ ├── select-t-t_1698679923.txt │ │ ├── select-t-t_1698679924.txt │ │ ├── select-t-t_1698679926.txt │ │ ├── select-t-t_1698679929.txt │ │ ├── select-t-t_1698679931.txt │ │ ├── select-t-t_1698679936.txt │ │ ├── select-t-t_1698679941.txt │ │ ├── select-t-t_1698679949.txt │ │ ├── select-t-t_1698679950.txt │ │ ├── select-t-t_1698679952.txt │ │ ├── select-t-t_1698679954.txt │ │ ├── select-t-t_1698679955.txt │ │ ├── select-t-t_1698679956.txt │ │ ├── select-t-t_1698679960.txt │ │ ├── select-t-t_1698679965.txt │ │ ├── select-t-t_1698679972.txt │ │ ├── select-t-t_1698679984.txt │ │ ├── select-t-t_1698679985.txt │ │ ├── select-t-t_1698679987.txt │ │ ├── select-t-t_1698679988.txt │ │ ├── select-t-t_1698679989.txt │ │ ├── select-t-t_1698679991.txt │ │ ├── select-t-t_1698679992.txt │ │ ├── select-t-t_1698679996.txt │ │ ├── select-t-t_1698679999.txt │ │ ├── select-t-t_1698680063.txt │ │ ├── select-t-t_1698680183.txt │ │ ├── select-t-t_1698680305.txt │ │ ├── select-ter_1698679965.txt │ │ ├── select-ter_1698679972.txt │ │ ├── select-tt-_1698679897.txt │ │ ├── select-tt-_1698679908.txt │ │ ├── select-use_1698679872.txt │ │ ├── select-use_1698679874.txt │ │ ├── select-use_1698679875.txt │ │ ├── select-use_1698679876.txt │ │ ├── select-use_1698679883.txt │ │ ├── select-use_1698679884.txt │ │ ├── select-use_1698679886.txt │ │ ├── select-use_1698679887.txt │ │ ├── select-use_1698679889.txt │ │ ├── select-use_1698679897.txt │ │ ├── select-use_1698679900.txt │ │ ├── select-use_1698679907.txt │ │ ├── select-use_1698679910.txt │ │ ├── select-use_1698679911.txt │ │ ├── select-use_1698679917.txt │ │ ├── select-use_1698679918.txt │ │ ├── select-use_1698679919.txt │ │ ├── select-use_1698679922.txt │ │ ├── select-use_1698679923.txt │ │ ├── select-use_1698679924.txt │ │ ├── select-use_1698679926.txt │ │ ├── select-use_1698679929.txt │ │ ├── select-use_1698679931.txt │ │ ├── select-use_1698679936.txt │ │ ├── select-use_1698679941.txt │ │ ├── select-use_1698679949.txt │ │ ├── select-use_1698679950.txt │ │ ├── select-use_1698679952.txt │ │ ├── select-use_1698679954.txt │ │ ├── select-use_1698679955.txt │ │ ├── select-use_1698679956.txt │ │ ├── select-use_1698679960.txt │ │ ├── select-use_1698679965.txt │ │ ├── select-use_1698679972.txt │ │ ├── select-use_1698679984.txt │ │ ├── select-use_1698679985.txt │ │ ├── select-use_1698679987.txt │ │ ├── select-use_1698679988.txt │ │ ├── select-use_1698679989.txt │ │ ├── select-use_1698679991.txt │ │ ├── select-use_1698679992.txt │ │ ├── select-use_1698679996.txt │ │ ├── select-use_1698679999.txt │ │ ├── select-use_1698680063.txt │ │ ├── select-use_1698680183.txt │ │ ├── select-use_1698680305.txt │ │ ├── select-wp-_1698679872.txt │ │ ├── select-wp-_1698679875.txt │ │ ├── select-wp-_1698679883.txt │ │ ├── select-wp-_1698679884.txt │ │ ├── select-wp-_1698679887.txt │ │ ├── select-wp-_1698679889.txt │ │ ├── select-wp-_1698679897.txt │ │ ├── select-wp-_1698679900.txt │ │ ├── select-wp-_1698679908.txt │ │ ├── select-wp-_1698679911.txt │ │ ├── select-wp-_1698679922.txt │ │ ├── select-wp-_1698679950.txt │ │ ├── select-wp-_1698679956.txt │ │ ├── select-wp-_1698679960.txt │ │ ├── select-wp-_1698679985.txt │ │ ├── select-wp-_1698679988.txt │ │ ├── select-wp-_1698680305.txt │ │ ├── set-names-_1698679872.txt │ │ ├── set-names-_1698679874.txt │ │ ├── set-names-_1698679875.txt │ │ ├── set-names-_1698679876.txt │ │ ├── set-names-_1698679883.txt │ │ ├── set-names-_1698679884.txt │ │ ├── set-names-_1698679886.txt │ │ ├── set-names-_1698679887.txt │ │ ├── set-names-_1698679889.txt │ │ ├── set-names-_1698679897.txt │ │ ├── set-names-_1698679900.txt │ │ ├── set-names-_1698679907.txt │ │ ├── set-names-_1698679910.txt │ │ ├── set-names-_1698679911.txt │ │ ├── set-names-_1698679917.txt │ │ ├── set-names-_1698679918.txt │ │ ├── set-names-_1698679919.txt │ │ ├── set-names-_1698679922.txt │ │ ├── set-names-_1698679923.txt │ │ ├── set-names-_1698679924.txt │ │ ├── set-names-_1698679926.txt │ │ ├── set-names-_1698679928.txt │ │ ├── set-names-_1698679931.txt │ │ ├── set-names-_1698679935.txt │ │ ├── set-names-_1698679936.txt │ │ ├── set-names-_1698679940.txt │ │ ├── set-names-_1698679949.txt │ │ ├── set-names-_1698679950.txt │ │ ├── set-names-_1698679952.txt │ │ ├── set-names-_1698679953.txt │ │ ├── set-names-_1698679954.txt │ │ ├── set-names-_1698679955.txt │ │ ├── set-names-_1698679956.txt │ │ ├── set-names-_1698679960.txt │ │ ├── set-names-_1698679965.txt │ │ ├── set-names-_1698679972.txt │ │ ├── set-names-_1698679984.txt │ │ ├── set-names-_1698679985.txt │ │ ├── set-names-_1698679987.txt │ │ ├── set-names-_1698679988.txt │ │ ├── set-names-_1698679989.txt │ │ ├── set-names-_1698679991.txt │ │ ├── set-names-_1698679992.txt │ │ ├── set-names-_1698679995.txt │ │ ├── set-names-_1698679999.txt │ │ ├── set-names-_1698680063.txt │ │ ├── set-names-_1698680183.txt │ │ ├── set-names-_1698680304.txt │ │ ├── show-full-_1698679874.txt │ │ ├── show-full-_1698679875.txt │ │ ├── show-full-_1698679884.txt │ │ ├── show-full-_1698679886.txt │ │ ├── show-full-_1698679897.txt │ │ ├── show-full-_1698679908.txt │ │ ├── show-full-_1698679917.txt │ │ ├── show-full-_1698679918.txt │ │ ├── show-full-_1698679923.txt │ │ ├── show-full-_1698679926.txt │ │ ├── show-full-_1698679936.txt │ │ ├── show-full-_1698679954.txt │ │ ├── show-full-_1698679965.txt │ │ ├── show-full-_1698679972.txt │ │ ├── show-full-_1698679996.txt │ │ ├── update-wp-_1698679874.txt │ │ ├── update-wp-_1698679884.txt │ │ ├── update-wp-_1698679886.txt │ │ ├── update-wp-_1698679917.txt │ │ ├── update-wp-_1698679923.txt │ │ ├── update-wp-_1698679926.txt │ │ ├── update-wp-_1698679936.txt │ │ ├── update-wp-_1698679965.txt │ │ ├── update-wp-_1698679972.txt │ │ └── update-wp-_1698679996.txt │ ├── tools/ │ │ ├── php-cs-fixer.phar │ │ └── phpunit.phar │ └── verifyAgainstStubsTest.php └── wp-includes/ ├── cache.php ├── class-wp-object-cache.php ├── class-wpdb.php ├── l10n.php └── version.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/sql-rewriting-issue.md ================================================ --- name: SQL Rewriting Issue about: This is used for filing bugs or problems with PG4WP title: '' labels: '' assignees: '' --- WP Version: PG4WP Version: Error: ``` ``` RAW SQL ``` ``` Expected Rewritten SQL ``` ``` Actual Rewritten SQL ``` ``` ================================================ FILE: .github/pull_request_template.md ================================================ Related Issues: - Added Tests: - ================================================ FILE: .github/workflows/php.yml ================================================ name: PHP Composer on: push: branches: [ "v2", "v3" ] pull_request: branches: [ "v2", "v3" ] permissions: contents: read jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run Tests run: ./tests/tools/phpunit.phar tests/ ================================================ FILE: .gitignore ================================================ logs .phpunit.result.cache .php-cs-fixer.cache ================================================ FILE: license.md ================================================ # GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. ## Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. ## TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION **0.** This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. **1.** You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. **2.** You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: **a)** You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. **b)** You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. **c)** If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. **3.** You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: **a)** Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, **b)** Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, **c)** Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. **4.** You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. **5.** You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. **6.** Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. **7.** If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. **8.** If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. **9.** The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. **10.** If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. **NO WARRANTY** **11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. **12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS ## How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands \`show w' and \`show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than \`show w' and \`show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the [GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl.html) instead of this License. ================================================ FILE: pg4wp/core.php ================================================ '// 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); } ================================================ FILE: pg4wp/db.php ================================================ 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. } ================================================ FILE: pg4wp/driver_pgsql_rewrite.php ================================================ 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; } ================================================ FILE: pg4wp/rewriters/AbstractSQLRewriter.php ================================================ 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; } } ================================================ FILE: pg4wp/rewriters/AlterTableSQLRewriter.php ================================================ ' 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; } } ================================================ FILE: pg4wp/rewriters/CreateTableSQLRewriter.php ================================================ ' 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; } } ================================================ FILE: pg4wp/rewriters/DeleteSQLRewriter.php ================================================ 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; } } ================================================ FILE: pg4wp/rewriters/DescribeSQLRewriter.php ================================================ 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; } } ================================================ FILE: pg4wp/rewriters/DropTableSQLRewriter.php ================================================ original(); $pattern = '/DROP TABLE.+ [`]?(\w+)[`]?$/'; preg_match($pattern, $sql, $matches); $table = $matches[1]; $seq = $table . '_seq'; $sql .= ";\nDROP SEQUENCE IF EXISTS $seq;"; return $sql; } } ================================================ FILE: pg4wp/rewriters/InsertSQLRewriter.php ================================================ 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; } } ================================================ FILE: pg4wp/rewriters/OptimizeTableSQLRewriter.php ================================================ original(); return str_replace('OPTIMIZE TABLE', 'VACUUM', $sql); } } ================================================ FILE: pg4wp/rewriters/ReplaceIntoSQLRewriter.php ================================================ 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; } } ================================================ FILE: pg4wp/rewriters/SelectSQLRewriter.php ================================================ 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; } } ================================================ FILE: pg4wp/rewriters/ShowIndexSQLRewriter.php ================================================ 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';"; } } ================================================ FILE: pg4wp/rewriters/UpdateSQLRewriter.php ================================================ 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; } } ================================================ FILE: phpunit.xml ================================================ tests ================================================ FILE: readme.md ================================================ ## PostgreSQL for WordPress (PG4WP) ### Description PostgreSQL for WordPress (PG4WP) gives you the possibility to install and use WordPress with a PostgreSQL database as a backend. #### Use Cases - Run Wordpress on your Existing Postgres Cluster - Run Wordpress with Georeplication with a Multi-Active Postgres instalation such as [EDB Postgres Distributed](https://www.enterprisedb.com/products/edb-postgres-distributed), or [CockroachDB](https://www.cockroachlabs.com/serverless/) for a [highly available](https://www.cockroachlabs.com/blog/brief-history-high-availability/) and resilient Wordpress Infrastructure ### Design PostgreSQL for Wordpress works by intercepting calls to the mysqli_ driver in wordpress's wpdb class. it replaces calls to mysqli_ with wpsqli_ which then are implemented by the driver files found in this plugin. ![PG4WP Design](docs/images/pg4wp_design.png) ### Supported Wordpress Versions This plugin has been tested against - Wordpress 6.5.3, 6.4.3 (v3 branch) - Wordpress 6.3.2 (v2 branch) ### Supported PHP versions This plugin requires PHP 8.1 or greater ### Supported PostgreSQL versions This plugin has been tested on PostgreSQL 14.2 ### Plugin Support | Plugin | Version | Working | | ----------- | ----------- | --------- | | Debug Bar | 1.1.4 | Confirmed | | Yoast Duplicate Post | 4.2 | Confirmed | ### Theme Support | Theme | Version | Working | | ----------- | ----------- | --------- | | Twenty Twenty-Four | 1.0 | Confirmed | | Twenty Twenty-Three | 1.2 | Confirmed | | Twenty Twenty-Two | 1.5 | Confirmed | | Twenty Twenty-One | 1.9 | Confirmed | ### Installation You have to install PG4WP *before* configuring your WordPress installation for things to work properly. This is because the database needs to be up and running before any plugin can be loaded. 1. Place your WordPress files in the right place on your web server. 1. Download the latest release From the [releases page](https://github.com/PostgreSQL-For-Wordpress/postgresql-for-wordpress/releases) 1. Unzip the files from PG4WP and put the `pg4wp` directory in your `/wp-content` directory. 1. Copy the `db.php` from the `pg4wp` directory to `wp-content` You can modify this file to configure the database driver you wish to use Currently you can set 'DB_DRIVER' to 'pgsql' or 'mysql' You can also activate DEBUG and/or ERROR logs 1. Create `wp-config.php` from `wp-config-sample.php` if it does not already exist (PG4WP does not currently intercept database connection setup). 1. Point your Web Browser to your WordPress installation and go through the traditional WordPress installation routine. ### Contributing Contributions are welcome, please open a pull request with your changes and make sure the tests pass by running the test suite using `./tests/tools/phpunit.phar tests/` If you find a failing scenario please add a test for it, A PR which fixes a scenario but does not include a test will not be accepted. ### License PG4WP is provided "as-is" with no warranty in the hope it can be useful. PG4WP is licensed under the [GNU GPL](http://www.gnu.org/licenses/gpl.html "GNU GPL") v2 or any newer version at your choice. ### Changelog #### Latest Changes - Fixed issue with SQL DELETE query rewriting to use DB_PREFIX consistently, which previously caused PostgreSQL syntax errors due to hardcoded table prefixes ### Contributors Code originally by Hawk__ (http://www.hawkix.net/) Modifications by @kevinoid and @mattbucci ================================================ FILE: tests/parseTest.php ================================================ assertSame($GLOBALS['pg4wp_ins_table'], "wp_options"); $this->assertSame($GLOBALS['pg4wp_ins_field'], "option_name"); } public function test_it_can_parse_a_page_creation_correctly() { $sql = 'INSERT INTO wp_posts (post_author, post_date, post_date_gmt, post_content, post_content_filtered, post_title, post_excerpt, post_status, post_type, comment_status, ping_status, post_password, post_name, to_ping, pinged, post_modified, post_modified_gmt, post_parent, menu_order, post_mime_type, guid) VALUES (\'1\', \'2023-10-31 03:54:02\', now() AT TIME ZONE \'gmt\', \'\', \'\', \'Auto Draft\', \'\', \'auto-draft\', \'page\', \'closed\', \'closed\', \'\', \'\', \'\', \'\', \'2023-10-31 03:54:02\', now() AT TIME ZONE \'gmt\', 0, 0, \'\', \'\')'; $postgresql = pg4wp_rewrite($sql); $this->assertSame($GLOBALS['pg4wp_ins_table'], "wp_posts"); $this->assertSame($GLOBALS['pg4wp_ins_field'], "post_author"); } protected function setUp(): void { global $wpdb; $wpdb = new class () { public $options = "wp_options"; public $categories = "wp_categories"; }; } } ================================================ FILE: tests/rewriteTest.php ================================================ assertSame(trim($expected), trim($postgresql)); } public function test_it_adds_group_by() { $sql = 'SELECT COUNT(id), username FROM users'; $expected = 'SELECT COUNT(id) AS count0, username FROM users GROUP BY username'; $postgresql = pg4wp_rewrite($sql); $this->assertSame(trim($expected), trim($postgresql)); } public function test_it_handles_auto_increment() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_handles_auto_increment_without_null() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_handles_numerics_without_auto_incrment_case_insensitively() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_handles_keys() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_handles_keys_without_unique() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_does_not_remove_if_not_exists() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_removes_character_sets() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_handles_multiple_keys() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_removes_table_charsets() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_can_create_keys_with_length() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_can_create_double_keys_with_length() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_will_handle_found_rows_on_queries_with_order_by_case() { $GLOBALS['pg4wp_numrows_query'] = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_will_append_returning_id_to_insert_statements() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_can_handle_replacement_sql() { $sql = "REPLACE INTO test2 (column1, column2, column3) VALUES (1, 'Old', '2014-08-20 18:47:00')"; $expected = "INSERT INTO test2 (column1, column2, column3) VALUES (1, 'Old', '2014-08-20 18:47:00') ON CONFLICT (column1) DO UPDATE SET column2 = EXCLUDED.column2, column3 = EXCLUDED.column3 RETURNING *"; $postgresql = pg4wp_rewrite($sql); $this->assertSame(trim($expected), trim($postgresql)); } public function test_it_doesnt_rewrite_when_it_doesnt_need_to() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_handles_alter_tables_with_indexes() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_handles_alter_tables_with_unique_indexes() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_rewrites_protected_column_names() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_rewrites_advanced_protected_column_names() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_doesnt_remove_single_quotes() { $sql = <<assertSame(trim($expected), trim($postgresql)); } public function test_it_can_handle_insert_sql_containing_nested_parathesis_with_numbers() { $sql = <<