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

See all snapshots pqi-native appears in

MIT licensed and maintained by Nikita Volkov
This version can be pinned in stack with:pqi-native-1.0.1.9@sha256:cd79fcea2fcfea3d0a81c75a46c334e6840482404f9a417ec6b0021121bdb36d,3841

Module documentation for 1.0.1.9

pqi-native

Hackage Continuous Haddock

Status: Alpha. pqi-native is an early implementation of a pure-Haskell transport for pqi. It exists alongside pqi-ffi, the established C-backed adapter, and the two are fully interchangeable: any code written against pqi runs unchanged on either one, so trying pqi-native carries 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, use pqi-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 connectdb reporting the raw Shown Network.Socket.getAddrInfo exception (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). connectFailureMessage now recognizes a resolver failure by Network.Socket.getAddrInfo naming itself in the exception’s ioe_location, and reproduces libpq’s wording using the exception’s ioe_description. Caught by the pqi-conformance spec Pqi.Conformance.Operation.Connectdb.UnresolvableHost.

  • Fixed a handshake-time hard TCP reset (ECONNRESET, as opposed to a clean EOF) reporting the raw Shown IOException (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 as connectionLostMessage) now also recognizes a reset - Network.Socket surfaces ECONNRESET as ioe_type == ResourceVanished rather than System.IO.Error.eofErrorType - not just a clean EOF. Caught by the pqi-conformance spec Pqi.Conformance.Operation.Connectdb.HandshakeReset.

  • Fixed a connection reset while a query response was in flight escaping Pqi.exec/execParams/prepare/execPrepared/describePrepared/describePortal as a raw, uncaught IOException instead of coming back as a classified FatalError result the way libpq’s PQexec does (a lost connection mid-query never throws there; it reports a result with FatalError status and the same “server closed the connection unexpectedly” wording used for a handshake-time loss). Each of those six flows now catches an IOException escaping its read loop and reports it through the same connectionLostMessage classification, also marking the connection ConnectionBad. Caught by the pqi-conformance spec Pqi.Conformance.Operation.Exec.ConnectionLostMidQuery. Found via hasql issue #329.

v1.0.1.8

Fixes

  • Fixed connectFailureMessage (used when the initial connect(2) fails, e.g. a missing Unix-socket directory) hand-rolling a TCP-shaped message - the raw Shown IOException prefixed 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 substring Hasql.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 to unixSocketFailureMessage, extracting the underlying IOException‘s ioe_description (the raw OS strerror text, e.g. "No such file or directory") instead of embedding its whole Shown 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 fixed unixSocketFailureMessage itself 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 the pqi-conformance spec Pqi.Conformance.Operation.Connectdb.MissingUnixSocketDirectory. Found via hasql issue #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 to host still percent-encoded instead of decoded. parseUri’s host/port splitter decoded every other URI component (user, password, dbname, query params) but forwarded the raw, undecoded host bytes. Caught by the pqi-conformance differential spec Pqi.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 IOException instead of coming back as a classified ConnectionBad (#8). establish only wrapped the initial TCP connect in an exception handler; the handshake read that follows it is now caught too and routed through the same tcpFailureMessage/unixSocketFailureMessage wrapper failWith uses, with libpq’s own wording for the EOF case. tcpFailureMessage also stopped adding a redundant “(ip)” parenthetical when the resolved peer IP is identical to the given host. Caught by the pqi-conformance spec added in nikita-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 eventual ParseComplete can be charged to the right command; that entry was only ever popped by a ParseComplete actually arriving. A command whose Parse itself fails - a syntax error, or being silently discarded by the server after a pipeline abort - never gets a ParseComplete, so its entry was leaked. A sendPrepare leaks a True entry, and once a later, unrelated command’s genuine ParseComplete popped that stale True instead of its own, that command terminated immediately as CommandOk instead of collecting its real result, observed as a SELECT returning CommandOk instead of TuplesOk. Every pipelined command now pops its FIFO entry exactly once, whether via its own ParseComplete or, 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 in pqi-conformance 1.0.5.1.

v1.0.1.5

Fixes

  • sendQueryParams, execParams, sendPrepare, prepare, sendQueryPrepared, and execPrepared now reject a parameter list (or, for prepare/sendPrepare, a parameter-type list) longer than 65535 locally, without writing anything to the socket, mirroring libpq’s PQ_QUERY_PARAM_MAX_LIMIT check. The Parse/Bind messages encode their parameter count as a 16-bit field; past the limit fromIntegral silently wrapped it (65536 became 0), producing a malformed message the server rejected with invalid message format and 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 in pqi-conformance 1.0.5.0. Found via hasql issue #326.

v1.0.1.4

Fixes

  • Fixed missing support for connecting over a Unix-domain socket (#6). A host value 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 the host conninfo parameter. A conninfo with no host (or an empty one) now defaults the way libpq itself does: the PGHOST environment variable if set and non-empty, otherwise a Unix-domain socket in /tmp on Unix-like systems (see Pqi.Native.Connection.defaultUnixSocketDir‘s Haddock for how a distribution that compiles its own libpq with a different default, e.g. Fedora’s /run/postgresql, can match it via cabal.project - no source patch needed), or localhost on 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 existing PGUSER lookup): libpq itself reads these with plain getenv(), with no secure_getenv/geteuid-vs-getuid check anywhere in fe-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.getNextResult now runs mask_ed. The connection’s result bookkeeping - the pending-command counter, the separator flag, the ParseComplete origin FIFO - lives in separate IORefs that one logical transition updates in sequence. An interrupt landing between two of those updates left them inconsistent, and an inconsistent pair sends the next getNextResult off to wait for a message the backend has already decided not to send.

    • Transport.receiveFrame no longer runs uninterruptibleMask_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 deadlock System.Timeout.timeout could not break.

    Found via a hang in hasql’s Integration.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.receiveFrame now runs uninterruptibleMask_ed, so a frame is either read to completion or not started, mirroring how pqi-ffi’s safe FFI call into libpq is structurally immune to the same hazard. Caught by the differential coverage in pqi-conformance 1.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 Win32 in place of unix.

  • Fixed a pipelined sendPrepare stealing the preceding sendQueryParamsParseComplete, which produced a spurious CommandOk and 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 pqi 1.1.0.0, which renamed Notify’s fields to notifyRelname/notifyBePid/notifyExtra to match postgresql-libpq

v1.0.0.1

Doc corrections.

v1.0.0.0

Non-breaking

  • Support for the resStatus field of Pqi.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 but user and database, so they reach the server the way libpq’s do. Caught by the new differential coverage in pqi-conformance 0.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 pqi 0.1’s record-of-functions redesign and switch to exporting the adapter value instead of the Connection type.