- Client certificate authentication (mTLS) for PostgreSQL servers requiring
it (e.g. Google Cloud SQL).
ConnectionParamsalready carriedssl_certandssl_key, butbuild_tls_connectornever read them — every TLS branch called.with_no_client_auth()unconditionally, so connections failed with "connection requires a valid client certificate" the same way the builtin driver'spool_manager.rsdid before it was fixed upstream (TabularisDB/tabularis#666). Addedload_client_cert_from_pem, reusing the samerustls::pki_types::pem::PemObjectmachinery as the existingload_roots_from_pem(rather than reintroducingrustls-pemfile, removed above for being unmaintained) —PrivateKeyDersupports PKCS1/SEC1/PKCS8 via the same trait. Both TLS branches now present the client cert via.with_client_auth_cert(...)whenssl_cert/ssl_keyare set, andbuild_tls_connectorerrors clearly if only one of the pair is provided. - Pool cache key ignored every TLS param (
ssl_mode/ssl_ca/ssl_cert/ssl_key) —connection_keymatched only onhost:port:database:user:startup_script, so two connections to the same target differing only in TLS configuration (e.g.requirevs.verify-full, or different client certs) could incorrectly share a cached pool and its already-negotiated TLS setup. This wasn't inherited from the builtin driver: the builtin'sbuild_connection_keyalready keyed onssl_mode/ssl_cabefore this plugin'sclient.rswas even staged, so this was a parity miss during extraction, not a later upstream change. Folded all four TLS params intoconnection_key, matching the builtin's TLS-param keying. ssl_mode=verify-caincorrectly enforced hostname verification —build_tls_connectorwrappedrustls::client::WebPkiServerVerifierforverify-ca, whoseverify_server_certunconditionally checks the hostname with no way to opt out, makingverify-cabehave identically toverify-full.verify-cais supposed to validate the certificate chain but skip hostname verification — that's the entire distinction fromverify-full(matches libpqsslmode=verify-casemantics). Added a dedicatedVerifyCaCertVerifier, ported from the builtin driver'ssrc-tauri/src/pool_manager.rs, usingrustls::client::verify_server_cert_signed_by_trust_anchordirectly instead. Proved the bug and the fix with a chain-valid cert whose hostname deliberately doesn't match the connection target: rejected before this fix, accepted after (while a CA-untrusted cert is still correctly rejected, andverify-fullstill correctly rejects the hostname mismatch).- MONEY columns always read as
null—extract_valuehad no case forType::MONEY, so it fell through to the generic string fallback, butString'sFromSql::acceptsreturnsfalseforType::MONEY(confirmed directly), making that fallback fail every time regardless of the actual value. MONEY is listed as a supported numeric type in the README andddl.rs's implicit-cast-compatible group, but reading it back was silently broken. Added aMoneywrapper (src/extract.rs) that decodes the same 8-byte big-endian i64 wire formatINT8uses and reuses the existingi64_to_jsonJS-safe-integer stringification, matching the builtin driver'sextract/advanced_types.rs::Money. ssl_mode=require/verify-ca/verify-fullsilently allowed plaintext connections —build_poolnever calledcfg.ssl_mode(...)on thedeadpool_postgres::Config, so the underlyingtokio_postgres::Configkept its own default (SslMode::Prefer: negotiate TLS if offered, but accept plaintext otherwise) regardless of what this plugin'sssl_modewas actually set to. A user opting into "TLS or nothing" got an unencrypted connection with no error if the server couldn't/wouldn't negotiate TLS — a security-relevant gap, not just a correctness one. Addedresolve_ssl_mode, mapping this plugin'sssl_modestrings todeadpool_postgres::SslModeto match the builtin driver'sbuild_postgres_configurationsmapping exactly (disable→Disable,allow/prefer→Prefer,require/verify-ca/verify-full→Require), and wired it intobuild_pool. Proved the bug and the fix with a live test against a real non-SSL PostgreSQL instance:ssl_mode=requireconnected successfully before this fix, and now correctly fails.ssl_mode=requirevalidated the server certificate against the platform trust store instead of skipping validation entirely.build_tls_connector's own doc comment already saidrequireshould force TLS "without certificate validation," butneeds_cert_validationonly matchedverify-ca/verify-full—requirefell through to the finalwith_platform_verifier()fallback, which does validate against the OS trust store, defeating the entire point ofrequirevs.verify-full(the standardrequireuse case is self-signed certs / private CAs the user hasn't configuredssl_cafor). AddedNoCertVerifier, ported from the builtin driver'ssrc-tauri/src/pool_manager.rs::NoCertVerifier(accepts any certificate unconditionally — no chain, hostname, or even TLS 1.2/1.3 signature verification), and routedrequiremode to it. Proved the bug and the fix live against a real self-signed-cert SSL-enabled PostgreSQL instance:requirefailed the TLS handshake before this fix, and now connects successfully; confirmed no regression inverify-ca/verify-full(still correctly validate) orrequireagainst a non-SSL server (still correctly fails, per the previous entry).verify-cawithout an explicitssl_cafile silently fell through towith_platform_verifier()— full OS-trust-store validation — instead of erroring, unlike the builtin driver'sbuild_postgres_tls_connector, which requires an explicit CA file for this mode (platform roots aren't used, since macOS's strict EKU check rejects them) and errors clearly otherwise.build_tls_connectornow returns the same class of error whenverify-cais set with nossl_ca;verify-fullwithoutssl_cacontinues to use the platform verifier unchanged, since that's the documented distinction between the two modes.
rustls-pemfiledependency. It's unmaintained (RUSTSEC-2025-0134 — archived upstream, no CVE) and was our only source of theSecurity auditjob'sunmaintainedwarning below. Its one call site (load_roots_from_peminclient.rs, parsing a user-suppliedssl_cabundle forverify-ca/verify-fullconnections) migrated torustls::pki_types::CertificateDer::pem_slice_iter, the maintained replacement the advisory itself recommends — already in our dependency tree transitively viarustls, so this is a dependency removal, not an addition. Verified against a disposable self-signed-cert Postgres container (verify-caconnects correctly; an invalid PEM file still produces the same clear error) and added 3 unit tests forload_roots_from_pemdirectly, which had zero coverage before this.
Security auditCI job failed on its first scheduled (cron) run withResource not accessible by integrationwhenrustsec/audit-checktried to file a tracking issue for an informationalunmaintainedwarning (rustls-pemfile, RUSTSEC-2025-0134 — no CVE, just an archived-upstream notice). The job'spermissions:block already hadchecks: writefrom a prior fix (CI publishing its pass/fail status), but notissues: write— a separate permission the action only exercises on cron-triggered runs when it has a warning to report, which is why push/PR runs of this same job never surfaced the gap. Added the missingissues: writepermission.
- README's header logo/plus-icon and screenshot-gallery images used
repo-relative
srcpaths (assets/plus.svg,assets/screenshots/*.png). GitHub auto-resolves those against the repo's raw-content base, so they rendered fine there — but the Tabularium registry serves the README's HTML standalone, with no such resolution, so all 6 images 404'd on the plugin's registry page once the README actually synced. Switched to absoluteraw.githubusercontent.comURLs, matching the pattern already used forpostgresql-icon.svgand the.tabulariumscreenshotsarray.
.tabularium'sdescriptionrewritten from the internal-facing "PostgreSQL plugin driver for Tabularis (parity implementation)" to a capability-focused tagline (schemas/tables/views/routines/triggers, EXPLAIN plans, type-aware row editing, DDL generation) matching the style of the strongest sibling plugin descriptions on the Tabularium registry (mongodb,firestore). Synced the GitHub repo's own description field to match, since it was still the old wording.
.tabularium:min_runtime_version: "0.20.0"— per debba's guidance,tabularis0.20.0 is the first release expected to ship the #614/#577 host-side fixes (capability-driven identifier quoting, etc.) this plugin depends on for correct behavior under a non-"postgres"driver id. Older runtimes will be refused rather than silently misbehaving.- README header: a plus icon between the Tabularis and PostgreSQL logos
(
assets/plus.svg, a lucide-style glyph matching the icon settabularis's own frontend uses) to read as "Tabularis + PostgreSQL" at a glance. Self-hosted a copy of the PostgreSQL project's 3-colors logo (assets/postgresql-logo-3colors.png) instead of hotlinkingwiki.postgresql.org, for the same reliability reasonpostgresql-icon.svgis already self-hosted rather than pointed at a third party. .tabularium:screenshotsarray (7 real captures, not mockups — fresh install, database picker, connection form, successful test, saved connection, multi-schema browser, and a live data grid showing a real enum value) plus a matching "Screenshots" section in the README, for the plugin's eventual Tabularium registry submission..tabularium: registry-listing metadata fields —category,tags,license,readme,homepage,documentation_url,support.issues_url,color— needed for the plugin's eventual Tabularium registry submission. These are purely presentational for the registry's plugin-card/detail page; the builtin driver's own definition (tabularis'ssrc/hooks/useDrivers.ts) has none of them, confirming they carry no runtime behavior. No version bump warranted.
- README's release badge showed "no releases or repo not found" because
every published release so far (
v1.0.0-beta.1through.4) is flaggedprerelease: true, and GitHub's/releases/latestAPI — which the unqualifiedimg.shields.io/github/release/...badge queries — excludes prereleases by design. Switched toimg.shields.io/github/v/release/... ?include_prereleases, confirmed renderingv1.0.0-beta.4correctly. - README's installation section and work-in-progress banner were stale:
described a hypothetical "Automatic (via Tabularis)" install path with
no registry to install from yet, and the banner's sign-off checklist
didn't reflect that
tabularisPR #577's checklist is now fully checked (though the PR itself remains open/unmerged). Updated both to describe the actual current state — manual install only, registry submission pending — and added a "From the Tabularium registry" placeholder section matching the pattern used by already-published sibling plugins (tabularis-elasticsearch-plugin,tabularis-duckdb-plugin).
- Plugin had no
icondeclared in.tabularium, so every connection using it fell through to the generic fallback icon in the sidebar, connection list/cards, and the new-connection engine picker (getDriverIcon/getConnectionIconintabularis'ssrc/utils/driverUI.tsxonly render the branded PostgreSQL mark for the literal built-in driver id"postgres"; anything else needs a manifest-declarediconURL). Addedpostgresql-icon.svg— the exact same Slonik elephant path/viewBox already used by the builtin driver'sPostgreSQLIconcomponent (tabularis'ssrc/utils/driverIcons.tsx), extracted as a standalone file and colored to PostgreSQL's brand blue (#336791) — and pointed.tabularium'siconfield at its raw GitHub URL, matching the mongodb/dynamodb sibling plugins' convention. Verified against the live registry schema, which documentsiconas a hosted image URL.
- Keyless-table numeric/temporal
WHEREbinding (port oftabularis#618): updating or deleting a row in a table with no primary key failed withoperator does not exist: numeric = text(SQLSTATE 42883) whenever the identifying column was numeric or temporal, sincebind_pk_valuebound those JSON-string values as plainTEXTwith no coercion. Factored the numeric/temporal coercion already used forSETbinding (bind_pg_string) into two shared functions and routedbind_pk_valuethrough them too. See PR #4 for the full TDD trail and live-database before/after verification.
- Version scheme reset from
0.1.0to1.0.0-beta.1. The actual target is1.0.0— Phase 1 byte-for-byte parity is complete pertabularisPR #577's own "CP-4 Gate Met" status — not an independent0.xdevelopment line, so SemVer's own prerelease mechanism (1.0.0-beta.1→-beta.2→-rc.1→1.0.0) expresses that relationship natively, which a bare0.1.0cannot.
ci.yml: aversion-suggestionjob posts a PR comment suggesting the next tag/version based on the PR title's Conventional Commits type (feat→minor,fix/refactor/perf→patch,docs/style/chore/test/ci/build→no release,!/BREAKING CHANGE:→major) and a requiredprerelease:alpha|beta|rc|stablePR label (missing label fails the job — no default channel is guessed). Purely informational, a precursor to eventually automating tag/release creation: nothing is tagged or released by this job. While the resolved baseline version carries a prerelease suffix matching the label's channel, the suggestion just increments that stage's counter (1.0.0-beta.1→-beta.2) rather than computing a full patch/minor/major bump — there's no shipped stable version yet to protect a SemVer contract against. Re-comments only when the PR's derived classification changes (type + breaking + channel), not on every title edit — tracked via a hidden marker in the comment body — and marks the previous suggestion as outdated via GitHub'sminimizeCommentAPI (same as the web UI's "Hide comment → Outdated") before posting the new one. Also widens the sharedpull_request:trigger'stypes:to includeedited/labeled/unlabeled(previously defaulted toopened/synchronize/reopenedonly, so a title-only edit never even re-ran CI). Checked precedent first: no sibling Tabularis plugin repo ortabularisitself has anything like this; a separate internal repo has a fuller PR-title-driven auto-tag/auto-release pipeline, but porting that whole pipeline was judged too large a behavioral change for this pass.- Two more CI checks:
release.yml: avalidatejob gates the build matrix on the pushed tag matching.tabularium'sversionfield (stripped of thevprefix) — ported from thetabularis-elasticsearch-pluginsibling's pattern. This isn't just convention: the registry's own manifest schema documents this as a hard rule ("the registry rejects ingests whose tag and manifest version disagree"), so this catches the mismatch at tag-push time instead of at registry-submission time.ci.yml: apr-titlejob enforces Conventional Commits PR titles viaamannn/action-semantic-pull-request, triggered on plainpull_request(notpull_request_target) since this repo doesn't need fork-PR support and the plain event avoids the elevated- permission surface entirely.
- CI hardening — deliberately set a higher bar than the sibling plugin
repos and the org's own documented requirements (no sibling runs
cargo audit; only 2 of 11 Rust siblings gate on clippy/fmt at all):.tabulariummanifest validation against the live registry schema via@tabularium/cli validate, catching a malformed manifest automatically (we gotname/idwrong once by hand earlier this session).markdownlint-clias an enforced CI job, not a manually-run habit.- A release-binary smoke test: pipe a trivial
initializeJSON-RPC request into each freshly-built platform binary and assert a valid (non-error) response before it ships in a zip. Skipped forlinux-arm64only, since that leg is cross-compiled and the binary can't execute on the x86_64 build runner without QEMU emulation. cargo audit(viarustsec/audit-check) for supply-chain vulnerabilities, on every push/PR and a weekly schedule (catches CVEs disclosed after merge against unchanged dependencies).tests/live_db.rs: a self-contained live-postgres:16-container integration test (first top-leveltests/dir in this repo — existing tests are all pure unit tests via the.rules/rust.md#4/#5 sibling-file convention). Covers connect, a basic query, an insert, and thestartup_script/connection_stringhandlers found completely uncovered during the security-audit pass — closes the actual biggest gap in this repo's CI: nothing previously verified the binary against a real database automatically. Deliberately NOT the cross-repo 82-test parity suite (that stays a manual/periodic check againsttabularis, per the "Repo Extraction" open question indocs/planning/02-phase-1-plugin-build.md).- Two further hardening ideas —
dependency-review-actionon PRs and SBOM generation viacargo-cyclonedx— were considered and deliberately deferred rather than implemented now; seedocs/planning/ci-hardening-deferred.mdfor the rationale.
-
execute_queryreturnednullfor every PostgreSQL enum column value, even when the database genuinely held a non-null value (issue #7).extract.rs'sextract_value()had no explicit match arm for enum types (custom-OID types), so they fell into the generic catch-all, which triesrow.try_get::<_, String>()—tokio_postgres'sFromSql for Stringenforces known-OID checks and can't decode an arbitrary enum OID, so this always errored and silently nulled out, with no secondary fallback (unliketry_extract<T>'s string retry used elsewhere in the same file). Pre-existing since Sprint 5; the 82-test parity suite never caught it because it only exercises enum writes (insert_record/update_recordbinding) and enum metadata (get_columns'spg_enumlookup), nothing reads an enum value back throughexecute_query. Fixed by decoding the raw label bytes directly as UTF-8 onKind::Enumcolumns, ported from the builtin driver'sextract/enum.rs::extract_or_null— bypassesFromSql's OID enforcement entirely rather than working around it. Genuinely-NULL enum columns still correctly returnnull. Added unit tests for the newEnumLabeldecode path plus atests/live_db.rsregression test covering the full RPC round-trip. -
Flaky unit test:
get_or_create_pool_reuses_cached_entry_for_identical_paramsandcleanup_idle_pools_evicts_pools_with_no_checked_out_connectionsboth read/write the shared process-widePOOLScache, and Rust's test harness runs#[tokio::test]fns concurrently —cleanup_idle_pools's sweep (which iterates every cached pool, not just its own key) could evict the other test's freshly-inserted, still-idle pool mid-assertion. Reproduced locally at roughly 1-in-100 runs; hit for real in CI on the first push after two other CI fixes made this job the last one standing between failure and green. Serialized both tests behind a dedicatedtokio::sync::Mutex(an async mutex, since the guard must span.awaitpoints — astd::sync::Mutexguard held across.awaitfailsclippy::await_holding_lock). -
Security auditCI job was failing on every run — including, unnoticed, the very firstv1.0.0-beta.1/v1.0.0-beta.2release builds — withResource not accessible by integrationwhenrustsec/audit-checktried to publish its Check Run result. The audit itself was passing (0 vulnerabilities found, ourRUSTSEC-2026-0235ignore working correctly); the job had nopermissions:block at all, so it inherited read-only default permissions instead of thechecks: writethe action needs. Added the missingpermissions:block. -
release.ymlnever setprereleaseon published GitHub releases —softprops/action-gh-releasedefaults tofalse, so bothv1.0.0-beta.1andv1.0.0-beta.2published as (and one incorrectly displayed as "Latest") full stable releases despite being betas. Added tag-based auto-detection (-suffix ⇒ prerelease), matchingtabularis's ownrelease.ymlconvention, plusmake_latestwired to the same check so only an actual stable tag can become "Latest". Retroactively corrected both already-published releases viagh release edit --prerelease. -
Three of the new CI jobs above failed on their first real run and were fixed:
Testjob: the existingcargo test(no target filter) tried to runtests/live_db.rstoo, which panics immediately without a running PostgreSQL instance. Scoped tocargo test --lib --bins, leaving the live-DB test to its own dedicatedlive-db-integrationjob.Markdown lintjob:docs/planning/.markdownlint.json's scoped override (MD024/MD060) only applied when markdownlint was invoked from within that directory — the CI step's root-level**/*.mdglob never picked it up. Merged the scoped overrides into the single root.markdownlint.jsoninstead of maintaining two config files. Also added.markdownlintignore(target/) since the glob was incidentally linting vendored third-party docs copied into build output by a dependency's build script.Security auditjob:cargo auditcorrectly found a real advisory, RUSTSEC-2026-0235 (vulnerablerkyv0.7.46) — but it's pulled in only becauserust_decimallists it as an optional dependency behind a feature (rkyv) we never enable; confirmed norkyvsymbols are linked into the release binary. Added a documentedignore:entry for that specific advisory ID, sincecargo auditscans the fullCargo.lockgraph regardless of which optional features are active.
-
main.rsrewritten to a worker-pool architecture (4 workers + a single writer task + a dedicated pool-cleanup task, coordinated via atokio::sync::watchshutdown signal on stdin EOF), matching the sqlserver/dynamodb sibling plugins. A slow query on one connection no longer blocks a concurrentpingor metadata call on another; the host already tolerates out-of-order responses (it correlates by JSON-RPCidvia aHashMap, not arrival order), so this required no protocol change. -
Periodic idle-pool eviction: every 10 minutes,
client::cleanup_idle_pools()drops cached connection pools that currently have no checked-out connections, so a long-running session that has connected to many distinct targets doesn't pin idle TCP connections and pool memory for the plugin's lifetime. Matches the sqlserver/dynamodb sibling plugins' pattern exactly (pool.status().size > pool.status().availableas the keep predicate). Found missing during the same security-audit pass that flagged "pool cleanup on shutdown" — investigation showed the host never sends the plugin ashutdownRPC call at all (it kills the process outright), so that specific checklist wording described something unreachable; comparing sibling plugins surfaced this as the real, exercisable gap instead. Added test-first (TDD): a unit test asserting an idle pool gets evicted, written and confirmed RED (cleanup_idle_poolsdidn't exist) before the function was implemented to GREEN. -
save_blob_to_filenow validatesfile_path(empty, existing-directory, or missing-parent-directory) before spending a DB round-trip on a write that would fail anyway — a clearly attributed-32602error instead of a bare OS error number surfacing after the query already ran. Not a security boundary (the path comes from the frontend's native save dialog), just a fast-fail. The builtin driver's identical gap is untouched; this fix is plugin-only. Found during the security-audit pass. -
startup_scriptsupport: SQL supplied on the connection now runs on every new pooled connection via adeadpool-postgrespost_createhook, with a preflight validation pass so a broken script fails fast with a clearly attributedStartup script failed: ...error instead of a misleading connection error. Matches the builtin driver'srun_postgres_startup_scriptbehavior (src-tauri/src/pool_manager.rs). Found missing during a security-audit pass — no parity test exercises this field, so the 82/82 parity suite didn't catch the gap. -
connection_stringsupport: when present, it's parsed viatokio_postgres::Config::from_strand takes precedence over the discrete host/port/database/username/password fields, matching the README's documented behavior. Previously the field was parsed intoConnectionParamsbut silently never consumed bybuild_pool(). Also found during the security-audit pass. -
Plugin source (
Cargo.toml,Cargo.lock,.tabularium,src/) imported fromTabularisDB/tabularis'splugins/postgres-plugin/at commitad765f3a(82/82 parity tests green per that commit). This is a parallel copy — the in-tree source has not been removed, and the two copies are kept in sync manually pending a later decision to deprecate the in-tree copy. -
docs/planning/: the 8 design documents that shaped this migration (phase docs, both migration-plan variants, and the feature-gap audit feeding Phase 2), copied fromtabularis's.github/planning/. -
src/lib.rsandsrc/bin/test_plugin.rs: extracted the plugin's module tree into a library crate so the justfile'sreplrecipe (a local JSON-RPC REPL) has a real binary to run, matching the oracle/dynamodb sibling plugins' structure. -
Repo scaffolding:
LICENSE(Apache-2.0),.gitignore,.editorconfig,CODEOWNERS,rust-toolchain.toml(pinningrustfmt/clippy),.github/dependabot.yml,justfile(build/test/lint/fmt/dev-install/ demo-db recipes, matching sibling plugin repos). -
CIworkflow (cargo build/test/clippy/fmt --check) andReleaseworkflow (cross-platform binary builds for Linux x86_64/aarch64, macOS x86_64/aarch64, Windows x86_64, published as GitHub release assets alongside.tabularium). -
README.mdandCLAUDE.mddescribing the plugin's purpose, architecture, and current migration status.
.tabularium'snamefield changed frompostgres-plugintopostgresql(and the redundantidfield dropped) to match this repo's own install-path/executable naming and the sibling-plugin convention of a bare engine-name slug. This field is a permanent registry slug once published, so it was fixed before any release.- README's connection config table:
ssl_ca/ssl_cert/ssl_keywere documented as a single group ("If usingverify-ca/verify-full"), but onlyssl_ca(custom CA pinning) is actually implemented — matches the builtin PostgreSQL driver, which also has no client-certificate support (unlike its MySQL driver). Documentation corrected to describe only what the plugin (and builtin) actually do.