pqi

A driver-agnostic interface to the PostgreSQL libpq API.
Ecosystem
| Package |
Description |
| pqi (this) |
The interface: IsConnection class, shared types, and connection-independent helpers |
| pqi-ffi |
FFI adapter backed by postgresql-libpq and the C libpq library. Battle-tested, production-safe |
| pqi-native |
Pure-Haskell adapter speaking the PostgreSQL wire protocol directly. No C dependency. Alpha — interchangeable with pqi-ffi |
| pqi-conformance |
Reusable hspec conformance suite that differentially tests any adapter against postgresql-libpq |
Motivation
Every major Haskell PostgreSQL driver today depends on
postgresql-libpq, a binding to the C libpq library.
This means every user of every driver needs libpq installed — on their
development machine, in CI, in production containers, on cross-compilation
targets. There is no way to opt out.
pqi solves this by separating the interface from the implementation. It
defines a driver-agnostic type class (IsConnection) that mirrors the libpq
API surface, then ships two adapters:
pqi-ffi — a thin wrapper
around postgresql-libpq. Battle-tested, production-safe. The default
choice.
pqi-native — a from-scratch
pure-Haskell implementation of the PostgreSQL wire protocol. Alpha. It
produces byte-identical output to postgresql-libpq for all
protocol-derived values (verified by differential testing), but it has not
yet been exercised in production at scale. Because it implements the same
pqi interface as pqi-ffi, the two are fully interchangeable — switching
between them is a one-line change, so adopting pqi-native now carries no
lock-in. If you adopt it, we want to hear from you.
A driver built against pqi gives its users transport choice without any
changes to the driver itself. Each adapter package exports a single
top-level Adapter value bundling its connection-establishing functions;
the user picks one at connection time:
-- C-backed (safe, requires libpq)
connection <- connectdb Pqi.Ffi.adapter settings
-- Pure Haskell (alpha, no C dependency)
connection <- connectdb Pqi.Native.adapter settings
Testing model
pqi comes accompanied by a comprehensive conformance suite isolated into an implementation-agnostic pqi-conformance package that covers various edge-cases and error conditions and covers most operations with a precondition that they must behave in exactly the same way that postgresql-libpq does.
Interface
pqi reproduces the API surface of the postgresql-libpq
package, but reifies the connection — and the results and cancellation
handles it produces — as plain records of closures instead of a single
concrete type tied to libpq. There is exactly one Connection, one
Result, and one Cancel type in the whole package; each field is an IO
action that an adapter has already closed over its own underlying handle
(a C PGconn pointer, a native socket, etc.). Code written against this
interface runs unchanged on any adapter:
pqi-ffi — a thin
adapter backed by the C libpq library via postgresql-libpq.
pqi-native — a
pure-Haskell adapter that speaks the PostgreSQL wire protocol directly.
The interface mirrors libpq in semantics, not just shape: every compliant
adapter must produce byte-identical output to libpq for all protocol-derived
values. This contract is enforced by
pqi-conformance, which
runs every operation differentially against postgresql-libpq and asserts equality.
This package ships only the interface: the Connection, Result, and
Cancel records, the Adapter type that adapter packages bundle their
connection-establishing functions under, and the shared type vocabulary
(statuses, field codes, formats, OIDs). It does not itself construct any
connections — that's each adapter package's job, exposed as a single
top-level adapter :: Adapter value.
Relationship to postgresql-libpq
The function names, argument order, and semantics mirror
Database.PostgreSQL.LibPQ. The deliberate departures are:
Connection, Result, and Cancel are plain records of closures rather
than a class-parameterised type and its associated types.
- OIDs are a plain
Word32 and row/column/parameter indices are a plain
Int32, instead of the C-specific newtypes of the original.
- There's no
invalidOid constant. It's just 0.
- Ambiguous, rarely-useful helpers (e.g.
resStatus) are omitted, as is
libpqVersion.
unescapeBytea is a field of Adapter rather than a
connection-independent top-level function, since its implementation is
adapter-specific.