pqi-native
Native (pure-Haskell) adapter for pqi
https://github.com/nikita-volkov/pqi-native
| Version on this page: | 1.0.1.9 |
| Stackage Nightly 2026-08-19: | 1.0.1.9 |
| Latest on Hackage: | 1.0.1.10 |
pqi-native-1.0.1.9@sha256:cd79fcea2fcfea3d0a81c75a46c334e6840482404f9a417ec6b0021121bdb36d,3841Module documentation for 1.0.1.9
- Pqi
pqi-native
Status: Alpha.
pqi-nativeis an early implementation of a pure-Haskell transport forpqi. It exists alongsidepqi-ffi, the established C-backed adapter, and the two are fully interchangeable: any code written againstpqiruns unchanged on either one, so tryingpqi-nativecarries no lock-in and no rewrite cost. Correctness is checked continuously against a conformance suite (see below), but the adapter hasn’t yet accumulated production mileage. If you need a production-proven transport today, usepqi-ffi. See Making libpq a choice for why this project exists and what tradeoffs that implies.
A pure-Haskell pqi adapter
that speaks the PostgreSQL frontend/backend wire protocol directly - no
dependency on the C libpq library.
pqi-native reimplements the wire protocol handled by the PostgreSQL C
client library, libpq,
from scratch in Haskell. The upstream libpq source is the direct reference for the implementation.
Fidelity goal
The goal is identical output to libpq for every protocol-derived
value - error message strings, notice text, result status, field metadata,
cell data, and all structured error fields. Fidelity is continuously enforced
by pqi-conformance, which
runs every operation on both this adapter and a direct
postgresql-libpq
reference connection against the same database and asserts exact equality.
Status
Alpha. The full Pqi.Connection capability record is implemented and
verified against the postgresql-libpq reference via the conformance
differential suite, but the library hasn’t yet accumulated real-world
production mileage.
Because it implements the same pqi interface as
pqi-ffi, switching between the
two is a one-line change - pass a different Adapter value, nothing else in
your code moves. That makes pqi-native low-risk to evaluate now and easy to
fall back from: adopt it where you want to shed the libpq dependency, and
drop back to pqi-ffi at any time without touching the rest of your
codebase. Read
Making libpq a choice
for the motivation and the tradeoffs of adopting it at this stage.
Authentication: trust, MD5, and SCRAM-SHA-256 are implemented. SCRAM
is verified against a password-auth PostgreSQL 17 container (which defaults to
scram-sha-256).
Transport: plain TCP and Unix-domain sockets. A host value that’s an
absolute path names a socket directory instead of a TCP host, and omitting
host altogether defaults the way libpq itself does: PGHOST if set,
otherwise a Unix-domain socket in /tmp on Unix-like systems, or localhost
on Windows.
Changes
v1.0.1.9
Fixes
-
Fixed a DNS resolution failure during
connectdbreporting the rawShownNetwork.Socket.getAddrInfoexception (e.g.could not connect to server: Network.Socket.getAddrInfo (called with preferred socket type/protocol: ...): does not exist (nodename nor servname provided, or not known)) instead of libpq’s own sentence for it (could not translate host name "..." to address: nodename nor servname provided, or not known).connectFailureMessagenow recognizes a resolver failure byNetwork.Socket.getAddrInfonaming itself in the exception’sioe_location, and reproduces libpq’s wording using the exception’sioe_description. Caught by thepqi-conformancespecPqi.Conformance.Operation.Connectdb.UnresolvableHost. -
Fixed a handshake-time hard TCP reset (
ECONNRESET, as opposed to a clean EOF) reporting the rawShownIOException(e.g.Network.Socket.recvBuf: resource vanished (Connection reset by peer)) instead of libpq’s own “server closed the connection unexpectedly” sentence, which it uses for both a reset and a clean EOF alike.handshakeFailureMessage’s classification (isConnectionLost, factored out asconnectionLostMessage) now also recognizes a reset -Network.SocketsurfacesECONNRESETasioe_type == ResourceVanishedrather thanSystem.IO.Error.eofErrorType- not just a clean EOF. Caught by thepqi-conformancespecPqi.Conformance.Operation.Connectdb.HandshakeReset. -
Fixed a connection reset while a query response was in flight escaping
Pqi.exec/execParams/prepare/execPrepared/describePrepared/describePortalas a raw, uncaughtIOExceptioninstead of coming back as a classifiedFatalErrorresult the way libpq’sPQexecdoes (a lost connection mid-query never throws there; it reports a result withFatalErrorstatus and the same “server closed the connection unexpectedly” wording used for a handshake-time loss). Each of those six flows now catches anIOExceptionescaping its read loop and reports it through the sameconnectionLostMessageclassification, also marking the connectionConnectionBad. Caught by thepqi-conformancespecPqi.Conformance.Operation.Exec.ConnectionLostMidQuery. Found viahasqlissue #329.
v1.0.1.8
Fixes
- Fixed
connectFailureMessage(used when the initialconnect(2)fails, e.g. a missing Unix-socket directory) hand-rolling a TCP-shaped message - the rawShownIOExceptionprefixed with"could not connect to server: "and a(Unix domain socket '<path>')parenthetical appended - instead of reporting the failure the way libpq does. That hand-rolled text retained the literal prefix"could not connect to server: ", which is also the substringHasql.Connection‘s error classifier matches to identify a transient networking failure - so a permanent misconfiguration (missing socket directory) was misclassified as transient. The Unix-socket branch now delegates tounixSocketFailureMessage, extracting the underlyingIOException‘sioe_description(the raw OSstrerrortext, e.g."No such file or directory") instead of embedding its wholeShown form, and appending the same"Is the server running locally and accepting connections on that socket?"hint libpq appends for this failure - matching libpq’s message for this case exactly. Also fixedunixSocketFailureMessageitself quoting the socket path with single quotes ('<path>') instead of libpq’s double quotes ("<path>"), which affects every message it formats, including the handshake-rejection path this Unix-socket branch now shares. Caught by thepqi-conformancespecPqi.Conformance.Operation.Connectdb.MissingUnixSocketDirectory. Found viahasqlissue #329.
v1.0.1.7
Fixes
- Fixed a
postgresql://URI conninfo with a percent-encoded Unix-socket host (e.g.%2ftmp%2f...) being passed through tohoststill percent-encoded instead of decoded.parseUri’shost/portsplitter decoded every other URI component (user,password,dbname, query params) but forwarded the raw, undecoded host bytes. Caught by thepqi-conformancedifferential specPqi.Conformance.Operation.Connectdb.UnixSocketUri.
v1.0.1.6
Fixes
-
Fixed a mid-handshake server rejection (e.g. “sorry, too many clients already”) escaping as an uncaught
IOExceptioninstead of coming back as a classifiedConnectionBad(#8).establishonly wrapped the initial TCP connect in an exception handler; the handshake read that follows it is now caught too and routed through the sametcpFailureMessage/unixSocketFailureMessagewrapperfailWithuses, with libpq’s own wording for the EOF case.tcpFailureMessagealso stopped adding a redundant “(ip)” parenthetical when the resolved peer IP is identical to the given host. Caught by thepqi-conformancespec added innikita-volkov/pqi-conformance@92f5205. -
Fixed a pipelined command’s result getting misattributed to a later, unrelated command after a prior pipeline aborted on a server error (#9). A pipelined command that sends a
Parse(sendQueryParams/sendPrepare) records a FIFO entry so the eventualParseCompletecan be charged to the right command; that entry was only ever popped by aParseCompleteactually arriving. A command whoseParseitself fails - a syntax error, or being silently discarded by the server after a pipeline abort - never gets aParseComplete, so its entry was leaked. AsendPrepareleaks aTrueentry, and once a later, unrelated command’s genuineParseCompletepopped that staleTrueinstead of its own, that command terminated immediately asCommandOkinstead of collecting its real result, observed as aSELECTreturningCommandOkinstead ofTuplesOk. Every pipelined command now pops its FIFO entry exactly once, whether via its ownParseCompleteor, failing that, at its own terminal message (including the synthetic result generated for a command discarded after an abort). Caught by the differential coverage added inpqi-conformance1.0.5.1.
v1.0.1.5
Fixes
sendQueryParams,execParams,sendPrepare,prepare,sendQueryPrepared, andexecPreparednow reject a parameter list (or, forprepare/sendPrepare, a parameter-type list) longer than 65535 locally, without writing anything to the socket, mirroringlibpq’sPQ_QUERY_PARAM_MAX_LIMITcheck. TheParse/Bindmessages encode their parameter count as a 16-bit field; past the limitfromIntegralsilently wrapped it (65536 became 0), producing a malformed message the server rejected withinvalid message formatand leaving the connection desynchronized - inside a pipeline, every command dispatched before the failing one was left with its results undrained. Caught by the differential coverage added inpqi-conformance1.0.5.0. Found viahasqlissue #326.
v1.0.1.4
Fixes
-
Fixed missing support for connecting over a Unix-domain socket (#6). A
hostvalue that looks like an absolute path (e.g.host=/var/run/postgresql) names a socket directory rather than a TCP host, and the connection is made to a.s.PGSQL.<port>unix-domain socket in that directory, mirroring libpq’s rule for thehostconninfo parameter. A conninfo with nohost(or an empty one) now defaults the way libpq itself does: thePGHOSTenvironment variable if set and non-empty, otherwise a Unix-domain socket in/tmpon Unix-like systems (seePqi.Native.Connection.defaultUnixSocketDir‘s Haddock for how a distribution that compiles its ownlibpqwith a different default, e.g. Fedora’s/run/postgresql, can match it viacabal.project- no source patch needed), orlocalhoston Windows, unchanged. Connect-failure and handshake-failure messages now describe the socket path rather than a host/port pair when connecting this way.No privilege-elevation guard is applied to reading
PGHOST(or the existingPGUSERlookup): libpq itself reads these with plaingetenv(), with nosecure_getenv/geteuid-vs-getuidcheck anywhere infe-connect.c- responsibility for scrubbing the environment before opening a database connection is on any setuid/setgid caller, exactly as it is for libpq.
v1.0.1.3
Fixes
-
Fixed an async exception around an aborted pipeline leaving a connection permanently stuck, with no timer able to reclaim it. Two changes, which are only a fix together:
-
Query.getNextResultnow runsmask_ed. The connection’s result bookkeeping - the pending-command counter, the separator flag, theParseCompleteorigin FIFO - lives in separateIORefs that one logical transition updates in sequence. An interrupt landing between two of those updates left them inconsistent, and an inconsistent pair sends the nextgetNextResultoff to wait for a message the backend has already decided not to send. -
Transport.receiveFrameno longer runsuninterruptibleMask_ed. It buffers a whole frame before consuming any of it and takes it out of the buffer in one atomic step, so the framing 1.0.1.2 set out to protect stays intact - but the blocking wait is masked only across moving bytes off the socket, not across waiting for them. The 1.0.1.2 shape made every wait unabandonable, which is what turned the stall above into a deadlockSystem.Timeout.timeoutcould not break.
Found via a hang in
hasql’sIntegration.Sharing.Connection.Use.PipelineAbortedInterruptionCleanup, which wedged only under concurrent load and only on this adapter. -
v1.0.1.2
Fixes
- Fixed an async exception (e.g. from
System.Timeout.timeout) landing mid-read permanently desyncing a connection’s message framing.Transport.receiveFramenow runsuninterruptibleMask_ed, so a frame is either read to completion or not started, mirroring howpqi-ffi’ssafeFFI call intolibpqis structurally immune to the same hazard. Caught by the differential coverage inpqi-conformance1.0.3.0 (#5).
v1.0.1.1
Fixes
-
Builds on Windows: socket I/O and signal handling now branch on the host OS, selecting
Win32in place ofunix. -
Fixed a pipelined
sendPreparestealing the precedingsendQueryParams’ParseComplete, which produced a spuriousCommandOkand shifted every later result by one. ParseComplete messages are now charged to the command that produced them via a per-command FIFO (#3).
v1.0.1.0
Non-breaking
- Picked up
pqi1.1.0.0, which renamedNotify’s fields tonotifyRelname/notifyBePid/notifyExtrato matchpostgresql-libpq
v1.0.0.1
Doc corrections.
v1.0.0.0
Non-breaking
- Support for the
resStatusfield ofPqi.Adapter
v0.2.0.5
Fixes
- Fixed the release process to actually result in publishing of the package.
v0.2.0.4
Fixes
- Fixed the connection closing.
v0.2.0.3
Documentation corrections.
v0.2.0.2
Fixes
- Connection startup now forwards extra conninfo params (e.g.
application_name,options) instead of silently dropping everything butuseranddatabase, so they reach the server the way libpq’s do. Caught by the new differential coverage inpqi-conformance0.1.1.0.
v0.2.0.1
Documentation corrections.
v0.2.0.0
Breaking
- Hid the public sublibs.
v0.1.0.1
Fixes
- Adapted to GHC 8.10: pruned unsupported default-extensions (
ApplicativeDo,DuplicateRecordFields,NoFieldSelectors,OverloadedRecordDot,TemplateHaskell) and rewrote the source accordingly
v0.1.0.0
Breaking
- Migrate to
pqi0.1’s record-of-functions redesign and switch to exporting theadaptervalue instead of theConnectiontype.