PostgreSQL Drop-in
PostgreSQL support for WordPress
Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/trgino/postgresql-drop-in/archive/refs/heads/main.zipPostgreSQL support for WordPress.
Status: beta. The translation runtime, health checks, WP-CLI commands, the compatibility scanner and the MySQL → PostgreSQL migration tool exist and are exercised by the test suite and by a PostgreSQL acceptance gate in CI. The plugin is not on wordpress.org yet and is not recommended for production sites yet — see Known limitations.
What it does
WordPress only speaks MySQL. PostgreSQL Drop-in lets a WordPress site run on PostgreSQL 16+ by installing a
wp-content/db.php drop-in that replaces $wpdb with a subclass. The subclass translates each MySQL
statement to PostgreSQL:
MySQL SQL → lexer → parser → AST → rewriter → PostgreSQL emitter → PDO (pdo_pgsql) → PostgreSQL
Translation works on a parsed syntax tree, not on regular expressions. Plugins and themes that use the
$wpdb API are in scope. Code that opens its own mysqli/PDO connections is not.
A MySQL (or MariaDB) site is migrated with WP-CLI:
wp slonik migrate --dry-run # plan only, writes nothing
wp slonik migrate --rehearse # full rehearsal in one transaction, rolled back
wp slonik migrate run # copy the data
wp slonik migrate verify # row count + checksum comparison
Known limitations
-
Case-insensitive equality. MySQL compares text case-insensitively by default; PostgreSQL does not.
WHERE user_login = 'Admin'therefore behaves differently than in MySQL unless case-sensitivity mode is configured. SetWPSLONIK_CI_MODEtocitext(requires thecitextPostgreSQL extension and superuser to install it) ordegraded(useslower()on comparisons, no extension needed, but cannot use indexes). See the admin panel → Case sensitivity section for the mode, its effect and theCREATE EXTENSION citext;setup steps. -
Unsupported MySQL constructs are rejected, not translated. They fail with an explicit error instead of reaching PostgreSQL with a different meaning:
- Functions without a PostgreSQL equivalent:
SHA1(),SHA2(),SUBSTRING_INDEX(). - Syntax without a PostgreSQL equivalent:
UPDATE/DELETEwithLIMIT, multi-tableUPDATE/DELETE(and a multi-tableDELETEacross different tables),@@system variables,CREATE TABLE … AS SELECT,CREATE TABLE … LIKE, complex or chained division (a/2/2). - Statements outside the supported subset:
SHOW CREATE TABLE,SHOW TABLE STATUS,LOCK/UNLOCK,OPTIMIZE,ANALYZE.DROP TABLE IF EXISTSandTRUNCATE TABLEare translated.ALTER TABLE(single-action and multi-action) is translated;SHOW VARIABLESis emulated for a fixed set of variables.INSERT IGNOREbecomesON CONFLICT DO NOTHING: the unique conflict is covered, the other MySQL errors keep failing.GROUP_CONCAT()becomesstring_agg(); theSEPARATORandORDER BYforms are not supported.
DATABASE()andVERSION()are translated:DATABASE()becomescurrent_database()andVERSION()becomesVERSION.The static scan (
wp slonik compat <path>) reports every occurrence with the rule idR-062, so a plugin that uses one of them is known before it runs. - Functions without a PostgreSQL equivalent:
-
Column types follow one rule. A MySQL type with a PostgreSQL equivalent keeps it; otherwise the encompassing upper type is used: the text family (
tinytext…longtext) becomestext, the binary family becomesbytea, and the integers widen to the next one (tinyint→smallint).enum/setbecometextwith aCHECKfor the value list. The upper type always keeps the comparison operators, so alongtextcolumn stays comparable and searchable. -
DDL coverage.
CREATE TABLE(withIF NOT EXISTS/TEMPORARY),DROP TABLE IF EXISTSandTRUNCATE TABLEare translated.ALTER TABLE(single-action and multi-action) is translated;SHOW CREATE TABLE,SHOW TABLE STATUS,LOCK/UNLOCK,OPTIMIZEandANALYZEare not.SHOW VARIABLESis emulated for a fixed set of variables. -
Multisite is not supported.
Plugin compatibility
A plugin matrix installs the plugins below on PostgreSQL 16 through the drop-in, activates them, exercises them and reads every database error from the compatibility log.
| Plugin | Result | Notes |
|---|---|---|
| Query Monitor | Compatible | Verified on WordPress 7.1.3, PostgreSQL 16.0.15, PHP 8.3 |
| Akismet | Compatible | Verified |
| Classic Editor | Compatible | Verified |
| WP Super Cache | Compatible | Verified |
| All-in-One WP Migration | Compatible | Verified |
| WP Mail SMTP | Compatible | Verified (DATABASE() is translated) |
| Contact Form 7 | Compatible | Verified |
| WooCommerce | Not compatible yet | The site runs, but the plugin's own installer and updater use ALTER TABLE and INFORMATION_SCHEMA, which the translation does not support yet |
| Wordfence | Not compatible yet | The site runs, but the plugin uses SHOW VARIABLES and compares binary values that PostgreSQL needs an explicit cast for |
The binary family (binary, varbinary, the blob types) is copied byte for byte into bytea.
The migration removes the NUL bytes from text values - PostgreSQL text cannot store them - and reports how
many were removed for every table. A column whose payload carries bytes a text column cannot hold is
mapped to bytea instead: the Action Scheduler schedule column (a serialized PHP object with NUL bytes
in the property names) is such a column, and its bytes are kept byte for byte.
Requirements
| Component | Version |
|---|---|
| PHP | 7.4+ with pdo_pgsql (mysqli for the migration command) |
| MySQL / MariaDB (source) | MySQL 5.5.5+ (WordPress' own minimum) or MariaDB 10.x+. Verified on MySQL 5.7, MySQL 8.4, MariaDB 10.11 and MariaDB 12.3 |
| WordPress | 6.4+ (single site; multisite is not supported in v1) |
| PostgreSQL | 16+ |
| Hosting | VPS, Docker or managed cloud PostgreSQL. Shared hosting is not a target. |
Design principles
- Opt-in. Activating the plugin never changes the database. PostgreSQL mode is enabled only by an explicit
define( 'WPSLONIK_ENABLED', true );inwp-config.php. - Transparent. Health checks (admin screen and WP-CLI) say what is missing and how to fix it. Errors explain what failed and what to do next.
- Strict. No silent data truncation. MySQL zero dates map to a documented sentinel.
- Private. No telemetry. Compatibility logs redact literal values.
- Migration by WP-CLI. A MySQL → PostgreSQL migration command with
--dry-runand--rehearsemodes.
Development
composer install
composer ci # PHPCS (PHP 7.4 compatibility) + PHPStan + PHPUnit
Code must run on PHP 7.4 as well as current PHP 8.x releases. CI lints on PHP 7.4–8.4 and runs the quality gates on 7.4 and 8.3.
Corpus capture records the real SQL WordPress sends to MySQL. It needs Docker; see tools/corpus/README.md.
License
PostgreSQL is a registered trademark of the PostgreSQL Community Association of Canada. WordPress is a registered trademark of the WordPress Foundation. This project is not affiliated with or endorsed by either.