PostgREST conformance
How closely the two agree, measured by replaying PostgREST’s own test cases against both servers and diffing the live responses.
| Compared on | All cases1499 cases | Reads (GET, HEAD, OPTIONS)1068 cases | Writes (POST, PATCH, PUT, DELETE)431 cases |
|---|---|---|---|
| Status code | 98.6%1478/1499 | 98.5%1052/1068 | 98.8%426/431 |
| Status and body | 96.7%1450/1499 | 96.3%1028/1068 | 97.9%422/431 |
| …and headers, except Content-Range | 95.2%1427/1499 | 94.6%1010/1068 | 96.8%417/431 |
| Full contract | 94.9%1423/1499 | 94.5%1009/1068 | 96.1%414/431 |
Measured 2026-08-29 against PostgREST v16.1, on Postrust built with admin-ui,compat-key-order and running in compatibility mode. Every figure is recomputed from the run’s per-case output; nothing on this page is typed by hand.
What is compared
Agreement is reported at four strictness levels, because one systemic gap — a single header never emitted — would otherwise sink every case and hide the hundreds that differ in nothing else. The strictest is status, body, and these six headers:
Content-TypeContent-RangeLocationPreference-AppliedAllowWWW-AuthenticateDate, Server and Connection differ between any two servers and say nothing about conformance, so they are not compared. Bodies are compared as parsed JSON, which means object key order does not survive the comparison — but it does survive into a CSV response, which puts its columns in key order.
How it is measured
PostgREST’s spec suite cannot be pointed at another server: it drives the WAI application in-process and imports its own config directly, so there is no HTTP boundary to intercept. Two parts of it are reusable — the fixture database, around 280 tables of plain SQL, and the request literals inside the examples, each spelling out a method, path, headers and body.
So the harness lifts those requests out of the Haskell and replays them over HTTP against both stock PostgREST and Postrust, each on an identically loaded fixture database, and diffs the live responses. The reference implementation is the oracle: no hspec expectation is ever interpreted, which means a mistake in the extractor shows up as a case both servers answer the same way rather than as a false failure.
scripts/conformance/conformance.shWhere the two disagree on purpose
Some cases fail because PostgREST is wrong, or because neither answer is wrong. They are listed so nobody later “fixes” one without deciding to.
PostgREST truncates a select at a stray )
Probed against the reference directly: /clients?select=id)ZZ,nameQQ returns 200 and nameQQ never becomes a column. Everything after the paren is discarded silently. Postrust rejects it, because matching this means reintroducing a bug that was fixed on purpose — select=id, name, billing(address) used to return the id alone.
Two upsert status codes
POST with an empty body, and a PUT that replaced an existing row, return 201 where PostgREST returns 200. The evidence is one case each, against 58 that pass.
Unspecified row order
Two cases return the same rows in a different order, and neither request specifies order=. SQL guarantees nothing there, so both answers are correct and the measurement is over-reporting.
Clock skew on nbf and iat, and none on exp
PostgREST checks all three to the second. Postrust allows thirty seconds on the two that describe a token not yet valid, and none on the one that describes a token withdrawn — forgiving an expiry keeps a session alive past the moment its issuer ended it.
Where the remaining disagreement lives
By spec file, on status and body, worst first.
| Spec | Agreement |
|---|---|
Query/RelatedQueriesSpec.hs | 91.7% (33/36) |
Query/Preferences/MaxAffectedSpec.hs | 92.3% (12/13) |
Query/SpreadQueriesSpec.hs | 92.9% (52/56) |
Query/EmbedDisambiguationSpec.hs | 93.1% (54/58) |
Query/EmbedInnerJoinSpec.hs | 94.7% (54/57) |
Query/QuerySpec.hs | 95% (286/301) |
Query/UpsertSpec.hs | 95% (57/60) |
Query/InsertSpec.hs | 95.1% (78/82) |
Known gaps
The largest is the OpenAPI document. PostgREST serves a 638 KB Swagger 2.0 document at / — 428 paths, 273 definitions, 1035 parameters. Postrust serves OpenAPI 3.0 for its own surface under /admin and does not yet generate PostgREST’s. Bodies compare as exact JSON, so this is all-or-nothing rather than something to land incrementally.
Parse errors are the other: ?or=() and some JSON-path failures answer generically where PostgREST names the offending character. Prefer: tx=rollback is not implemented, and is no longer reported as applied.