pqi

Driver-agnostic interface to the PostgreSQL libpq API

pqi

Hackage Continuous Haddock

A driver-agnostic interface to the PostgreSQL libpq API.

Ecosystem

PackageDescription
pqi (this)The interface: IsConnection class, shared types, and connection-independent helpers
pqi-ffiFFI adapter backed by postgresql-libpq and the C libpq library. Battle-tested, production-safe
pqi-nativePure-Haskell adapter speaking the PostgreSQL wire protocol directly. No C dependency. Experimental
pqi-conformanceReusable 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 pure-Haskell implementation of the PostgreSQL wire protocol, generated with LLM assistance. Experimental. 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. 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 (experimental, 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.