packages feed

pqi-conformance (empty) → 0.0.1.0

raw patch · 110 files changed

+4232/−0 lines, 110 filesdep +basedep +bytestringdep +containers

Dependencies added: base, bytestring, containers, directory, hspec, postgresql-libpq, pqi, testcontainers-postgresql, text

Files

+ CHANGELOG.md view
+ LICENSE view
@@ -0,0 +1,22 @@+Copyright (c) 2026, Nikita Volkov++Permission is hereby granted, free of charge, to any person+obtaining a copy of this software and associated documentation+files (the "Software"), to deal in the Software without+restriction, including without limitation the rights to use,+copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the+Software is furnished to do so, subject to the following+conditions:++The above copyright notice and this permission notice shall be+included in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR+OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,80 @@+# pqi-conformance++[![Hackage](https://img.shields.io/hackage/v/pqi-conformance.svg)](https://hackage.haskell.org/package/pqi-conformance)+[![Continuous Haddock](https://img.shields.io/badge/haddock-master-blue)](https://nikita-volkov.github.io/pqi-conformance/)++A reusable [`hspec`](https://hackage.haskell.org/package/hspec) toolkit that+checks any [`pqi`](https://github.com/nikita-volkov/pqi) adapter against the+battle-tested [`postgresql-libpq`](https://hackage.haskell.org/package/postgresql-libpq)+library as a reference.++## Goal: full libpq fidelity++The purpose of this suite is to enforce **byte-identical output to `libpq`**+for every protocol-derived value. This means error message strings, notice+text, result status, field metadata, cell data, and all structured error fields+must all match `libpq`'s output exactly, not just in shape or presence.++The suite runs the same operation on the candidate adapter and on a direct+[`postgresql-libpq`](https://hackage.haskell.org/package/postgresql-libpq)+reference connection (the fidelity reference, which delegates directly to the C+`libpq` library), connected to the same throwaway PostgreSQL container. It then+asserts that the protocol-derived observations are equal.++### Structurally incomparable values++A small number of values are **structurally incomparable across connections**+and are handled differently:++- **`backendPID`** — the OS process ID of the backend. Each connection gets a+  distinct backend, so the two PIDs will never match. The spec asserts `> 0`+  independently for each adapter.+- **`socket`** — the file descriptor of the client socket. Also+  connection-specific. Covered only by its own presence check.+- **`Notify.bePid`** — the PID of the notifying backend. Since each adapter's+  connection has its own backend, cross-adapter comparison would always fail.+  Instead, each adapter asserts independently that `notification.bePid ==+  backendPID connection` (a within-connection assertion that verifies the PID+  field is correctly populated).++These omissions are a structural constraint of the differential testing+approach, not an intentional leniency in the suite. All other values —+including error message text, notice text, and cancel error text — are compared+in full.++## Structure: one module per operation++The suite is organised as **one spec module per API operation**, under+`Pqi.Conformance.Operation.*` — `...Operation.Exec`, `...Operation.ExecParams`,+`...Operation.LoSeek`, `...Operation.Fnumber`, and so on, one for every public+method of `Pqi.IsConnection`, `Pqi.IsResult`, and `Pqi.IsCancel`,+plus the standalone `Pqi.unescapeBytea`. Each module holds the differential+scenarios that exercise that one operation (its happy paths, its edge cases,+and its error paths). Shared scenario fragments live in+`Pqi.Conformance.Scenario`.++## Usage++An adapter's own test suite is a one-liner — it hands `specs` a `Proxy` of its+connection type and `specs` takes care of booting the container and running the+whole battery (every operation, the coverage meta-test, and SCRAM):++```haskell+module Main (main) where++import Data.Proxy (Proxy (..))+import Pqi.Conformance (specs)+import MyAdapter (MyConnection)+import Test.Hspec (hspec)++main :: IO ()+main = hspec (specs (Proxy @MyConnection))+```++`MyConnection` only needs a `Pqi.IsConnection` instance; the candidate and+the `postgresql-libpq` reference are both driven through that interface.++## Contributing++Contributions extending the suite are very welcome. The more thoroughly we+cover the operations, the more confidence we can have in the adapters.
+ pqi-conformance.cabal view
@@ -0,0 +1,213 @@+cabal-version: 3.0+name: pqi-conformance+version: 0.0.1.0+category: Database, PostgreSQL, Testing+synopsis: Differential conformance tests for pqi adapters+description:+  A reusable @hspec@ toolkit that checks any @pqi@ 'Pqi.IsConnection'+  adapter against the FFI reference adapter. It boots a throwaway PostgreSQL+  container (via @testcontainers@), runs the same operation on the candidate and+  the FFI connection, and asserts that the protocol-derived observations match.++  .++  Each adapter's own test suite reuses these spec builders, wiring itself in as+  the candidate.++homepage: https://github.com/nikita-volkov/pqi-conformance+bug-reports: https://github.com/nikita-volkov/pqi-conformance/issues+author: Nikita Volkov <nikita.y.volkov@mail.ru>+maintainer: Nikita Volkov <nikita.y.volkov@mail.ru>+copyright: (c) 2026, Nikita Volkov+license: MIT+license-file: LICENSE+extra-doc-files:+  CHANGELOG.md+  LICENSE+  README.md++source-repository head+  type: git+  location: https://github.com/nikita-volkov/pqi-conformance++common base+  default-language: Haskell2010+  default-extensions:+    ApplicativeDo+    BangPatterns+    BinaryLiterals+    BlockArguments+    ConstraintKinds+    DataKinds+    DefaultSignatures+    DeriveDataTypeable+    DeriveFoldable+    DeriveFunctor+    DeriveTraversable+    DerivingStrategies+    DerivingVia+    DuplicateRecordFields+    EmptyDataDecls+    ExistentialQuantification+    FlexibleContexts+    FlexibleInstances+    FunctionalDependencies+    GADTs+    GeneralizedNewtypeDeriving+    LambdaCase+    LiberalTypeSynonyms+    MagicHash+    MultiParamTypeClasses+    MultiWayIf+    NamedFieldPuns+    NoFieldSelectors+    NoImplicitPrelude+    NoMonomorphismRestriction+    NumericUnderscores+    OverloadedRecordDot+    OverloadedStrings+    ParallelListComp+    PatternGuards+    QuasiQuotes+    RankNTypes+    RecordWildCards+    ScopedTypeVariables+    StandaloneDeriving+    StrictData+    TemplateHaskell+    TupleSections+    TypeApplications+    TypeFamilies+    TypeOperators+    UnboxedTuples+    ViewPatterns++common test+  import: base+  ghc-options:+    -threaded+    -with-rtsopts=-N++library+  import: base+  hs-source-dirs: src/library+  exposed-modules:+    Pqi.Conformance++  other-modules:+    Pqi.Conformance.Harness+    Pqi.Conformance.Observation+    Pqi.Conformance.Operation.BackendPID+    Pqi.Conformance.Operation.Cancel+    Pqi.Conformance.Operation.CancelCleanup+    Pqi.Conformance.Operation.ClientEncoding+    Pqi.Conformance.Operation.CmdStatus+    Pqi.Conformance.Operation.CmdTuples+    Pqi.Conformance.Operation.Connectdb+    Pqi.Conformance.Operation.ConnectionNeedsPassword+    Pqi.Conformance.Operation.ConnectionUsedPassword+    Pqi.Conformance.Operation.ConnectPoll+    Pqi.Conformance.Operation.ConnectStart+    Pqi.Conformance.Operation.ConsumeInput+    Pqi.Conformance.Operation.Db+    Pqi.Conformance.Operation.DescribePortal+    Pqi.Conformance.Operation.DescribePrepared+    Pqi.Conformance.Operation.DisableNoticeReporting+    Pqi.Conformance.Operation.EnableNoticeReporting+    Pqi.Conformance.Operation.EnterPipelineMode+    Pqi.Conformance.Operation.ErrorMessage+    Pqi.Conformance.Operation.EscapeByteaConn+    Pqi.Conformance.Operation.EscapeIdentifier+    Pqi.Conformance.Operation.EscapeStringConn+    Pqi.Conformance.Operation.Exec+    Pqi.Conformance.Operation.ExecParams+    Pqi.Conformance.Operation.ExecPrepared+    Pqi.Conformance.Operation.ExitPipelineMode+    Pqi.Conformance.Operation.Fformat+    Pqi.Conformance.Operation.Finish+    Pqi.Conformance.Operation.Flush+    Pqi.Conformance.Operation.Fmod+    Pqi.Conformance.Operation.Fname+    Pqi.Conformance.Operation.Fnumber+    Pqi.Conformance.Operation.Fsize+    Pqi.Conformance.Operation.Ftable+    Pqi.Conformance.Operation.Ftablecol+    Pqi.Conformance.Operation.Ftype+    Pqi.Conformance.Operation.GetCancel+    Pqi.Conformance.Operation.GetCopyData+    Pqi.Conformance.Operation.Getisnull+    Pqi.Conformance.Operation.Getlength+    Pqi.Conformance.Operation.GetNotice+    Pqi.Conformance.Operation.GetResult+    Pqi.Conformance.Operation.Getvalue+    Pqi.Conformance.Operation.GetvalueCopy+    Pqi.Conformance.Operation.Host+    Pqi.Conformance.Operation.IsBusy+    Pqi.Conformance.Operation.Isnonblocking+    Pqi.Conformance.Operation.IsNullConnection+    Pqi.Conformance.Operation.LoClose+    Pqi.Conformance.Operation.LoCreat+    Pqi.Conformance.Operation.LoCreate+    Pqi.Conformance.Operation.LoExport+    Pqi.Conformance.Operation.LoImport+    Pqi.Conformance.Operation.LoImportWithOid+    Pqi.Conformance.Operation.LoOpen+    Pqi.Conformance.Operation.LoRead+    Pqi.Conformance.Operation.LoSeek+    Pqi.Conformance.Operation.LoTell+    Pqi.Conformance.Operation.LoTruncate+    Pqi.Conformance.Operation.LoUnlink+    Pqi.Conformance.Operation.LoWrite+    Pqi.Conformance.Operation.NewNullConnection+    Pqi.Conformance.Operation.Nfields+    Pqi.Conformance.Operation.Notifies+    Pqi.Conformance.Operation.Nparams+    Pqi.Conformance.Operation.Ntuples+    Pqi.Conformance.Operation.Options+    Pqi.Conformance.Operation.ParameterStatus+    Pqi.Conformance.Operation.Paramtype+    Pqi.Conformance.Operation.Pass+    Pqi.Conformance.Operation.PipelineStatus+    Pqi.Conformance.Operation.PipelineSync+    Pqi.Conformance.Operation.Port+    Pqi.Conformance.Operation.Prepare+    Pqi.Conformance.Operation.ProtocolVersion+    Pqi.Conformance.Operation.PutCopyData+    Pqi.Conformance.Operation.PutCopyEnd+    Pqi.Conformance.Operation.Reset+    Pqi.Conformance.Operation.ResetPoll+    Pqi.Conformance.Operation.ResetStart+    Pqi.Conformance.Operation.ResultErrorField+    Pqi.Conformance.Operation.ResultErrorMessage+    Pqi.Conformance.Operation.ResultStatus+    Pqi.Conformance.Operation.SendDescribePortal+    Pqi.Conformance.Operation.SendDescribePrepared+    Pqi.Conformance.Operation.SendFlushRequest+    Pqi.Conformance.Operation.SendPrepare+    Pqi.Conformance.Operation.SendQuery+    Pqi.Conformance.Operation.SendQueryParams+    Pqi.Conformance.Operation.SendQueryPrepared+    Pqi.Conformance.Operation.ServerVersion+    Pqi.Conformance.Operation.SetClientEncoding+    Pqi.Conformance.Operation.SetErrorVerbosity+    Pqi.Conformance.Operation.Setnonblocking+    Pqi.Conformance.Operation.SetSingleRowMode+    Pqi.Conformance.Operation.Socket+    Pqi.Conformance.Operation.Status+    Pqi.Conformance.Operation.TransactionStatus+    Pqi.Conformance.Operation.UnsafeFreeResult+    Pqi.Conformance.Operation.User+    Pqi.Conformance.Prelude+    Pqi.Conformance.Reference+    Pqi.Conformance.Scenario++  build-depends:+    base >=4.11 && <5,+    bytestring >=0.10 && <0.13,+    containers >=0.6 && <0.9,+    directory >=1.3 && <1.4,+    hspec >=2.11 && <2.12,+    postgresql-libpq >=0.11 && <0.12,+    pqi ^>=0,+    testcontainers-postgresql ^>=0.2,+    text >=1.2 && <3,
+ src/library/Pqi/Conformance.hs view
@@ -0,0 +1,240 @@+-- | A reusable differential-testing toolkit for @pqi@ adapters.+--+-- An adapter's test suite calls 'specs' with a @'Proxy' \@MyConnection@. The+-- battery runs the same operation on the candidate and on the FFI reference+-- and asserts that the protocol-derived observations match.+--+-- The throwaway PostgreSQL container lifecycle is baked into 'specs', so+-- adapter test suites only need @hspec (specs (Proxy \@MyConnection))@; they+-- don't have to know about @testcontainers@ at all. The SCRAM-SHA-256+-- authentication spec is part of the @connectdb@ group and boots its own+-- password-auth container.+module Pqi.Conformance+  ( specs,+  )+where++import Pqi (IsConnection)+import Pqi.Conformance.Harness+import qualified Pqi.Conformance.Operation.BackendPID as BackendPID+import qualified Pqi.Conformance.Operation.Cancel as Cancel+import qualified Pqi.Conformance.Operation.CancelCleanup as CancelCleanup+import qualified Pqi.Conformance.Operation.ClientEncoding as ClientEncoding+import qualified Pqi.Conformance.Operation.CmdStatus as CmdStatus+import qualified Pqi.Conformance.Operation.CmdTuples as CmdTuples+import qualified Pqi.Conformance.Operation.ConnectPoll as ConnectPoll+import qualified Pqi.Conformance.Operation.ConnectStart as ConnectStart+import qualified Pqi.Conformance.Operation.Connectdb as Connectdb+import qualified Pqi.Conformance.Operation.ConnectionNeedsPassword as ConnectionNeedsPassword+import qualified Pqi.Conformance.Operation.ConnectionUsedPassword as ConnectionUsedPassword+import qualified Pqi.Conformance.Operation.ConsumeInput as ConsumeInput+import qualified Pqi.Conformance.Operation.Db as Db+import qualified Pqi.Conformance.Operation.DescribePortal as DescribePortal+import qualified Pqi.Conformance.Operation.DescribePrepared as DescribePrepared+import qualified Pqi.Conformance.Operation.DisableNoticeReporting as DisableNoticeReporting+import qualified Pqi.Conformance.Operation.EnableNoticeReporting as EnableNoticeReporting+import qualified Pqi.Conformance.Operation.EnterPipelineMode as EnterPipelineMode+import qualified Pqi.Conformance.Operation.ErrorMessage as ErrorMessage+import qualified Pqi.Conformance.Operation.EscapeByteaConn as EscapeByteaConn+import qualified Pqi.Conformance.Operation.EscapeIdentifier as EscapeIdentifier+import qualified Pqi.Conformance.Operation.EscapeStringConn as EscapeStringConn+import qualified Pqi.Conformance.Operation.Exec as Exec+import qualified Pqi.Conformance.Operation.ExecParams as ExecParams+import qualified Pqi.Conformance.Operation.ExecPrepared as ExecPrepared+import qualified Pqi.Conformance.Operation.ExitPipelineMode as ExitPipelineMode+import qualified Pqi.Conformance.Operation.Fformat as Fformat+import qualified Pqi.Conformance.Operation.Finish as Finish+import qualified Pqi.Conformance.Operation.Flush as Flush+import qualified Pqi.Conformance.Operation.Fmod as Fmod+import qualified Pqi.Conformance.Operation.Fname as Fname+import qualified Pqi.Conformance.Operation.Fnumber as Fnumber+import qualified Pqi.Conformance.Operation.Fsize as Fsize+import qualified Pqi.Conformance.Operation.Ftable as Ftable+import qualified Pqi.Conformance.Operation.Ftablecol as Ftablecol+import qualified Pqi.Conformance.Operation.Ftype as Ftype+import qualified Pqi.Conformance.Operation.GetCancel as GetCancel+import qualified Pqi.Conformance.Operation.GetCopyData as GetCopyData+import qualified Pqi.Conformance.Operation.GetNotice as GetNotice+import qualified Pqi.Conformance.Operation.GetResult as GetResult+import qualified Pqi.Conformance.Operation.Getisnull as Getisnull+import qualified Pqi.Conformance.Operation.Getlength as Getlength+import qualified Pqi.Conformance.Operation.Getvalue as Getvalue+import qualified Pqi.Conformance.Operation.GetvalueCopy as GetvalueCopy+import qualified Pqi.Conformance.Operation.Host as Host+import qualified Pqi.Conformance.Operation.IsBusy as IsBusy+import qualified Pqi.Conformance.Operation.IsNullConnection as IsNullConnection+import qualified Pqi.Conformance.Operation.Isnonblocking as Isnonblocking+import qualified Pqi.Conformance.Operation.LoClose as LoClose+import qualified Pqi.Conformance.Operation.LoCreat as LoCreat+import qualified Pqi.Conformance.Operation.LoCreate as LoCreate+import qualified Pqi.Conformance.Operation.LoExport as LoExport+import qualified Pqi.Conformance.Operation.LoImport as LoImport+import qualified Pqi.Conformance.Operation.LoImportWithOid as LoImportWithOid+import qualified Pqi.Conformance.Operation.LoOpen as LoOpen+import qualified Pqi.Conformance.Operation.LoRead as LoRead+import qualified Pqi.Conformance.Operation.LoSeek as LoSeek+import qualified Pqi.Conformance.Operation.LoTell as LoTell+import qualified Pqi.Conformance.Operation.LoTruncate as LoTruncate+import qualified Pqi.Conformance.Operation.LoUnlink as LoUnlink+import qualified Pqi.Conformance.Operation.LoWrite as LoWrite+import qualified Pqi.Conformance.Operation.NewNullConnection as NewNullConnection+import qualified Pqi.Conformance.Operation.Nfields as Nfields+import qualified Pqi.Conformance.Operation.Notifies as Notifies+import qualified Pqi.Conformance.Operation.Nparams as Nparams+import qualified Pqi.Conformance.Operation.Ntuples as Ntuples+import qualified Pqi.Conformance.Operation.Options as Options+import qualified Pqi.Conformance.Operation.ParameterStatus as ParameterStatus+import qualified Pqi.Conformance.Operation.Paramtype as Paramtype+import qualified Pqi.Conformance.Operation.Pass as Pass+import qualified Pqi.Conformance.Operation.PipelineStatus as PipelineStatus+import qualified Pqi.Conformance.Operation.PipelineSync as PipelineSync+import qualified Pqi.Conformance.Operation.Port as Port+import qualified Pqi.Conformance.Operation.Prepare as Prepare+import qualified Pqi.Conformance.Operation.ProtocolVersion as ProtocolVersion+import qualified Pqi.Conformance.Operation.PutCopyData as PutCopyData+import qualified Pqi.Conformance.Operation.PutCopyEnd as PutCopyEnd+import qualified Pqi.Conformance.Operation.Reset as Reset+import qualified Pqi.Conformance.Operation.ResetPoll as ResetPoll+import qualified Pqi.Conformance.Operation.ResetStart as ResetStart+import qualified Pqi.Conformance.Operation.ResultErrorField as ResultErrorField+import qualified Pqi.Conformance.Operation.ResultErrorMessage as ResultErrorMessage+import qualified Pqi.Conformance.Operation.ResultStatus as ResultStatus+import qualified Pqi.Conformance.Operation.SendDescribePortal as SendDescribePortal+import qualified Pqi.Conformance.Operation.SendDescribePrepared as SendDescribePrepared+import qualified Pqi.Conformance.Operation.SendFlushRequest as SendFlushRequest+import qualified Pqi.Conformance.Operation.SendPrepare as SendPrepare+import qualified Pqi.Conformance.Operation.SendQuery as SendQuery+import qualified Pqi.Conformance.Operation.SendQueryParams as SendQueryParams+import qualified Pqi.Conformance.Operation.SendQueryPrepared as SendQueryPrepared+import qualified Pqi.Conformance.Operation.ServerVersion as ServerVersion+import qualified Pqi.Conformance.Operation.SetClientEncoding as SetClientEncoding+import qualified Pqi.Conformance.Operation.SetErrorVerbosity as SetErrorVerbosity+import qualified Pqi.Conformance.Operation.SetSingleRowMode as SetSingleRowMode+import qualified Pqi.Conformance.Operation.Setnonblocking as Setnonblocking+import qualified Pqi.Conformance.Operation.Socket as Socket+import qualified Pqi.Conformance.Operation.Status as Status+import qualified Pqi.Conformance.Operation.TransactionStatus as TransactionStatus+import qualified Pqi.Conformance.Operation.UnsafeFreeResult as UnsafeFreeResult+import qualified Pqi.Conformance.Operation.User as User+import Pqi.Conformance.Prelude+import Test.Hspec++-- | The full conformance battery: every per-operation spec under one shared+-- trust-auth container, plus SCRAM-SHA-256 authentication (which boots its own+-- password-auth container). Every operation spec is differential against the+-- FFI reference.+specs :: (IsConnection c) => Proxy c -> Spec+specs proxy = parallel do+  containerHook do+    -- Connection lifecycle+    Connectdb.spec proxy+    ConnectStart.spec proxy+    ConnectPoll.spec proxy+    NewNullConnection.spec proxy+    IsNullConnection.spec proxy+    Finish.spec proxy+    Reset.spec proxy+    ResetStart.spec proxy+    ResetPoll.spec proxy+    -- Connection information accessors+    Db.spec proxy+    User.spec proxy+    Pass.spec proxy+    Host.spec proxy+    Port.spec proxy+    Options.spec proxy+    Status.spec proxy+    TransactionStatus.spec proxy+    ParameterStatus.spec proxy+    ProtocolVersion.spec proxy+    ServerVersion.spec proxy+    ErrorMessage.spec proxy+    Socket.spec proxy+    BackendPID.spec proxy+    ConnectionNeedsPassword.spec proxy+    ConnectionUsedPassword.spec proxy+    -- Querying+    Exec.spec proxy+    ExecParams.spec proxy+    Prepare.spec proxy+    ExecPrepared.spec proxy+    DescribePrepared.spec proxy+    DescribePortal.spec proxy+    -- Escaping+    EscapeStringConn.spec proxy+    EscapeByteaConn.spec proxy+    EscapeIdentifier.spec proxy+    -- Asynchronous command processing+    SendQuery.spec proxy+    SendQueryParams.spec proxy+    SendPrepare.spec proxy+    SendQueryPrepared.spec proxy+    SendDescribePrepared.spec proxy+    SendDescribePortal.spec proxy+    GetResult.spec proxy+    ConsumeInput.spec proxy+    IsBusy.spec proxy+    Setnonblocking.spec proxy+    Isnonblocking.spec proxy+    SetSingleRowMode.spec proxy+    Flush.spec proxy+    -- Pipelining+    PipelineStatus.spec proxy+    EnterPipelineMode.spec proxy+    ExitPipelineMode.spec proxy+    PipelineSync.spec proxy+    SendFlushRequest.spec proxy+    -- Cancellation+    GetCancel.spec proxy+    Cancel.spec proxy+    CancelCleanup.spec proxy+    -- Notifications and notices+    Notifies.spec proxy+    DisableNoticeReporting.spec proxy+    EnableNoticeReporting.spec proxy+    GetNotice.spec proxy+    -- Copy sub-protocol+    PutCopyData.spec proxy+    PutCopyEnd.spec proxy+    GetCopyData.spec proxy+    -- Large objects+    LoCreat.spec proxy+    LoCreate.spec proxy+    LoImport.spec proxy+    LoImportWithOid.spec proxy+    LoExport.spec proxy+    LoOpen.spec proxy+    LoWrite.spec proxy+    LoRead.spec proxy+    LoSeek.spec proxy+    LoTell.spec proxy+    LoTruncate.spec proxy+    LoClose.spec proxy+    LoUnlink.spec proxy+    -- Connection control+    ClientEncoding.spec proxy+    SetClientEncoding.spec proxy+    SetErrorVerbosity.spec proxy+    -- Result inspection+    ResultStatus.spec proxy+    ResultErrorMessage.spec proxy+    ResultErrorField.spec proxy+    UnsafeFreeResult.spec proxy+    Ntuples.spec proxy+    Nfields.spec proxy+    Fname.spec proxy+    Fnumber.spec proxy+    Ftable.spec proxy+    Ftablecol.spec proxy+    Fformat.spec proxy+    Ftype.spec proxy+    Fmod.spec proxy+    Fsize.spec proxy+    Getvalue.spec proxy+    GetvalueCopy.spec proxy+    Getisnull.spec proxy+    Getlength.spec proxy+    Nparams.spec proxy+    Paramtype.spec proxy+    CmdStatus.spec proxy+    CmdTuples.spec proxy
+ src/library/Pqi/Conformance/Harness.hs view
@@ -0,0 +1,99 @@+-- | The differential-testing harness: a throwaway PostgreSQL container and+-- the comparison combinators.+module Pqi.Conformance.Harness+  ( -- * Container+    containerHook,++    -- * Comparison+    differential,+    differentialConnect,+  )+where++import Control.Exception (bracket, bracket_)+import qualified Data.ByteString.Char8 as ByteString.Char8+import qualified Data.Text as Text+import Data.Unique (hashUnique, newUnique)+import Pqi (IsConnection (..))+import Pqi.Conformance.Prelude+import Pqi.Conformance.Reference (Reference)+import Test.Hspec+import qualified TestcontainersPostgresql as TcPg++-- | Boot a single trust-auth PostgreSQL container for the whole spec tree and+-- hand each example a ready conninfo string.+containerHook :: SpecWith ByteString -> Spec+containerHook = aroundAll (TcPg.run config) . aroundWith withConninfo+  where+    config =+      TcPg.Config+        { TcPg.tagName = "postgres:17",+          TcPg.forwardLogs = False,+          TcPg.auth = TcPg.TrustAuth+        }+    withConninfo action (host, port) = action (conninfo host port)+    conninfo host port =+      ByteString.Char8.pack+        ( "host="+            <> Text.unpack host+            <> " port="+            <> show port+            <> " user=postgres dbname=postgres"+        )++-- | Run a scenario on both the candidate and the FFI reference (each on its own+-- fresh connection to the same database) and assert that the two observations+-- are equal.+--+-- Each call creates a fresh database for the scenario and drops it afterwards,+-- so tests are isolated even when the container is shared.+differential ::+  forall c a.+  (Eq a, Show a, IsConnection c, HasCallStack) =>+  Proxy c ->+  ByteString ->+  (forall c'. (IsConnection c') => c' -> IO a) ->+  Expectation+differential _ adminConninfo scenario =+  withTestDb adminConninfo \testConninfo -> do+    candidate <- bracket (connectdb testConninfo :: IO c) finish scenario+    reference <- bracket (connectdb testConninfo :: IO Reference) finish scenario+    candidate `shouldBe` reference++-- Create a uniquely named database, run the action against it, and drop it on+-- exit (including on exception). The admin conninfo must point to an existing+-- database (e.g. @dbname=postgres@); the test conninfo appended with the new+-- database name is passed to the action. In libpq keyword=value strings the+-- last occurrence of a keyword wins, so appending @dbname=…@ overrides any+-- earlier value.+withTestDb :: ByteString -> (ByteString -> IO a) -> IO a+withTestDb adminConninfo action = do+  u <- newUnique+  let dbName = ByteString.Char8.pack ("lq" <> show (abs (hashUnique u)))+  bracket_+    (adminExec adminConninfo ("create database " <> dbName))+    (adminExec adminConninfo ("drop database " <> dbName))+    (action (adminConninfo <> " dbname=" <> dbName))++adminExec :: ByteString -> ByteString -> IO ()+adminExec conninfo sql = do+  conn <- connectdb conninfo :: IO Reference+  _ <- exec conn sql+  finish conn++-- | Like 'differential', but for scenarios that exercise connection+-- establishment itself ('Pqi.connectdb' on a broken conninfo,+-- 'Pqi.connectStart', 'Pqi.newNullConnection', ...): instead of an+-- opened connection the scenario receives the conninfo and the candidate's+-- connection type (via 'Proxy'), and manages any connections it opens itself.+differentialConnect ::+  forall c a.+  (Eq a, Show a, IsConnection c, HasCallStack) =>+  Proxy c ->+  ByteString ->+  (forall c'. (IsConnection c', HasCallStack) => Proxy c' -> ByteString -> IO a) ->+  Expectation+differentialConnect proxy conninfo scenario = do+  candidate <- scenario proxy conninfo+  reference <- scenario (Proxy :: Proxy Reference) conninfo+  candidate `shouldBe` reference
+ src/library/Pqi/Conformance/Observation.hs view
@@ -0,0 +1,144 @@+-- | Plain, comparable snapshots of a connection or result.+--+-- The candidate adapter and the FFI reference produce values of /different/+-- types (@'Pqi.ResultOf' candidate@ vs @'Pqi.ResultOf'+-- 'Pqi.Ffi.Connection'@), so they cannot be compared directly. Instead we+-- project each into one of these driver-independent records and compare those.+--+-- Only protocol-derived information is captured: both adapters parse the same+-- wire bytes, so these fields genuinely agree. Per-connection identity values+-- (@backendPID@, @socket@) are structurally incomparable across connections+-- and are omitted. All result fields — including the flat error message text+-- and all structured error fields — are captured in full and compared+-- byte-identically.+module Pqi.Conformance.Observation+  ( ResultObservation (..),+    FieldObservation (..),+    CellObservation (..),+    observeResult,+    ConnectionObservation (..),+    observeConnection,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import qualified Pqi as Lq+import Pqi.Conformance.Prelude++-- | A snapshot of an entire result: its status, structured error report,+-- shape, per-field metadata, and every cell.+data ResultObservation = ResultObservation+  { status :: Lq.ExecStatus,+    -- | Every structured field of the error report, keyed by 'Lq.FieldCode'.+    -- All of them are carried by the wire error response.+    errorFields :: [(Lq.FieldCode, Maybe ByteString)],+    -- | The flat formatted error message, byte-identical to libpq's+    -- @PQresultErrorMessage@ at DEFAULT verbosity.+    errorMessage :: Maybe ByteString,+    ntuples :: Int32,+    nfields :: Int32,+    nparams :: Int32,+    paramTypes :: [Word32],+    fields :: [FieldObservation],+    rows :: [[CellObservation]],+    cmdStatus :: Maybe ByteString,+    cmdTuples :: Maybe ByteString+  }+  deriving stock (Eq, Show)++-- | A snapshot of one result column's metadata.+data FieldObservation = FieldObservation+  { name :: Maybe ByteString,+    typeOid :: Word32,+    modifier :: Int,+    size :: Int,+    format :: Lq.Format,+    tableOid :: Word32,+    tableColumn :: Int32+  }+  deriving stock (Eq, Show)++-- | A snapshot of one cell.+data CellObservation = CellObservation+  { value :: Maybe ByteString,+    isNull :: Bool,+    length :: Int+  }+  deriving stock (Eq, Show)++-- | Project a result into a 'ResultObservation'.+observeResult :: (IsResult r) => r -> IO ResultObservation+observeResult result = do+  status <- Lq.resultStatus result+  errorFields <-+    traverse+      (\code -> (,) code <$> Lq.resultErrorField result code)+      [minBound .. maxBound]+  errorMessage <- Lq.resultErrorMessage result+  ntuples <- Lq.ntuples result+  nfields <- Lq.nfields result+  nparams <- Lq.nparams result+  paramTypes <- traverse (Lq.paramtype result) [0 .. nparams - 1]+  fields <- traverse (observeField result) [0 .. nfields - 1]+  rows <- traverse (\row -> traverse (observeCell result row) [0 .. nfields - 1]) [0 .. ntuples - 1]+  cmdStatus <- Lq.cmdStatus result+  cmdTuples <- Lq.cmdTuples result+  pure ResultObservation {..}++observeField :: (IsResult r) => r -> Int32 -> IO FieldObservation+observeField result column = do+  name <- Lq.fname result column+  typeOid <- Lq.ftype result column+  modifier <- Lq.fmod result column+  size <- Lq.fsize result column+  format <- Lq.fformat result column+  tableOid <- Lq.ftable result column+  tableColumn <- Lq.ftablecol result column+  pure FieldObservation {..}++observeCell :: (IsResult r) => r -> Int32 -> Int32 -> IO CellObservation+observeCell result row column = do+  value <- Lq.getvalue result row column+  isNull <- Lq.getisnull result row column+  length <- Lq.getlength result row column+  pure CellObservation {..}++-- | A snapshot of the comparable portion of a connection's state. The+-- candidate and the reference open their connections from the same conninfo+-- string, so the conninfo-derived identity accessors agree as well.+data ConnectionObservation = ConnectionObservation+  { status :: Lq.ConnStatus,+    transactionStatus :: Lq.TransactionStatus,+    serverVersion :: Int,+    serverVersionParam :: Maybe ByteString,+    protocolVersion :: Int,+    db :: Maybe ByteString,+    user :: Maybe ByteString,+    pass :: Maybe ByteString,+    host :: Maybe ByteString,+    port :: Maybe ByteString,+    options :: Maybe ByteString,+    connectionNeedsPassword :: Bool,+    connectionUsedPassword :: Bool,+    isNull :: Bool+  }+  deriving stock (Eq, Show)++-- | Project a connection into a 'ConnectionObservation'.+observeConnection :: (IsConnection c) => c -> IO ConnectionObservation+observeConnection connection = do+  status <- Lq.status connection+  transactionStatus <- Lq.transactionStatus connection+  serverVersion <- Lq.serverVersion connection+  serverVersionParam <- Lq.parameterStatus connection "server_version"+  protocolVersion <- Lq.protocolVersion connection+  db <- Lq.db connection+  user <- Lq.user connection+  pass <- Lq.pass connection+  host <- Lq.host connection+  port <- Lq.port connection+  options <- Lq.options connection+  connectionNeedsPassword <- Lq.connectionNeedsPassword connection+  connectionUsedPassword <- Lq.connectionUsedPassword connection+  let isNull = Lq.isNullConnection connection+  pure ConnectionObservation {..}
+ src/library/Pqi/Conformance/Operation/BackendPID.hs view
@@ -0,0 +1,20 @@+-- | Coverage for 'Pqi.backendPID': the backend process ID.+--+-- The PID differs between the candidate's and the reference's backends, so+-- only its positivity is compared.+module Pqi.Conformance.Operation.BackendPID+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "backendPID" do+    it "is positive on an open connection" \conninfo ->+      differential proxy conninfo \connection ->+        (> 0) <$> backendPID connection
+ src/library/Pqi/Conformance/Operation/Cancel.hs view
@@ -0,0 +1,46 @@+-- | Coverage for 'Pqi.cancel': requesting cancellation through a handle,+-- both on an idle connection and against a running query (which then fails+-- with SQLSTATE @57014@).+--+-- The full 'Either' value is compared — not just success\/failure — so that+-- any divergence in error text is caught.+module Pqi.Conformance.Operation.Cancel+  ( spec,+  )+where++import Pqi (IsCancel (..), IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults, execScenario)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "cancel" do+    it "succeeds on an idle connection" \conninfo ->+      differential proxy conninfo \connection -> do+        handle <- getCancel connection+        for handle cancel++    it "fails a running query with 57014" \conninfo ->+      differential proxy conninfo \connection -> do+        sent <- sendQuery connection "select pg_sleep(10)"+        threadDelay 100000+        handle <- getCancel connection+        cancelled <- for handle cancel+        results <- drainResults connection+        usable <- execScenario "select 1" connection+        pure (sent, cancelled, results, usable)++    it "leaves the connection usable after cancelling a short-running query" \conninfo ->+      differential proxy conninfo \connection -> do+        outcomes <- replicateM 3 do+          sent <- sendQuery connection "select pg_sleep(0.1)"+          threadDelay 50000+          handle <- getCancel connection+          cancelled <- for handle cancel+          results <- drainResults connection+          usable <- execScenario "select 1" connection+          pure (sent, cancelled, results, usable)+        pure outcomes
+ src/library/Pqi/Conformance/Operation/CancelCleanup.hs view
@@ -0,0 +1,84 @@+-- | Regression coverage for the stale-cancel bug that corrupts a connection+-- after a pipelined query completes.+--+-- Root cause: in pqi-native, 'Pqi.getResult' returns 'Nothing' (the pipeline+-- separator) __before__ reading the trailing 'ReadyForQuery' message, leaving+-- @asyncPending = True@.  If 'Pqi.cancel' is called while @asyncPending@ is+-- still @True@ — as 'Hasql.Comms.Session.cleanUpAfterInterruption' does after+-- draining results — a cancel request is sent to the server even though the+-- query has already finished.  The server receives the signal, sets+-- @QueryCancelPending@, and the __next__ command (e.g. @ABORT@) is cancelled+-- with SQLSTATE @57014@, leaving the connection unusable.+module Pqi.Conformance.Operation.CancelCleanup+  ( spec,+  )+where++import Control.Exception (bracket)+import Pqi (ExecStatus (..), IsCancel (..), IsConnection (..), IsResult (..))+import qualified Pqi as Lq+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: forall c. (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "cancel cleanup" do+    -- Reproduces the state that hasql's cleanUpAfterInterruption reaches after+    -- a timeout fires mid-pipeline:+    --+    --   1. drainResults reads CommandComplete → separator, exits loop.+    --      In pqi-native asyncPending is still True here (ReadyForQuery not+    --      yet consumed); in libpq the ReadyForQuery has already been+    --      processed internally.+    --   2. cancel is called.  pqi-native sends a cancel because+    --      asyncPending=True; the query has long since finished so this is+    --      a stale cancel.+    --   3. drainResults reads the remaining ReadyForQuery.+    --   4. The connection re-enters serial mode.+    --   5. exec runs a follow-up command — it must NOT be cancelled by the+    --      stale signal that arrived in step 2.+    it "does not corrupt subsequent commands when cancel is called after pipeline results are drained" \conninfo ->+      bracket (connectdb conninfo :: IO c) finish \connection -> do+        -- Enter pipeline mode and dispatch a fast query.+        _ <- enterPipelineMode connection+        _ <- sendQueryParams connection "select 1" [] Lq.Text+        _ <- pipelineSync connection++        -- Wait for the server to process the query so both messages+        -- (CommandComplete + ReadyForQuery) are already in the socket by the+        -- time we start reading.+        threadDelay 10_000 -- 10 ms++        -- Drain the command result then the pipeline separator (Nothing).+        -- After this loop exits, asyncPending=True in pqi-native because+        -- ReadyForQuery has not been read yet.+        let drainAll = do+              mr <- getResult connection+              case mr of+                Nothing -> pure ()+                Just _ -> drainAll+        drainAll++        -- Send cancel.  Because asyncPending=True in pqi-native, a cancel+        -- request is dispatched to the server despite the query being done.+        handle <- getCancel connection+        for_ handle cancel++        -- Give the stale cancel enough time to reach the server and set+        -- QueryCancelPending before the next command arrives.+        threadDelay 10_000 -- 10 ms++        -- Read the ReadyForQuery that was still pending.+        drainAll++        -- Exit pipeline mode (sends an implicit Sync in the reference impl).+        _ <- exitPipelineMode connection++        -- A follow-up command must succeed; 57014 here means the stale cancel+        -- corrupted the connection.+        mResult <- exec connection "select 1"+        case mResult of+          Nothing -> expectationFailure "exec returned no result after pipeline cleanup"+          Just (result :: ResultOf c) -> do+            status <- resultStatus result+            status `shouldBe` TuplesOk
+ src/library/Pqi/Conformance/Operation/ClientEncoding.hs view
@@ -0,0 +1,27 @@+-- | Coverage for 'Pqi.clientEncoding': reporting the current client+-- encoding, which tracks 'Pqi.setClientEncoding' and governs how result+-- cells are re-encoded.+module Pqi.Conformance.Operation.ClientEncoding+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (execScenario)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "clientEncoding" do+    it "round-trips and governs result re-encoding" \conninfo ->+      differential proxy conninfo \connection -> do+        initial <- clientEncoding connection+        setOk <- setClientEncoding connection "LATIN1"+        switched <- clientEncoding connection+        reported <- parameterStatus connection "client_encoding"+        latinCell <- execScenario "select chr(233) as e" connection+        restoreOk <- setClientEncoding connection "UTF8"+        utfCell <- execScenario "select chr(233) as e" connection+        pure (initial, setOk, switched, reported, latinCell, restoreOk, utfCell)
+ src/library/Pqi/Conformance/Operation/CmdStatus.hs view
@@ -0,0 +1,24 @@+-- | Coverage for 'Pqi.cmdStatus': the command status tag (e.g.+-- @\"INSERT 0 2\"@) for each kind of command.+module Pqi.Conformance.Operation.CmdStatus+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "cmdStatus" do+    it "reports the command tag for each command" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "create temporary table conformance_cmd_status (id int4, label text)"+        let tagOf sql = exec connection sql >>= traverse cmdStatus+        insert <- tagOf "insert into conformance_cmd_status values (1, 'a'), (2, 'b')"+        update <- tagOf "update conformance_cmd_status set label = 'c' where id = 1"+        delete <- tagOf "delete from conformance_cmd_status where id = 2"+        select <- tagOf "select * from conformance_cmd_status"+        pure (insert, update, delete, select)
+ src/library/Pqi/Conformance/Operation/CmdTuples.hs view
@@ -0,0 +1,25 @@+-- | Coverage for 'Pqi.cmdTuples': the affected-row count (as text) for+-- each kind of command.+module Pqi.Conformance.Operation.CmdTuples+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "cmdTuples" do+    it "reports the affected-row count for each command" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "create temporary table conformance_cmd_tuples (id int4, label text)"+        let countOf sql = exec connection sql >>= traverse cmdTuples+        insert <- countOf "insert into conformance_cmd_tuples values (1, 'a'), (2, 'b')"+        update <- countOf "update conformance_cmd_tuples set label = 'c' where id = 1"+        delete <- countOf "delete from conformance_cmd_tuples where id = 2"+        select <- countOf "select * from conformance_cmd_tuples"+        ddl <- countOf "create temporary table conformance_cmd_tuples_2 (id int4)"+        pure (insert, update, delete, select, ddl)
+ src/library/Pqi/Conformance/Operation/ConnectPoll.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.connectPoll': driving an asynchronous connection+-- attempt forward until it reports a terminal polling status.+module Pqi.Conformance.Operation.ConnectPoll+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (pollUntilDone)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "connectPoll" do+    it "reaches a terminal polling status and a ready connection" \conninfo ->+      differentialConnect proxy conninfo \(_ :: Proxy c) conninfo' -> do+        connection <- connectStart conninfo' :: IO c+        terminal <- pollUntilDone (connectPoll connection)+        connStatus <- status connection+        finish connection+        pure (terminal, connStatus)
+ src/library/Pqi/Conformance/Operation/ConnectStart.hs view
@@ -0,0 +1,24 @@+-- | Coverage for 'Pqi.connectStart': beginning an asynchronous connection+-- attempt that is then driven to readiness with 'Pqi.connectPoll'.+module Pqi.Conformance.Operation.ConnectStart+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (pollUntilDone)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "connectStart" do+    it "begins an asynchronous connection that polls to readiness" \conninfo ->+      differentialConnect proxy conninfo \(_ :: Proxy c) conninfo' -> do+        connection <- connectStart conninfo' :: IO c+        polled <- pollUntilDone (connectPoll connection)+        observation <- observeConnection connection+        finish connection+        pure (polled, observation)
+ src/library/Pqi/Conformance/Operation/Connectdb.hs view
@@ -0,0 +1,95 @@+-- | Coverage for 'Pqi.connectdb': a blocking connection from a conninfo+-- string, including the rejected-conninfo error paths and SCRAM-SHA-256+-- authentication.+module Pqi.Conformance.Operation.Connectdb+  ( spec,+  )+where++import Control.Exception (bracket)+import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Char8 as ByteString.Char8+import qualified Data.Map.Strict as Map+import qualified Data.Text as Text+import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude+import Pqi.Conformance.Reference (Reference)+import Test.Hspec+import qualified TestcontainersPostgresql as TcPg++spec :: forall c. (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy = do+  describe "connectdb" do+    it "opens a usable connection" \conninfo ->+      differential proxy conninfo observeConnection++    it "accepts a URI-format conninfo" \conninfo ->+      differentialConnect proxy conninfo \(_ :: Proxy d) conninfo' -> do+        connection <- connectdb (kvToUri conninfo') :: IO d+        s <- status connection+        finish connection+        pure s++    it "rejects an unknown database" \conninfo ->+      differentialConnect proxy conninfo \(_ :: Proxy d) conninfo' -> do+        connection <- connectdb (conninfo' <> " dbname=pqi_no_such_db") :: IO d+        observation <- status connection+        nullness <- pure (isNullConnection connection)+        finish connection+        pure (observation, nullness)++    it "rejects an unknown user" \conninfo ->+      differentialConnect proxy conninfo \(_ :: Proxy d) conninfo' -> do+        connection <- connectdb (conninfo' <> " user=pqi_no_such_user") :: IO d+        observation <- status connection+        finish connection+        pure observation++  describe "SCRAM-SHA-256 authentication" do+    it "the candidate authenticates and queries like the FFI reference" \_ ->+      let scramConfig =+            TcPg.Config+              { TcPg.tagName = "postgres:17",+                TcPg.forwardLogs = False,+                TcPg.auth = TcPg.CredentialsAuth "scram" "secret"+              }++          scramScenario :: forall c. (IsConnection c) => c -> IO (Maybe ResultObservation)+          scramScenario connection = exec connection "select 1 as scram_works" >>= traverse observeResult+       in TcPg.run scramConfig \(host, port) -> do+            let conninfo =+                  ByteString.Char8.pack+                    ( "host="+                        <> Text.unpack host+                        <> " port="+                        <> show port+                        <> " user=scram password=secret dbname=scram"+                    )+            native <- bracket (connectdb conninfo) finish (scramScenario @c)+            reference <- bracket (connectdb conninfo) finish (scramScenario @Reference)+            native `shouldBe` reference++-- | Convert a @key=value@ conninfo to a @postgresql://@ URI, for testing+-- that adapters accept URI-format connection strings.+kvToUri :: ByteString -> ByteString+kvToUri raw =+  "postgresql://"+    <> user+    <> (if ByteString.null password then "" else ":" <> password)+    <> (if ByteString.null user && ByteString.null password then "" else "@")+    <> host+    <> (if ByteString.null port then "" else ":" <> port)+    <> (if ByteString.null dbname then "" else "/" <> dbname)+  where+    pairs = Map.fromList $ mapMaybe toPair (ByteString.Char8.words raw)+    get k = Map.findWithDefault "" k pairs+    toPair token = case ByteString.Char8.break (== '=') token of+      (k, v) | not (ByteString.null v) -> Just (k, ByteString.drop 1 v)+      _ -> Nothing+    host = get "host"+    port = get "port"+    user = get "user"+    password = get "password"+    dbname = get "dbname"
+ src/library/Pqi/Conformance/Operation/ConnectionNeedsPassword.hs view
@@ -0,0 +1,17 @@+-- | Coverage for 'Pqi.connectionNeedsPassword': whether authentication+-- needed a password that was unavailable (False under trust auth).+module Pqi.Conformance.Operation.ConnectionNeedsPassword+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "connectionNeedsPassword" do+    it "reports whether a password was needed" \conninfo ->+      differential proxy conninfo connectionNeedsPassword
+ src/library/Pqi/Conformance/Operation/ConnectionUsedPassword.hs view
@@ -0,0 +1,17 @@+-- | Coverage for 'Pqi.connectionUsedPassword': whether authentication+-- used a password (False under trust auth).+module Pqi.Conformance.Operation.ConnectionUsedPassword+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "connectionUsedPassword" do+    it "reports whether a password was used" \conninfo ->+      differential proxy conninfo connectionUsedPassword
+ src/library/Pqi/Conformance/Operation/ConsumeInput.hs view
@@ -0,0 +1,28 @@+-- | Coverage for 'Pqi.consumeInput': reading server input into the+-- driver's buffer so that 'Pqi.isBusy' can settle before collecting+-- results.+module Pqi.Conformance.Operation.ConsumeInput+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "consumeInput" do+    it "drives result collection together with isBusy" \conninfo ->+      differential proxy conninfo \connection -> do+        sent <- sendQuery connection "select 42"+        let settle (0 :: Int) = pure False+            settle n = do+              consumed <- consumeInput connection+              busy <- isBusy connection+              if busy then threadDelay 1000 >> settle (n - 1) else pure consumed+        consumed <- settle 10000+        results <- drainResults connection+        pure (sent, consumed, results)
+ src/library/Pqi/Conformance/Operation/Db.hs view
@@ -0,0 +1,16 @@+-- | Coverage for 'Pqi.db': the database name of the connection.+module Pqi.Conformance.Operation.Db+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "db" do+    it "reports the database name from the conninfo" \conninfo ->+      differential proxy conninfo db
+ src/library/Pqi/Conformance/Operation/DescribePortal.hs view
@@ -0,0 +1,28 @@+-- | Coverage for 'Pqi.describePortal': describing a declared cursor's+-- portal and the unknown-portal error path.+module Pqi.Conformance.Operation.DescribePortal+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "describePortal" do+    it "describes a declared cursor" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "begin"+        _ <-+          exec+            connection+            "declare conformance_cursor cursor for select 1 :: int4 as n, 'x' :: text as t"+        describePortal connection "conformance_cursor" >>= traverse observeResult++    it "rejects an unknown portal" \conninfo ->+      differential proxy conninfo \connection ->+        describePortal connection "conformance_no_portal" >>= traverse observeResult
+ src/library/Pqi/Conformance/Operation/DescribePrepared.hs view
@@ -0,0 +1,31 @@+-- | Coverage for 'Pqi.describePrepared': reporting a prepared statement's+-- parameter types, including explicitly typed parameters and the+-- unknown-statement error path.+module Pqi.Conformance.Operation.DescribePrepared+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (int8Oid)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "describePrepared" do+    it "reports parameter types" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- prepare connection "conformance_desc" "select $1 :: int4, $2 :: text" Nothing+        describePrepared connection "conformance_desc" >>= traverse observeResult++    it "reports explicit parameter types" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- prepare connection "conformance_typed" "select $1" (Just [int8Oid])+        describePrepared connection "conformance_typed" >>= traverse observeResult++    it "rejects an unknown statement" \conninfo ->+      differential proxy conninfo \connection ->+        describePrepared connection "conformance_missing" >>= traverse observeResult
+ src/library/Pqi/Conformance/Operation/DisableNoticeReporting.hs view
@@ -0,0 +1,27 @@+-- | Coverage for 'Pqi.disableNoticeReporting': turning off notice+-- accumulation again so subsequently raised notices are not retained.+module Pqi.Conformance.Operation.DisableNoticeReporting+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "disableNoticeReporting" do+    it "stops retaining notices once disabled" \conninfo ->+      differential proxy conninfo \connection -> do+        enableNoticeReporting connection+        whileEnabled <- raiseNoticeAndCollect connection+        disableNoticeReporting connection+        whileDisabled <- raiseNoticeAndCollect connection+        pure (whileEnabled, whileDisabled)++raiseNoticeAndCollect :: (IsConnection c) => c -> IO Bool+raiseNoticeAndCollect connection = do+  _ <- exec connection "do $$ begin raise notice 'conformance notice'; end $$"+  isJust <$> getNotice connection
+ src/library/Pqi/Conformance/Operation/EnableNoticeReporting.hs view
@@ -0,0 +1,27 @@+-- | Coverage for 'Pqi.enableNoticeReporting': turning on notice+-- accumulation so that a raised notice becomes retrievable via+-- 'Pqi.getNotice'.+module Pqi.Conformance.Operation.EnableNoticeReporting+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "enableNoticeReporting" do+    it "makes a raised notice retrievable" \conninfo ->+      differential proxy conninfo \connection -> do+        beforeEnable <- raiseNoticeAndCollect connection+        enableNoticeReporting connection+        afterEnable <- raiseNoticeAndCollect connection+        pure (beforeEnable, afterEnable)++raiseNoticeAndCollect :: (IsConnection c) => c -> IO Bool+raiseNoticeAndCollect connection = do+  _ <- exec connection "do $$ begin raise notice 'conformance notice'; end $$"+  isJust <$> getNotice connection
+ src/library/Pqi/Conformance/Operation/EnterPipelineMode.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.enterPipelineMode': switching a connection into+-- pipeline mode (idempotently).+module Pqi.Conformance.Operation.EnterPipelineMode+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "enterPipelineMode" do+    it "enters pipeline mode and is idempotent" \conninfo ->+      differential proxy conninfo \connection -> do+        before <- pipelineStatus connection+        entered <- enterPipelineMode connection+        enteredAgain <- enterPipelineMode connection+        while <- pipelineStatus connection+        _ <- exitPipelineMode connection+        pure (before, entered, enteredAgain, while)
+ src/library/Pqi/Conformance/Operation/ErrorMessage.hs view
@@ -0,0 +1,62 @@+-- | Coverage for 'Pqi.errorMessage': the connection-level error string.+--+-- The goal is byte-identical output to libpq's @PQerrorMessage@ in all+-- documented scenarios. Error strings are compared in full — not just for+-- presence — so formatting bugs are caught. Scenarios are chosen to avoid+-- statement-position fields (@'P'@), which depend on the client-stored query+-- text and cannot be reproduced from wire fields alone.+--+-- The one structurally incomparable value is the null-connection sentinel+-- @\"connection pointer is NULL\\n\"@: it is hardcoded in libpq rather than+-- derived from a wire response, but both adapters must return exactly that+-- string.+module Pqi.Conformance.Operation.ErrorMessage+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "errorMessage" do+    it "is empty on a healthy connection" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "select 1"+        errorMessage connection++    it "is populated after a failed exec and cleared by a subsequent success" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "do $$ begin raise exception 'conformance error'; end $$"+        afterFail <- errorMessage connection+        _ <- exec connection "select 1"+        afterSuccess <- errorMessage connection+        pure (afterFail, afterSuccess)++    it "is populated after a failed getResult and cleared by a subsequent success" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- sendQuery connection "do $$ begin raise exception 'conformance error'; end $$"+        _ <- drainResults connection+        afterFail <- errorMessage connection+        _ <- sendQuery connection "select 1"+        _ <- drainResults connection+        afterSuccess <- errorMessage connection+        pure (afterFail, afterSuccess)++    it "is populated after a connection failure" \conninfo ->+      differentialConnect proxy conninfo \(_ :: Proxy c) conninfo' -> do+        conn <- connectdb (conninfo' <> " user=pqi_no_such_user") :: IO c+        msg <- errorMessage conn+        finish conn+        pure msg++    it "is the null-connection sentinel on a null connection" \conninfo ->+      differentialConnect proxy conninfo \(_ :: Proxy c) _ -> do+        conn <- newNullConnection :: IO c+        msg <- errorMessage conn+        finish conn+        pure msg
+ src/library/Pqi/Conformance/Operation/EscapeByteaConn.hs view
@@ -0,0 +1,26 @@+-- | Coverage for 'Pqi.escapeByteaConn': escaping binary data for a+-- @bytea@ literal across the full byte range.+module Pqi.Conformance.Operation.EscapeByteaConn+  ( spec,+  )+where++import qualified Data.ByteString as ByteString+import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "escapeByteaConn" do+    it "escapes a range of binary inputs" \conninfo ->+      differential proxy conninfo \connection ->+        traverse (escapeByteaConn connection) byteaCases+  where+    byteaCases =+      [ "",+        "plain",+        "\NUL\1\2\3",+        ByteString.pack [0 .. 255]+      ]
+ src/library/Pqi/Conformance/Operation/EscapeIdentifier.hs view
@@ -0,0 +1,34 @@+-- | Coverage for 'Pqi.escapeIdentifier': escaping SQL identifiers+-- (including the surrounding quotes) and a round-trip through a query.+module Pqi.Conformance.Operation.EscapeIdentifier+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (execScenario)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "escapeIdentifier" do+    it "escapes a range of identifiers" \conninfo ->+      differential proxy conninfo \connection ->+        traverse (escapeIdentifier connection) identifierCases++    it "produces identifiers that round-trip through a query" \conninfo ->+      differential proxy conninfo \connection -> do+        escaped <- escapeIdentifier connection "Wéird \"column\" name"+        for escaped \identifier ->+          execScenario ("select 1 as " <> identifier) connection+  where+    identifierCases =+      [ "plain",+        "MixedCase",+        "with space",+        "with\"quote",+        "héllo",+        "select"+      ]
+ src/library/Pqi/Conformance/Operation/EscapeStringConn.hs view
@@ -0,0 +1,39 @@+-- | Coverage for 'Pqi.escapeStringConn': escaping strings for SQL+-- literals, the invalid-encoding error path, and a round-trip through a query.+module Pqi.Conformance.Operation.EscapeStringConn+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (execScenario)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "escapeStringConn" do+    it "escapes a range of strings" \conninfo ->+      differential proxy conninfo \connection ->+        traverse (escapeStringConn connection) stringCases++    it "rejects invalid encoding" \conninfo ->+      differential proxy conninfo \connection ->+        escapeStringConn connection "\255\254"++    it "produces literals that round-trip through a query" \conninfo ->+      differential proxy conninfo \connection -> do+        escaped <- escapeStringConn connection "it's \\ tricky\nstuff"+        for escaped \literal ->+          execScenario ("select '" <> literal <> "' :: text") connection+  where+    stringCases =+      [ "",+        "plain",+        "it's",+        "back\\slash",+        "newline\nand\ttab",+        "héllo🐘",+        "double''single"+      ]
+ src/library/Pqi/Conformance/Operation/Exec.hs view
@@ -0,0 +1,36 @@+-- | Coverage for 'Pqi.exec': the simple-query protocol over a range of+-- result shapes, command tags, and degenerate inputs.+module Pqi.Conformance.Operation.Exec+  ( spec,+  )+where++import Pqi (IsConnection)+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "exec" do+    let forCase title sql =+          it title \conninfo -> differential proxy conninfo (execScenario sql)+    forCase "select literal" "select 1"+    forCase "multi-row, multi-column" "select i, i * 2 from generate_series (1, 3) as i"+    forCase "nulls and text" "select null :: int4, 'hello' :: text, true"+    forCase "no rows" "select 1 where false"+    forCase "zero columns" "select"+    forCase "empty query" ""+    forCase "semicolon only" ";"+    forCase "comment only" "-- nothing to see here"+    forCase "whitespace only" "   "+    forCase "multiple statements take the last result" "select 1; select 2, 3"+    forCase "non-ASCII text" "select 'héllo🐘' as greeting"+    forCase "bytea hex output" "select '\\xdeadbeef' :: bytea"+    forCase "large value" "select repeat('x', 100000)"+    forCase "many rows" "select i from generate_series (1, 1000) as i"+    forCase "show command" "show server_version"+    forCase "set command" "set application_name to 'conformance-exec'"+    forCase "DDL command tag" "create temporary table conformance_exec (id int4)"+    forCase "DDL with a notice" "drop table if exists pqi_conformance_absent"
+ src/library/Pqi/Conformance/Operation/ExecParams.hs view
@@ -0,0 +1,85 @@+-- | Coverage for 'Pqi.execParams': the extended query protocol over+-- parameter formats, result formats, inferred types, nulls, and its error+-- paths.+module Pqi.Conformance.Operation.ExecParams+  ( spec,+  )+where++import Pqi (IsConnection (..))+import qualified Pqi as Lq+import Pqi.Conformance.Harness+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "execParams" do+    it "text result format" \conninfo ->+      differential proxy conninfo (paramsScenario Lq.Text)+    it "binary result format" \conninfo ->+      differential proxy conninfo (paramsScenario Lq.Binary)+    it "null parameter" \conninfo ->+      differential proxy conninfo (observed "select $1 :: int4 as maybe_value" [Nothing] Lq.Text)+    it "no parameters" \conninfo ->+      differential proxy conninfo (observed "select 'none' :: text" [] Lq.Text)+    it "binary parameter" \conninfo ->+      differential proxy conninfo+        $ observed "select $1 :: int4 * 2" [Just (int4Oid, "\NUL\NUL\NUL*", Lq.Binary)] Lq.Text+    it "binary bytea round-trip" \conninfo ->+      differential proxy conninfo+        $ observed "select $1 :: bytea" [Just (byteaOid, "\NUL\1\2\255", Lq.Binary)] Lq.Binary+    it "inferred parameter type" \conninfo ->+      differential proxy conninfo+        $ observed "select $1 :: int4 + 1" [Just (0, "41", Lq.Text)] Lq.Text+    it "empty string is not null" \conninfo ->+      differential proxy conninfo+        $ observed "select $1 :: text, length ($1 :: text)" [Just (textOid, "", Lq.Text)] Lq.Text+    it "many mixed parameters" \conninfo ->+      differential proxy conninfo+        $ observed+          "select $1 :: int8, $2 :: float8, $3 :: bool, $4 :: text, $5 :: int4, $6 :: text"+          [ Just (int8Oid, "9000000000000000000", Lq.Text),+            Just (float8Oid, "2.5", Lq.Text),+            Just (boolOid, "t", Lq.Text),+            Nothing,+            Just (int4Oid, "-1", Lq.Text),+            Just (textOid, "héllo", Lq.Text)+          ]+          Lq.Text+    it "DML with parameters" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "create temporary table conformance_exec_params (id int4)"+        insert <-+          observed+            "insert into conformance_exec_params values ($1), ($2)"+            [Just (int4Oid, "1", Lq.Text), Just (int4Oid, "2", Lq.Text)]+            Lq.Text+            connection+        check <-+          observed+            "select count(*) from conformance_exec_params where id <= $1"+            [Just (int4Oid, "2", Lq.Text)]+            Lq.Text+            connection+        pure (insert, check)+    it "too few parameters" \conninfo ->+      differential proxy conninfo+        $ observed "select $1 :: int4 + $2 :: int4" [Just (int4Oid, "1", Lq.Text)] Lq.Text+    it "malformed parameter value" \conninfo ->+      differential proxy conninfo+        $ observed "select $1 :: int4" [Just (int4Oid, "not-a-number", Lq.Text)] Lq.Text+    it "multiple statements are rejected" \conninfo ->+      differential proxy conninfo (observed "select 1; select 2" [] Lq.Text)++paramsScenario :: (IsConnection c) => Lq.Format -> c -> IO (Maybe ResultObservation)+paramsScenario resultFormat =+  observed+    "select $1 :: int4 + $2 :: int4 as sum, $3 :: text as label"+    [ Just (int4Oid, "40", Lq.Text),+      Just (int4Oid, "2", Lq.Text),+      Just (textOid, "hi", Lq.Text)+    ]+    resultFormat
+ src/library/Pqi/Conformance/Operation/ExecPrepared.hs view
@@ -0,0 +1,46 @@+-- | Coverage for 'Pqi.execPrepared': executing a prepared statement over+-- named and unnamed statements, parameter formats, and the unknown-statement+-- error path.+module Pqi.Conformance.Operation.ExecPrepared+  ( spec,+  )+where++import Pqi (IsConnection (..))+import qualified Pqi as Lq+import Pqi.Conformance.Harness+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "execPrepared" do+    it "executes a prepared statement" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- prepare connection "conformance_stmt" "select $1 :: text as a, $2 :: int4 as b" Nothing+        execPrepared connection "conformance_stmt" [Just ("hello", Lq.Text), Just ("7", Lq.Text)] Lq.Text+          >>= traverse observeResult++    it "executes the unnamed prepared statement" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- prepare connection "" "select $1 :: int4 + 1" Nothing+        execPrepared connection "" [Just ("41", Lq.Text)] Lq.Text+          >>= traverse observeResult++    it "executes a zero-parameter statement" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- prepare connection "conformance_no_params" "select 42" Nothing+        execPrepared connection "conformance_no_params" [] Lq.Text+          >>= traverse observeResult++    it "binds null and binary parameters with a binary result" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- prepare connection "conformance_binary" "select $1 :: text, $2 :: bytea" Nothing+        execPrepared connection "conformance_binary" [Nothing, Just ("\NUL\1\255", Lq.Binary)] Lq.Binary+          >>= traverse observeResult++    it "rejects an unknown statement" \conninfo ->+      differential proxy conninfo \connection ->+        execPrepared connection "conformance_missing" [] Lq.Text+          >>= traverse observeResult
+ src/library/Pqi/Conformance/Operation/ExitPipelineMode.hs view
@@ -0,0 +1,117 @@+-- | Coverage for 'Pqi.exitPipelineMode': leaving pipeline mode, which+-- fails while work is still pending and succeeds once the pipeline is drained.+module Pqi.Conformance.Operation.ExitPipelineMode+  ( spec,+  )+where++import Pqi (IsConnection (..))+import qualified Pqi as Lq+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults, execScenario, float8Oid, takeCommandResults, takeResult)+import System.Timeout (timeout)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "exitPipelineMode" do+    it "returns the connection to its non-pipeline status" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- enterPipelineMode connection+        exited <- exitPipelineMode connection+        after <- pipelineStatus connection+        pure (exited, after)++    it "fails with work pending and succeeds once drained" \conninfo ->+      differential proxy conninfo \connection -> do+        entered <- enterPipelineMode connection+        sent <- sendQueryParams connection "select 1" [] Lq.Text+        prematureExit <- exitPipelineMode connection+        synced <- pipelineSync connection+        results <- takeCommandResults connection+        syncResult <- takeResult connection+        exited <- exitPipelineMode connection+        pure (entered, sent, prematureExit, synced, results, syncResult, exited)++    -- Reproduces the cleanup sequence that hasql's cleanUpAfterInterruption ++    -- leavePipeline performs after a timeout mid-pipeline.  Two prepared+    -- statements are used so that pendingParses is tracked (matching the+    -- real-world scenario).  The slow statement is cancelled before its result+    -- is consumed, then the connection is restored via the exact drain/sync+    -- sequence that hasql uses.+    it "recovers after mid-pipeline cancel (mirrors cleanUpAfterInterruption)" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- enterPipelineMode connection+        _ <- sendPrepare connection "s1" "select 1" Nothing+        _ <- sendQueryPrepared connection "s1" [] Lq.Text+        _ <- sendPrepare connection "s2" "select pg_sleep($1)" (Just [float8Oid])+        _ <- sendQueryPrepared connection "s2" [Just ("0.5", Lq.Text)] Lq.Text+        _ <- pipelineSync connection+        _ <- sendFlushRequest connection+        -- Consume what toPipelineIO would have read before the timeout:+        -- parse1 result + separator, exec1 result + separator, parse2 result + separator.+        _ <- takeCommandResults connection+        _ <- takeCommandResults connection+        _ <- takeCommandResults connection+        -- exec2 (pg_sleep) is still running; cancel it to simulate the+        -- timeout-triggered cancel in cleanUpAfterInterruption.+        mHandle <- getCancel connection+        _ <- for mHandle Lq.cancel+        -- cleanUpAfterInterruption: drain1, then drain2 (after cancel)+        _ <- drainResults connection+        _ <- drainResults connection+        -- leavePipeline: new Sync, drain, Flush, drain+        _ <- pipelineSync connection+        _ <- drainResults connection+        _ <- sendFlushRequest connection+        _ <- drainResults connection+        exited <- exitPipelineMode connection+        pure exited++    -- Reproduces the failure seen in hasql's "Leaves the connection usable+    -- after timeout in pipeline" test.  A fast prepared statement is followed+    -- by a slow one; the whole read phase is wrapped in a short timeout so the+    -- slow statement is interrupted.  The exact cleanup sequence hasql uses is+    -- then applied, and the connection must be left out of pipeline mode and+    -- usable.+    --+    -- The reference (libpq) completes the blocked read before the async+    -- exception is delivered, so the session has already exited pipeline mode+    -- when cleanup starts.  The pqi-native adapter is interrupted mid-read and+    -- currently fails to leave pipeline mode, which is the bug this scenario+    -- captures.+    it "recovers after timeout interrupts mid-pipeline read" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- enterPipelineMode connection+        _ <- sendPrepare connection "s1" "select $1::int" Nothing+        _ <- sendQueryPrepared connection "s1" [Just ("42", Lq.Text)] Lq.Text+        _ <- sendPrepare connection "s2" "select pg_sleep($1)" (Just [float8Oid])+        _ <- sendQueryPrepared connection "s2" [Just ("0.1", Lq.Text)] Lq.Text+        _ <- pipelineSync connection+        -- Interrupt the read just like hasql's Connection.use + timeout does.+        _ <- timeout 50000 (drainResults connection)+        -- cleanUpAfterInterruption+        _ <- drainResults connection+        mHandle <- getCancel connection+        _ <- for mHandle Lq.cancel+        _ <- drainResults connection+        -- leavePipeline (including the retry that hasql performs)+        pipelineStatusBefore <- pipelineStatus connection+        exited <-+          if pipelineStatusBefore == Lq.PipelineOn+            then do+              _ <- pipelineSync connection+              _ <- drainResults connection+              _ <- sendFlushRequest connection+              _ <- drainResults connection+              ok <- exitPipelineMode connection+              if ok+                then pure True+                else do+                  _ <- drainResults connection+                  exitPipelineMode connection+            else pure True+        afterStatus <- pipelineStatus connection+        usable <- execScenario "select 99" connection+        pure (exited, afterStatus, usable)
+ src/library/Pqi/Conformance/Operation/Fformat.hs view
@@ -0,0 +1,27 @@+-- | Coverage for 'Pqi.fformat': the format (text or binary) of each+-- column, which follows the result format requested of+-- 'Pqi.execParams'.+module Pqi.Conformance.Operation.Fformat+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import qualified Pqi as Lq+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "fformat" do+    it "follows the requested result format" \conninfo ->+      differential proxy conninfo \connection -> do+        let formatsOf fmt =+              execParams connection "select 1 :: int4, 'x' :: text" [] fmt+                >>= traverse \r -> do+                  n <- nfields r+                  traverse (fformat r) [0 .. n - 1]+        textFormats <- formatsOf Lq.Text+        binaryFormats <- formatsOf Lq.Binary+        pure (textFormats, binaryFormats)
+ src/library/Pqi/Conformance/Operation/Finish.hs view
@@ -0,0 +1,27 @@+-- | Coverage for 'Pqi.finish': closing a connection releases it without+-- error, whether the connection was open or the null sentinel.+module Pqi.Conformance.Operation.Finish+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "finish" do+    it "closes an open connection cleanly" \conninfo ->+      differentialConnect proxy conninfo \(_ :: Proxy c) conninfo' -> do+        connection <- connectdb conninfo' :: IO c+        before <- status connection+        finish connection+        pure before++    it "closes the null sentinel cleanly" \conninfo ->+      differentialConnect proxy conninfo \(_ :: Proxy c) _ -> do+        connection <- newNullConnection :: IO c+        finish connection+        pure ()
+ src/library/Pqi/Conformance/Operation/Flush.hs view
@@ -0,0 +1,24 @@+-- | Coverage for 'Pqi.flush': flushing queued output to the server in+-- non-blocking mode until it reports completion.+module Pqi.Conformance.Operation.Flush+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults, flushUntilDone)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "flush" do+    it "flushes queued output to completion in non-blocking mode" \conninfo ->+      differential proxy conninfo \connection -> do+        setOk <- setnonblocking connection True+        sent <- sendQuery connection "select 1"+        flushed <- flushUntilDone (flush connection)+        results <- drainResults connection+        restoreOk <- setnonblocking connection False+        pure (setOk, sent, flushed, results, restoreOk)
+ src/library/Pqi/Conformance/Operation/Fmod.hs view
@@ -0,0 +1,24 @@+-- | Coverage for 'Pqi.fmod': the type modifier of each column (e.g. the+-- precision\/scale of a @numeric@), including an out-of-range index.+module Pqi.Conformance.Operation.Fmod+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "fmod" do+    it "reports type modifiers and degrades out of range" \conninfo ->+      differential proxy conninfo \connection -> do+        result <-+          exec connection "select 1.5 :: numeric(10,2), 'pad' :: char(5), 'x' :: varchar(3), true, 1 :: int4"+        for result \r -> do+          n <- nfields r+          modifiers <- traverse (fmod r) [0 .. n - 1]+          outOfRange <- fmod r 9+          pure (modifiers, outOfRange)
+ src/library/Pqi/Conformance/Operation/Fname.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.fname': column names by index, including an+-- out-of-range index.+module Pqi.Conformance.Operation.Fname+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "fname" do+    it "names columns and degrades out of range" \conninfo ->+      differential proxy conninfo \connection -> do+        result <- exec connection "select 1 as foo, 2 as bar"+        for result \r -> do+          n <- nfields r+          names <- traverse (fname r) [0 .. n - 1]+          outOfRange <- fname r 5+          pure (names, outOfRange)
+ src/library/Pqi/Conformance/Operation/Fnumber.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.fnumber': resolving a column name to an index,+-- folding the argument the way @PQfnumber@ does (ASCII case folding outside+-- double quotes, quoted runs verbatim).+module Pqi.Conformance.Operation.Fnumber+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "fnumber" do+    it "resolves names like an identifier" \conninfo ->+      differential proxy conninfo \connection -> do+        result <- exec connection "select 1 as foo, 2 as \"Bar\""+        for result \r ->+          traverse+            (fnumber r)+            ["foo", "FOO", "Foo", "Bar", "bar", "\"Bar\"", "\"foo\"", "missing"]
+ src/library/Pqi/Conformance/Operation/Fsize.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.fsize': the server-side storage size of each+-- column's type (negative for variable size), including an out-of-range index.+module Pqi.Conformance.Operation.Fsize+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "fsize" do+    it "reports type sizes and degrades out of range" \conninfo ->+      differential proxy conninfo \connection -> do+        result <- exec connection "select 1 :: int2, 1 :: int4, 1 :: int8, 'x' :: text, true"+        for result \r -> do+          n <- nfields r+          sizes <- traverse (fsize r) [0 .. n - 1]+          outOfRange <- fsize r 9+          pure (sizes, outOfRange)
+ src/library/Pqi/Conformance/Operation/Ftable.hs view
@@ -0,0 +1,24 @@+-- | Coverage for 'Pqi.ftable': the source-table OID of each column.+--+-- A shared @pg_catalog@ table is selected so the OID is identical across the+-- candidate's and the reference's connections (a temporary table's OID would+-- differ per connection).+module Pqi.Conformance.Operation.Ftable+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "ftable" do+    it "reports the source-table OID, or none for a computed column" \conninfo ->+      differential proxy conninfo \connection -> do+        result <- exec connection "select relname, relkind, 1 as computed from pg_catalog.pg_class where false"+        for result \r -> do+          n <- nfields r+          traverse (ftable r) [0 .. n - 1]
+ src/library/Pqi/Conformance/Operation/Ftablecol.hs view
@@ -0,0 +1,24 @@+-- | Coverage for 'Pqi.ftablecol': the source-column number of each result+-- column.+--+-- A shared @pg_catalog@ table is selected so the provenance is identical across+-- the candidate's and the reference's connections.+module Pqi.Conformance.Operation.Ftablecol+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "ftablecol" do+    it "reports the source-column number, or zero for a computed column" \conninfo ->+      differential proxy conninfo \connection -> do+        result <- exec connection "select relname, relkind, 1 as computed from pg_catalog.pg_class where false"+        for result \r -> do+          n <- nfields r+          traverse (ftablecol r) [0 .. n - 1]
+ src/library/Pqi/Conformance/Operation/Ftype.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.ftype': the data-type OID of each column, including+-- an out-of-range index.+module Pqi.Conformance.Operation.Ftype+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "ftype" do+    it "reports type OIDs and degrades out of range" \conninfo ->+      differential proxy conninfo \connection -> do+        result <- exec connection "select 1 :: int4, 'x' :: text, true, 1.5 :: float8, 1 :: int2"+        for result \r -> do+          n <- nfields r+          types <- traverse (ftype r) [0 .. n - 1]+          outOfRange <- ftype r 9+          pure (types, outOfRange)
+ src/library/Pqi/Conformance/Operation/GetCancel.hs view
@@ -0,0 +1,18 @@+-- | Coverage for 'Pqi.getCancel': obtaining a cancellation handle from a+-- connection.+module Pqi.Conformance.Operation.GetCancel+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "getCancel" do+    it "produces a handle for an open connection" \conninfo ->+      differential proxy conninfo \connection ->+        isJust <$> getCancel connection
+ src/library/Pqi/Conformance/Operation/GetCopyData.hs view
@@ -0,0 +1,29 @@+-- | Coverage for 'Pqi.getCopyData': receiving rows from a+-- @COPY TO STDOUT@ in text, CSV, and binary formats.+module Pqi.Conformance.Operation.GetCopyData+  ( spec,+  )+where++import Pqi (IsConnection)+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (collectCopyOut, drainResults, execScenario)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "getCopyData" do+    let copyOut sql conninfo =+          differential proxy conninfo \connection -> do+            started <- execScenario sql connection+            rows <- collectCopyOut connection+            outcome <- drainResults connection+            pure (started, rows, outcome)+    it "receives text rows"+      $ copyOut "copy (select i, i * 2 from generate_series (1, 3) as i) to stdout"+    it "receives CSV rows with a header"+      $ copyOut+        "copy (select i as n, 'v' || i as v from generate_series (1, 2) as i) to stdout (format csv, header)"+    it "receives binary rows"+      $ copyOut "copy (select i from generate_series (1, 2) as i) to stdout (format binary)"
+ src/library/Pqi/Conformance/Operation/GetNotice.hs view
@@ -0,0 +1,27 @@+-- | Coverage for 'Pqi.getNotice': retrieving accumulated notices, which+-- are present only while reporting is enabled and drain after retrieval.+--+-- The notice text is compared byte-identically — both adapters must produce+-- the same formatted string as libpq's notice processor at DEFAULT verbosity.+-- At that verbosity, context (@'W'@ field) is suppressed for NOTICE-level+-- messages, so the formatted string is just @\"NOTICE:  \<message\>\\n\"@.+module Pqi.Conformance.Operation.GetNotice+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "getNotice" do+    it "yields the formatted notice while enabled and then drains" \conninfo ->+      differential proxy conninfo \connection -> do+        enableNoticeReporting connection+        _ <- exec connection "do $$ begin raise notice 'conformance notice'; end $$"+        firstNotice <- getNotice connection+        afterDrain <- getNotice connection+        pure (firstNotice, afterDrain)
+ src/library/Pqi/Conformance/Operation/GetResult.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.getResult': collecting the results of an+-- asynchronous command one at a time, ending with the 'Nothing' terminator.+module Pqi.Conformance.Operation.GetResult+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (takeResult)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "getResult" do+    it "yields each result then a Nothing terminator" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- sendQuery connection "select 1 :: int4"+        first <- takeResult connection+        terminator <- takeResult connection+        afterTerminator <- takeResult connection+        pure (first, terminator, afterTerminator)
+ src/library/Pqi/Conformance/Operation/Getisnull.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.getisnull': whether a cell is SQL @NULL@,+-- distinguishing a null from an empty string.+module Pqi.Conformance.Operation.Getisnull+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "getisnull" do+    it "distinguishes null, empty, and non-empty cells" \conninfo ->+      differential proxy conninfo \connection -> do+        result <- exec connection "select 'x' :: text, '' :: text, null :: int4"+        for result \r -> do+          nonEmpty <- getisnull r 0 0+          empty <- getisnull r 0 1+          nullCell <- getisnull r 0 2+          pure (nonEmpty, empty, nullCell)
+ src/library/Pqi/Conformance/Operation/Getlength.hs view
@@ -0,0 +1,21 @@+-- | Coverage for 'Pqi.getlength': the byte length of a cell value,+-- including a multibyte value, an empty string, and a null.+module Pqi.Conformance.Operation.Getlength+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "getlength" do+    it "reports byte lengths across cell shapes" \conninfo ->+      differential proxy conninfo \connection -> do+        result <- exec connection "select 'hello' :: text, 'héllo' :: text, '' :: text, null :: int4"+        for result \r -> do+          n <- nfields r+          traverse (getlength r 0) [0 .. n - 1]
+ src/library/Pqi/Conformance/Operation/Getvalue.hs view
@@ -0,0 +1,24 @@+-- | Coverage for 'Pqi.getvalue': the cell value at a position, 'Nothing'+-- for SQL @NULL@, and out-of-range row and column probes.+module Pqi.Conformance.Operation.Getvalue+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "getvalue" do+    it "reads cells, nulls, and degrades out of range" \conninfo ->+      differential proxy conninfo \connection -> do+        result <- exec connection "select 'hello' :: text, null :: int4"+        for result \r -> do+          present <- getvalue r 0 0+          nullCell <- getvalue r 0 1+          badRow <- getvalue r 1 0+          badColumn <- getvalue r 0 5+          pure (present, nullCell, badRow, badColumn)
+ src/library/Pqi/Conformance/Operation/GetvalueCopy.hs view
@@ -0,0 +1,23 @@+-- | Coverage for @getvalue'@ ('Pqi.getvalue''): a copying cell read whose+-- bytes remain valid after the result is freed.+module Pqi.Conformance.Operation.GetvalueCopy+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "getvalue'" do+    it "copies cells that survive freeing the result" \conninfo ->+      differential proxy conninfo \connection -> do+        result <- exec connection "select 'copied' :: text, null :: int4"+        for result \r -> do+          first <- getvalue' r 0 0+          second <- getvalue' r 0 1+          unsafeFreeResult r+          pure (first, second)
+ src/library/Pqi/Conformance/Operation/Host.hs view
@@ -0,0 +1,16 @@+-- | Coverage for 'Pqi.host': the server host name of the connection.+module Pqi.Conformance.Operation.Host+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "host" do+    it "reports the host from the conninfo" \conninfo ->+      differential proxy conninfo host
+ src/library/Pqi/Conformance/Operation/IsBusy.hs view
@@ -0,0 +1,27 @@+-- | Coverage for 'Pqi.isBusy': reporting whether a 'Pqi.getResult'+-- would block, settling to not-busy once the result has arrived.+module Pqi.Conformance.Operation.IsBusy+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "isBusy" do+    it "settles to not-busy after the result arrives" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- sendQuery connection "select 42"+        let settle (0 :: Int) = isBusy connection+            settle n = do+              _ <- consumeInput connection+              busy <- isBusy connection+              if busy then threadDelay 1000 >> settle (n - 1) else pure busy+        stillBusy <- settle 10000+        _ <- drainResults connection+        pure stillBusy
+ src/library/Pqi/Conformance/Operation/IsNullConnection.hs view
@@ -0,0 +1,24 @@+-- | Coverage for 'Pqi.isNullConnection': distinguishes the null sentinel+-- from a genuinely open connection.+module Pqi.Conformance.Operation.IsNullConnection+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "isNullConnection" do+    it "is True for the null sentinel and False for an open connection" \conninfo ->+      differentialConnect proxy conninfo \(_ :: Proxy c) conninfo' -> do+        nullConn <- newNullConnection :: IO c+        let nullIsNull = isNullConnection nullConn+        finish nullConn+        openConn <- connectdb conninfo' :: IO c+        let openIsNull = isNullConnection openConn+        finish openConn+        pure (nullIsNull, openIsNull)
+ src/library/Pqi/Conformance/Operation/Isnonblocking.hs view
@@ -0,0 +1,21 @@+-- | Coverage for 'Pqi.isnonblocking': reporting the connection's+-- non-blocking flag, which defaults to off.+module Pqi.Conformance.Operation.Isnonblocking+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "isnonblocking" do+    it "is off initially and reflects a change" \conninfo ->+      differential proxy conninfo \connection -> do+        initially <- isnonblocking connection+        _ <- setnonblocking connection True+        afterEnable <- isnonblocking connection+        pure (initially, afterEnable)
+ src/library/Pqi/Conformance/Operation/LoClose.hs view
@@ -0,0 +1,28 @@+-- | Coverage for 'Pqi.loClose': closing an open large object, after which+-- reads through the stale descriptor fail.+module Pqi.Conformance.Operation.LoClose+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (inTransaction)+import System.IO (IOMode (..))+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loClose" do+    it "closes the descriptor and invalidates further reads" \conninfo ->+      differential proxy conninfo \connection ->+        inTransaction connection do+          oid <- loCreat connection+          outcome <- for oid \o -> do+            fd <- loOpen connection o ReadMode+            closed <- for fd \f -> loClose connection f+            readAfterClose <- join <$> for fd \f -> loRead connection f 10+            pure (closed, readAfterClose)+          traverse_ (loUnlink connection) oid+          pure outcome
+ src/library/Pqi/Conformance/Operation/LoCreat.hs view
@@ -0,0 +1,25 @@+-- | Coverage for 'Pqi.loCreat': creating a new large object with a+-- server-assigned OID.+--+-- The assigned OID differs between the candidate's and the reference's runs,+-- so only its presence is compared.+module Pqi.Conformance.Operation.LoCreat+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (inTransaction)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loCreat" do+    it "creates a large object" \conninfo ->+      differential proxy conninfo \connection ->+        inTransaction connection do+          oid <- loCreat connection+          traverse_ (loUnlink connection) oid+          pure (isJust oid)
+ src/library/Pqi/Conformance/Operation/LoCreate.hs view
@@ -0,0 +1,30 @@+-- | Coverage for 'Pqi.loCreate': creating a new large object with an+-- explicitly requested OID.+--+-- Each run removes the object it creates, so the explicit OID is free for the+-- reference run and can be compared in full.+module Pqi.Conformance.Operation.LoCreate+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (inTransaction)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loCreate" do+    it "creates a large object with an explicit OID" \conninfo ->+      differential proxy conninfo \connection ->+        inTransaction connection do+          -- Best-effort cleanup of leftovers from an earlier crashed run; its+          -- outcome legitimately differs between runs, so it is not observed.+          _ <- loUnlink connection explicitOid+          created <- loCreate connection explicitOid+          unlinked <- for created (loUnlink connection)+          pure (created, unlinked)+  where+    explicitOid = 424242
+ src/library/Pqi/Conformance/Operation/LoExport.hs view
@@ -0,0 +1,37 @@+-- | Coverage for 'Pqi.loExport': exporting a large object back to a file,+-- round-tripping its bytes.+--+-- @lo_import@\/@lo_export@ are self-contained and run in autocommit; they need+-- no explicit transaction block.+module Pqi.Conformance.Operation.LoExport+  ( spec,+  )+where++import qualified Data.ByteString as ByteString+import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import System.Directory (removeFile)+import System.IO (hClose, openBinaryTempFile)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loExport" do+    it "exports an imported object, round-tripping its bytes" \conninfo ->+      differential proxy conninfo \connection -> do+        (importPath, importHandle) <- openBinaryTempFile "/tmp" "pqi-conformance-export-in"+        ByteString.hPut importHandle payload+        hClose importHandle+        (exportPath, exportHandle) <- openBinaryTempFile "/tmp" "pqi-conformance-export-out"+        hClose exportHandle+        imported <- loImport connection importPath+        exported <- for imported \o -> loExport connection o exportPath+        traverse_ (loUnlink connection) imported+        roundTripped <- ByteString.readFile exportPath+        removeFile importPath+        removeFile exportPath+        pure (exported, roundTripped == payload)+  where+    payload = "pqi conformance payload\n" <> ByteString.pack [0 .. 255]
+ src/library/Pqi/Conformance/Operation/LoImport.hs view
@@ -0,0 +1,30 @@+-- | Coverage for 'Pqi.loImport': importing a file as a new large object.+--+-- The server-assigned OID differs between runs, so only its presence is+-- compared. @lo_import@ is self-contained and runs in autocommit; it needs no+-- explicit transaction block.+module Pqi.Conformance.Operation.LoImport+  ( spec,+  )+where++import qualified Data.ByteString as ByteString+import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import System.Directory (removeFile)+import System.IO (hClose, openBinaryTempFile)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loImport" do+    it "imports a file as a large object" \conninfo ->+      differential proxy conninfo \connection -> do+        (path, handle) <- openBinaryTempFile "/tmp" "pqi-conformance-import"+        ByteString.hPut handle "pqi conformance payload"+        hClose handle+        imported <- loImport connection path+        traverse_ (loUnlink connection) imported+        removeFile path+        pure (isJust imported)
+ src/library/Pqi/Conformance/Operation/LoImportWithOid.hs view
@@ -0,0 +1,34 @@+-- | Coverage for 'Pqi.loImportWithOid': importing a file as a new large+-- object with an explicitly requested OID.+--+-- Each run removes the object it creates, so the explicit OID is free for the+-- reference run and can be compared in full. @lo_import@ is self-contained and+-- runs in autocommit; it needs no explicit transaction block.+module Pqi.Conformance.Operation.LoImportWithOid+  ( spec,+  )+where++import qualified Data.ByteString as ByteString+import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import System.Directory (removeFile)+import System.IO (hClose, openBinaryTempFile)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loImportWithOid" do+    it "imports a file as a large object with an explicit OID" \conninfo ->+      differential proxy conninfo \connection -> do+        (path, handle) <- openBinaryTempFile "/tmp" "pqi-conformance-import-oid"+        ByteString.hPut handle "pqi conformance payload"+        hClose handle+        _ <- loUnlink connection explicitOid+        imported <- loImportWithOid connection path explicitOid+        unlinked <- for imported (loUnlink connection)+        removeFile path+        pure (imported, unlinked)+  where+    explicitOid = 424243
+ src/library/Pqi/Conformance/Operation/LoOpen.hs view
@@ -0,0 +1,30 @@+-- | Coverage for 'Pqi.loOpen': opening a large object, including the+-- error path for a non-existent object.+module Pqi.Conformance.Operation.LoOpen+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (inTransaction)+import System.IO (IOMode (..))+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loOpen" do+    it "opens an existing object and rejects a missing one" \conninfo ->+      differential proxy conninfo \connection ->+        inTransaction connection do+          oid <- loCreat connection+          opened <- for oid \o -> do+            fd <- loOpen connection o ReadWriteMode+            traverse_ (loClose connection) fd+            pure (isJust fd)+          loUnlink' oid connection+          missing <- loOpen connection 4242424 ReadMode+          pure (opened, isJust missing)+  where+    loUnlink' oid connection = traverse_ (loUnlink connection) oid
+ src/library/Pqi/Conformance/Operation/LoRead.hs view
@@ -0,0 +1,31 @@+-- | Coverage for 'Pqi.loRead': reading bytes back from an open large+-- object after seeking to its start.+module Pqi.Conformance.Operation.LoRead+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (inTransaction)+import System.IO (IOMode (..), SeekMode (..))+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loRead" do+    it "reads back what was written" \conninfo ->+      differential proxy conninfo \connection ->+        inTransaction connection do+          oid <- loCreat connection+          outcome <- for oid \o -> do+            fd <- loOpen connection o ReadWriteMode+            readBytes <- for fd \f -> do+              _ <- loWrite connection f "hello, large object"+              _ <- loSeek connection f AbsoluteSeek 0+              loRead connection f 5+            traverse_ (loClose connection) fd+            pure readBytes+          traverse_ (loUnlink connection) oid+          pure outcome
+ src/library/Pqi/Conformance/Operation/LoSeek.hs view
@@ -0,0 +1,33 @@+-- | Coverage for 'Pqi.loSeek': repositioning within an open large object+-- with absolute, relative, and from-end seeks.+module Pqi.Conformance.Operation.LoSeek+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (inTransaction)+import System.IO (IOMode (..), SeekMode (..))+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loSeek" do+    it "seeks absolutely, relatively, and from the end" \conninfo ->+      differential proxy conninfo \connection ->+        inTransaction connection do+          oid <- loCreat connection+          outcome <- for oid \o -> do+            fd <- loOpen connection o ReadWriteMode+            seeks <- for fd \f -> do+              _ <- loWrite connection f "hello, large object"+              absolute <- loSeek connection f AbsoluteSeek 0+              relative <- loSeek connection f RelativeSeek 2+              fromEnd <- loSeek connection f SeekFromEnd (-6)+              pure (absolute, relative, fromEnd)+            traverse_ (loClose connection) fd+            pure seeks+          traverse_ (loUnlink connection) oid+          pure outcome
+ src/library/Pqi/Conformance/Operation/LoTell.hs view
@@ -0,0 +1,33 @@+-- | Coverage for 'Pqi.loTell': reporting the current seek position of an+-- open large object.+module Pqi.Conformance.Operation.LoTell+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (inTransaction)+import System.IO (IOMode (..), SeekMode (..))+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loTell" do+    it "reports the position after a write and a seek" \conninfo ->+      differential proxy conninfo \connection ->+        inTransaction connection do+          oid <- loCreat connection+          outcome <- for oid \o -> do+            fd <- loOpen connection o ReadWriteMode+            positions <- for fd \f -> do+              _ <- loWrite connection f "hello, large object"+              afterWrite <- loTell connection f+              _ <- loSeek connection f AbsoluteSeek 3+              afterSeek <- loTell connection f+              pure (afterWrite, afterSeek)+            traverse_ (loClose connection) fd+            pure positions+          traverse_ (loUnlink connection) oid+          pure outcome
+ src/library/Pqi/Conformance/Operation/LoTruncate.hs view
@@ -0,0 +1,32 @@+-- | Coverage for 'Pqi.loTruncate': truncating an open large object, after+-- which a seek to the end reports the new size.+module Pqi.Conformance.Operation.LoTruncate+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (inTransaction)+import System.IO (IOMode (..), SeekMode (..))+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loTruncate" do+    it "truncates to a new size" \conninfo ->+      differential proxy conninfo \connection ->+        inTransaction connection do+          oid <- loCreat connection+          outcome <- for oid \o -> do+            fd <- loOpen connection o ReadWriteMode+            result <- for fd \f -> do+              _ <- loWrite connection f "hello, large object"+              truncated <- loTruncate connection f 5+              newEnd <- loSeek connection f SeekFromEnd 0+              pure (truncated, newEnd)+            traverse_ (loClose connection) fd+            pure result+          traverse_ (loUnlink connection) oid+          pure outcome
+ src/library/Pqi/Conformance/Operation/LoUnlink.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.loUnlink': removing a large object, including the+-- error path for a non-existent object.+module Pqi.Conformance.Operation.LoUnlink+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (inTransaction)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loUnlink" do+    it "removes an existing object and rejects a missing one" \conninfo ->+      differential proxy conninfo \connection ->+        inTransaction connection do+          oid <- loCreat connection+          removed <- for oid (loUnlink connection)+          missing <- loUnlink connection 4242424+          pure (removed, missing)
+ src/library/Pqi/Conformance/Operation/LoWrite.hs view
@@ -0,0 +1,27 @@+-- | Coverage for 'Pqi.loWrite': writing bytes to an open large object.+module Pqi.Conformance.Operation.LoWrite+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (inTransaction)+import System.IO (IOMode (..))+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "loWrite" do+    it "reports the number of bytes written" \conninfo ->+      differential proxy conninfo \connection ->+        inTransaction connection do+          oid <- loCreat connection+          outcome <- for oid \o -> do+            fd <- loOpen connection o ReadWriteMode+            written <- for fd \f -> loWrite connection f "hello, large object"+            traverse_ (loClose connection) fd+            pure written+          traverse_ (loUnlink connection) oid+          pure outcome
+ src/library/Pqi/Conformance/Operation/NewNullConnection.hs view
@@ -0,0 +1,22 @@+-- | Coverage for 'Pqi.newNullConnection': the sentinel \"null\" connection+-- is reported as null and bad.+module Pqi.Conformance.Operation.NewNullConnection+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "newNullConnection" do+    it "produces a connection that is null and bad" \conninfo ->+      differentialConnect proxy conninfo \(_ :: Proxy c) _ -> do+        connection <- newNullConnection :: IO c+        nullness <- pure (isNullConnection connection)+        badness <- status connection+        finish connection+        pure (nullness, badness)
+ src/library/Pqi/Conformance/Operation/Nfields.hs view
@@ -0,0 +1,22 @@+-- | Coverage for 'Pqi.nfields': the column count across a multi-column+-- result, a zero-column result, and a command result.+module Pqi.Conformance.Operation.Nfields+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "nfields" do+    it "counts columns across result shapes" \conninfo ->+      differential proxy conninfo \connection -> do+        let countOf sql = exec connection sql >>= traverse nfields+        several <- countOf "select 1, 2, 3"+        zero <- countOf "select"+        command <- countOf "create temporary table conformance_nfields (id int4)"+        pure (several, zero, command)
+ src/library/Pqi/Conformance/Operation/Notifies.hs view
@@ -0,0 +1,57 @@+-- | Coverage for 'Pqi.notifies': @LISTEN@\/@NOTIFY@ delivery, queueing,+-- and that @UNLISTEN@ stops delivery.+--+-- The backend PID carried by a notification is connection-specific — the+-- candidate and the reference are distinct backends — so 'bePid' is omitted+-- from the cross-adapter comparison. Each scenario that receives a+-- notification instead asserts independently (per adapter) that+-- @notification.bePid == backendPID connection@, verifying that the PID field+-- is correctly populated without comparing it across adapters.+module Pqi.Conformance.Operation.Notifies+  ( spec,+  )+where++import Pqi (IsConnection (..), Notify (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "notifies" do+    it "is empty with no pending notifications" \conninfo ->+      differential proxy conninfo \connection ->+        fmap channelAndPayload <$> notifies connection++    it "delivers a listen/notify round-trip and then drains" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "listen conformance_channel"+        _ <- exec connection "notify conformance_channel, 'payload-1'"+        notification <- notifies connection+        pid <- backendPID connection+        for_ notification \n -> n.bePid `shouldBe` pid+        drained <- fmap channelAndPayload <$> notifies connection+        pure (fmap channelAndPayload notification, drained)++    it "queues notifications in order" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "listen conformance_channel"+        _ <- exec connection "notify conformance_channel, 'first'"+        _ <- exec connection "notify conformance_channel, 'second'"+        first <- notifies connection+        second <- notifies connection+        third <- notifies connection+        pid <- backendPID connection+        for_ first \n -> n.bePid `shouldBe` pid+        for_ second \n -> n.bePid `shouldBe` pid+        pure (fmap channelAndPayload first, fmap channelAndPayload second, fmap channelAndPayload third)++    it "stops delivery after unlisten" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "listen conformance_channel"+        _ <- exec connection "unlisten conformance_channel"+        _ <- exec connection "notify conformance_channel, 'lost'"+        fmap channelAndPayload <$> notifies connection+  where+    channelAndPayload notification = (notification.relname, notification.extra)
+ src/library/Pqi/Conformance/Operation/Nparams.hs view
@@ -0,0 +1,26 @@+-- | Coverage for 'Pqi.nparams': the parameter count of a+-- 'Pqi.describePrepared' result.+module Pqi.Conformance.Operation.Nparams+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "nparams" do+    it "counts the parameters of a prepared statement" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- prepare connection "conformance_nparams" "select $1 :: int4, $2 :: text" Nothing+        described <- describePrepared connection "conformance_nparams"+        for described nparams++    it "is zero for a parameterless statement" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- prepare connection "conformance_nparams_zero" "select 42" Nothing+        described <- describePrepared connection "conformance_nparams_zero"+        for described nparams
+ src/library/Pqi/Conformance/Operation/Ntuples.hs view
@@ -0,0 +1,22 @@+-- | Coverage for 'Pqi.ntuples': the row count across a multi-row result,+-- an empty result, and a command result.+module Pqi.Conformance.Operation.Ntuples+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "ntuples" do+    it "counts rows across result shapes" \conninfo ->+      differential proxy conninfo \connection -> do+        let countOf sql = exec connection sql >>= traverse ntuples+        many <- countOf "select i from generate_series (1, 3) as i"+        none <- countOf "select 1 where false"+        command <- countOf "create temporary table conformance_ntuples (id int4)"+        pure (many, none, command)
+ src/library/Pqi/Conformance/Operation/Options.hs view
@@ -0,0 +1,17 @@+-- | Coverage for 'Pqi.options': the command-line options of the+-- connection request.+module Pqi.Conformance.Operation.Options+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "options" do+    it "reports the command-line options from the conninfo" \conninfo ->+      differential proxy conninfo options
+ src/library/Pqi/Conformance/Operation/ParameterStatus.hs view
@@ -0,0 +1,25 @@+-- | Coverage for 'Pqi.parameterStatus': server-reported parameter+-- settings, including @GUC_REPORT@ updates and an absent parameter.+module Pqi.Conformance.Operation.ParameterStatus+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "parameterStatus" do+    it "reports parameter statuses, including GUC_REPORT updates" \conninfo ->+      differential proxy conninfo \connection -> do+        before <- parameterStatus connection "application_name"+        _ <- exec connection "set application_name to 'pqi-conformance'"+        after <- parameterStatus connection "application_name"+        clientEncoding <- parameterStatus connection "client_encoding"+        standardConformingStrings <- parameterStatus connection "standard_conforming_strings"+        integerDatetimes <- parameterStatus connection "integer_datetimes"+        missing <- parameterStatus connection "no_such_parameter"+        pure (before, after, clientEncoding, standardConformingStrings, integerDatetimes, missing)
+ src/library/Pqi/Conformance/Operation/Paramtype.hs view
@@ -0,0 +1,31 @@+-- | Coverage for 'Pqi.paramtype': the data-type OID of each parameter of+-- a 'Pqi.describePrepared' result, both inferred and explicitly typed.+module Pqi.Conformance.Operation.Paramtype+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (int8Oid)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "paramtype" do+    it "reports inferred parameter types" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- prepare connection "conformance_paramtype" "select $1 :: int4, $2 :: text" Nothing+        described <- describePrepared connection "conformance_paramtype"+        for described \r -> do+          n <- nparams r+          traverse (paramtype r) [0 .. n - 1]++    it "reports explicitly requested parameter types" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- prepare connection "conformance_paramtype_typed" "select $1" (Just [int8Oid])+        described <- describePrepared connection "conformance_paramtype_typed"+        for described \r -> do+          n <- nparams r+          traverse (paramtype r) [0 .. n - 1]
+ src/library/Pqi/Conformance/Operation/Pass.hs view
@@ -0,0 +1,17 @@+-- | Coverage for 'Pqi.pass': the password of the connection (absent under+-- the trust-auth conninfo).+module Pqi.Conformance.Operation.Pass+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "pass" do+    it "reports the password from the conninfo" \conninfo ->+      differential proxy conninfo pass
+ src/library/Pqi/Conformance/Operation/PipelineStatus.hs view
@@ -0,0 +1,33 @@+-- | Coverage for 'Pqi.pipelineStatus': the pipeline-mode status,+-- including the aborted state after an in-pipeline error and recovery at the+-- sync point.+module Pqi.Conformance.Operation.PipelineStatus+  ( spec,+  )+where++import Pqi (IsConnection (..))+import qualified Pqi as Lq+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (takeCommandResults, takeResult)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "pipelineStatus" do+    it "reports off, on, aborted, and recovery" \conninfo ->+      differential proxy conninfo \connection -> do+        off <- pipelineStatus connection+        _ <- enterPipelineMode connection+        on <- pipelineStatus connection+        _ <- traverse (\sql -> sendQueryParams connection sql [] Lq.Text) ["select 1", "select 1 / 0", "select 3"]+        _ <- pipelineSync connection+        _ <- takeCommandResults connection+        _ <- takeCommandResults connection+        aborted <- pipelineStatus connection+        _ <- takeCommandResults connection+        _ <- takeResult connection+        recovered <- pipelineStatus connection+        _ <- exitPipelineMode connection+        pure (off, on, aborted, recovered)
+ src/library/Pqi/Conformance/Operation/PipelineSync.hs view
@@ -0,0 +1,56 @@+-- | Coverage for 'Pqi.pipelineSync': marking a synchronization point that+-- batches pipelined commands, and the abort semantics when one of them fails.+module Pqi.Conformance.Operation.PipelineSync+  ( spec,+  )+where++import Pqi (IsConnection (..))+import qualified Pqi as Lq+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (takeCommandResults, takeResult)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "pipelineSync" do+    it "collects pipelined queries per sync" \conninfo ->+      differential proxy conninfo \connection -> do+        entered <- enterPipelineMode connection+        sent <-+          traverse+            (\sql -> sendQueryParams connection sql [] Lq.Text)+            ["select 1 :: int4", "select 'two' :: text", "select 3 :: int4, 'three' :: text"]+        synced <- pipelineSync connection+        first <- takeCommandResults connection+        second <- takeCommandResults connection+        third <- takeCommandResults connection+        syncResult <- takeResult connection+        idle <- takeResult connection+        exited <- exitPipelineMode connection+        pure (entered, sent, synced, first, second, third, syncResult, idle, exited)++    it "aborts the rest of the pipeline after an error" \conninfo ->+      differential proxy conninfo \connection -> do+        entered <- enterPipelineMode connection+        sent <-+          traverse+            (\sql -> sendQueryParams connection sql [] Lq.Text)+            ["select 1", "select 1 / 0", "select 3"]+        synced <- pipelineSync connection+        first <- takeCommandResults connection+        failed <- takeCommandResults connection+        aborted <- takeCommandResults connection+        syncResult <- takeResult connection+        exited <- exitPipelineMode connection+        pure (entered, sent, synced, first, failed, aborted, syncResult, exited)++    it "returns a sync result when called without prior commands" \conninfo ->+      differential proxy conninfo \connection -> do+        entered <- enterPipelineMode connection+        synced <- pipelineSync connection+        syncResult <- takeResult connection+        trailing <- takeResult connection+        exited <- exitPipelineMode connection+        pure (entered, synced, syncResult, trailing, exited)
+ src/library/Pqi/Conformance/Operation/Port.hs view
@@ -0,0 +1,16 @@+-- | Coverage for 'Pqi.port': the port of the connection.+module Pqi.Conformance.Operation.Port+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "port" do+    it "reports the port from the conninfo" \conninfo ->+      differential proxy conninfo port
+ src/library/Pqi/Conformance/Operation/Prepare.hs view
@@ -0,0 +1,28 @@+-- | Coverage for 'Pqi.prepare': preparing a named statement, the result+-- it reports, and duplicate-name handling.+module Pqi.Conformance.Operation.Prepare+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "prepare" do+    it "reports its own result" \conninfo ->+      differential proxy conninfo \connection ->+        prepare connection "conformance_prep_result" "select $1 :: int4" Nothing+          >>= traverse observeResult++    it "rejects a duplicate statement name" \conninfo ->+      differential proxy conninfo \connection -> do+        first <-+          prepare connection "conformance_dup" "select 1" Nothing >>= traverse observeResult+        second <-+          prepare connection "conformance_dup" "select 2" Nothing >>= traverse observeResult+        pure (first, second)
+ src/library/Pqi/Conformance/Operation/ProtocolVersion.hs view
@@ -0,0 +1,17 @@+-- | Coverage for 'Pqi.protocolVersion': the frontend/backend protocol+-- version.+module Pqi.Conformance.Operation.ProtocolVersion+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "protocolVersion" do+    it "reports the protocol version" \conninfo ->+      differential proxy conninfo protocolVersion
+ src/library/Pqi/Conformance/Operation/PutCopyData.hs view
@@ -0,0 +1,36 @@+-- | Coverage for 'Pqi.putCopyData': streaming rows into a+-- @COPY FROM STDIN@, including malformed data that the server rejects at end.+module Pqi.Conformance.Operation.PutCopyData+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults, execScenario)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "putCopyData" do+    it "streams rows into a COPY FROM STDIN" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "create temporary table conformance_copy (id int4, label text)"+        started <- execScenario "copy conformance_copy from stdin" connection+        firstRow <- putCopyData connection "1\thello\n"+        secondRow <- putCopyData connection "2\tworld\n"+        ended <- putCopyEnd connection Nothing+        outcome <- drainResults connection+        check <-+          execScenario "select count(*), min(label), max(label) from conformance_copy" connection+        pure (started, firstRow, secondRow, ended, outcome, check)++    it "feeds malformed data that the server rejects" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "create temporary table conformance_copy_bad (id int4)"+        started <- execScenario "copy conformance_copy_bad from stdin" connection+        row <- putCopyData connection "not-a-number\n"+        ended <- putCopyEnd connection Nothing+        outcome <- drainResults connection+        pure (started, row, ended, outcome)
+ src/library/Pqi/Conformance/Operation/PutCopyEnd.hs view
@@ -0,0 +1,35 @@+-- | Coverage for 'Pqi.putCopyEnd': finishing a @COPY FROM STDIN@, both+-- committing the rows and aborting the copy with a client error.+module Pqi.Conformance.Operation.PutCopyEnd+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults, execScenario)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "putCopyEnd" do+    it "commits the copied rows when ended without an error" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "create temporary table conformance_copy_end (id int4)"+        started <- execScenario "copy conformance_copy_end from stdin" connection+        row <- putCopyData connection "1\n"+        ended <- putCopyEnd connection Nothing+        outcome <- drainResults connection+        check <- execScenario "select count(*) from conformance_copy_end" connection+        pure (started, row, ended, outcome, check)++    it "aborts the copy when ended with an error" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "create temporary table conformance_copy_abort (id int4)"+        started <- execScenario "copy conformance_copy_abort from stdin" connection+        row <- putCopyData connection "1\n"+        ended <- putCopyEnd connection (Just "conformance abort")+        outcome <- drainResults connection+        check <- execScenario "select count(*) from conformance_copy_abort" connection+        pure (started, row, ended, outcome, check)
+ src/library/Pqi/Conformance/Operation/Reset.hs view
@@ -0,0 +1,26 @@+-- | Coverage for 'Pqi.reset': a blocking reset restores a fresh session,+-- discarding transaction state and prepared statements.+module Pqi.Conformance.Operation.Reset+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "reset" do+    it "restores a fresh session" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- prepare connection "conformance_reset" "select 1" Nothing+        _ <- exec connection "begin"+        inTransaction <- transactionStatus connection+        reset connection+        afterReset <- observeConnection connection+        -- The prepared statement must be gone in the fresh session.+        describeAfter <- describePrepared connection "conformance_reset" >>= traverse observeResult+        pure (inTransaction, afterReset, describeAfter)
+ src/library/Pqi/Conformance/Operation/ResetPoll.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.resetPoll': driving an asynchronous reset forward+-- until it reports a terminal polling status.+module Pqi.Conformance.Operation.ResetPoll+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (pollUntilDone)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "resetPoll" do+    it "reaches a terminal polling status and a ready connection" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "begin"+        started <- resetStart connection+        terminal <- pollUntilDone (resetPoll connection)+        afterStatus <- transactionStatus connection+        pure (started, terminal, afterStatus)
+ src/library/Pqi/Conformance/Operation/ResetStart.hs view
@@ -0,0 +1,24 @@+-- | Coverage for 'Pqi.resetStart': beginning an asynchronous reset that+-- is then driven to completion with 'Pqi.resetPoll'.+module Pqi.Conformance.Operation.ResetStart+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (pollUntilDone)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "resetStart" do+    it "begins an asynchronous reset that polls to a fresh session" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "begin"+        started <- resetStart connection+        polled <- pollUntilDone (resetPoll connection)+        afterReset <- observeConnection connection+        pure (started, polled, afterReset)
+ src/library/Pqi/Conformance/Operation/ResultErrorField.hs view
@@ -0,0 +1,46 @@+-- | Coverage for 'Pqi.resultErrorField': the structured fields carried by+-- a wire error response — SQLSTATE, severity, primary message, detail, hint,+-- positions, internal query, context, and source location.+--+-- These all come from the wire error response and are compared in full+-- (via 'Pqi.Conformance.Observation.observeResult', which captures every+-- 'Pqi.FieldCode').+module Pqi.Conformance.Operation.ResultErrorField+  ( spec,+  )+where++import Pqi (IsConnection)+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (execAllScenario, execScenario)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "resultErrorField" do+    let forCase title sql =+          it title \conninfo -> differential proxy conninfo (execScenario sql)+    forCase "syntax error" "selct 1"+    forCase "undefined table" "select * from pqi_no_such_table"+    forCase "undefined column" "select no_such_column from (select 1) as t"+    forCase "division by zero" "select 1 / 0"+    forCase "statement position past a prefix" "select 1 where tlse"+    forCase+      "detail, hint, and a custom errcode"+      "do $$ begin raise exception 'boom' using detail = 'the detail', hint = 'the hint', errcode = 'P0123'; end $$"+    forCase "internal query and position" "do $$ begin execute 'selct 1'; end $$"+    forCase "value too long" "select 'abc' :: varchar(2)"++    it "constraint violations" \conninfo ->+      differential proxy conninfo+        $ execAllScenario+          [ "create temporary table conformance_errors (id int4 primary key, label text not null)",+            "insert into conformance_errors values (1, 'a')",+            "insert into conformance_errors values (1, 'b')",+            "insert into conformance_errors values (2, null)"+          ]++    it "a failed transaction block rejects further commands" \conninfo ->+      differential proxy conninfo+        $ execAllScenario ["begin", "select 1 / 0", "select 1", "rollback", "select 1"]
+ src/library/Pqi/Conformance/Operation/ResultErrorMessage.hs view
@@ -0,0 +1,31 @@+-- | Coverage for 'Pqi.resultErrorMessage': the formatted error string on a+-- failed result, and empty on a successful one.+--+-- The message is compared byte-identically: both adapters must produce the+-- same formatted string as libpq's @PQresultErrorMessage@ at DEFAULT+-- verbosity. The failure scenario uses a @RAISE EXCEPTION@ from a PL\/pgSQL+-- anonymous block, which produces an error without a statement-position field+-- (@'P'@), so the formatted string is fully reproducible from the wire error+-- fields alone.+module Pqi.Conformance.Operation.ResultErrorMessage+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "resultErrorMessage" do+    it "matches the full formatted libpq error string on failure and is empty on success" \conninfo ->+      differential proxy conninfo \connection -> do+        failed <-+          exec connection "do $$ begin raise exception 'conformance error'; end $$"+            >>= traverse resultErrorMessage+        succeeded <-+          exec connection "select 1"+            >>= traverse resultErrorMessage+        pure (failed, succeeded)
+ src/library/Pqi/Conformance/Operation/ResultStatus.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.resultStatus': the status reported for each kind of+-- result — tuples, a command, an empty query, and a failure.+module Pqi.Conformance.Operation.ResultStatus+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "resultStatus" do+    it "reports the status for each kind of result" \conninfo ->+      differential proxy conninfo \connection -> do+        let statusOf sql = exec connection sql >>= traverse resultStatus+        tuples <- statusOf "select 1"+        command <- statusOf "create temporary table conformance_status (id int4)"+        empty <- statusOf ""+        failed <- statusOf "selct 1"+        pure (tuples, command, empty, failed)
+ src/library/Pqi/Conformance/Operation/SendDescribePortal.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.sendDescribePortal': asynchronously describing a+-- declared cursor's portal.+module Pqi.Conformance.Operation.SendDescribePortal+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "sendDescribePortal" do+    it "describes a declared cursor asynchronously" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- exec connection "begin"+        _ <- exec connection "declare conformance_async_cursor cursor for select 1 :: int4 as n"+        sent <- sendDescribePortal connection "conformance_async_cursor"+        results <- drainResults connection+        pure (sent, results)
+ src/library/Pqi/Conformance/Operation/SendDescribePrepared.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.sendDescribePrepared': asynchronously describing a+-- prepared statement.+module Pqi.Conformance.Operation.SendDescribePrepared+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "sendDescribePrepared" do+    it "describes a prepared statement asynchronously" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- sendPrepare connection "conformance_send_desc" "select $1 :: int4 * 2" Nothing+        _ <- drainResults connection+        sent <- sendDescribePrepared connection "conformance_send_desc"+        results <- drainResults connection+        pure (sent, results)
+ src/library/Pqi/Conformance/Operation/SendFlushRequest.hs view
@@ -0,0 +1,27 @@+-- | Coverage for 'Pqi.sendFlushRequest': asking the server to flush its+-- output buffer so pipelined results arrive without a 'Pqi.pipelineSync'.+module Pqi.Conformance.Operation.SendFlushRequest+  ( spec,+  )+where++import Pqi (IsConnection (..))+import qualified Pqi as Lq+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (takeCommandResults, takeResult)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "sendFlushRequest" do+    it "delivers results without a sync" \conninfo ->+      differential proxy conninfo \connection -> do+        entered <- enterPipelineMode connection+        sent <- sendQueryParams connection "select 42" [] Lq.Text+        flushRequested <- sendFlushRequest connection+        results <- takeCommandResults connection+        synced <- pipelineSync connection+        syncResult <- takeResult connection+        exited <- exitPipelineMode connection+        pure (entered, sent, flushRequested, results, synced, syncResult, exited)
+ src/library/Pqi/Conformance/Operation/SendPrepare.hs view
@@ -0,0 +1,21 @@+-- | Coverage for 'Pqi.sendPrepare': asynchronously preparing a named+-- statement and collecting its result.+module Pqi.Conformance.Operation.SendPrepare+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "sendPrepare" do+    it "prepares a statement asynchronously" \conninfo ->+      differential proxy conninfo \connection -> do+        sent <- sendPrepare connection "conformance_send_prepare" "select $1 :: int4 * 2" Nothing+        results <- drainResults connection+        pure (sent, results)
+ src/library/Pqi/Conformance/Operation/SendQuery.hs view
@@ -0,0 +1,30 @@+-- | Coverage for 'Pqi.sendQuery': submitting a command without waiting,+-- then collecting its results, including multi-statement scripts, a mid-script+-- error, and the empty query.+module Pqi.Conformance.Operation.SendQuery+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "sendQuery" do+    let sendAndDrain sql conninfo =+          differential proxy conninfo \connection -> do+            sent <- sendQuery connection sql+            results <- drainResults connection+            pure (sent, results)+    it "sends a query and collects its result"+      $ sendAndDrain "select i, i * 10 from generate_series (1, 3) as i"+    it "yields multiple results for multiple statements"+      $ sendAndDrain "select 1; select 2, 3; select 4"+    it "ends the results at a mid-script error"+      $ sendAndDrain "select 1; select 1 / 0; select 3"+    it "handles an empty string"+      $ sendAndDrain ""
+ src/library/Pqi/Conformance/Operation/SendQueryParams.hs view
@@ -0,0 +1,27 @@+-- | Coverage for 'Pqi.sendQueryParams': the asynchronous parameterized+-- query, including a null parameter.+module Pqi.Conformance.Operation.SendQueryParams+  ( spec,+  )+where++import Pqi (IsConnection (..))+import qualified Pqi as Lq+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults, int4Oid)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "sendQueryParams" do+    it "sends a parameterized query and collects its result" \conninfo ->+      differential proxy conninfo \connection -> do+        sent <-+          sendQueryParams+            connection+            "select $1 :: int4 + $2 :: int4, $3 :: text"+            [Just (int4Oid, "40", Lq.Text), Just (int4Oid, "2", Lq.Text), Nothing]+            Lq.Text+        results <- drainResults connection+        pure (sent, results)
+ src/library/Pqi/Conformance/Operation/SendQueryPrepared.hs view
@@ -0,0 +1,24 @@+-- | Coverage for 'Pqi.sendQueryPrepared': asynchronously executing a+-- previously prepared statement.+module Pqi.Conformance.Operation.SendQueryPrepared+  ( spec,+  )+where++import Pqi (IsConnection (..))+import qualified Pqi as Lq+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "sendQueryPrepared" do+    it "executes a prepared statement asynchronously" \conninfo ->+      differential proxy conninfo \connection -> do+        _ <- sendPrepare connection "conformance_send_exec" "select $1 :: int4 * 2" Nothing+        _ <- drainResults connection+        sent <- sendQueryPrepared connection "conformance_send_exec" [Just ("21", Lq.Text)] Lq.Text+        results <- drainResults connection+        pure (sent, results)
+ src/library/Pqi/Conformance/Operation/ServerVersion.hs view
@@ -0,0 +1,17 @@+-- | Coverage for 'Pqi.serverVersion': the server version as an @MMmmpp@+-- integer.+module Pqi.Conformance.Operation.ServerVersion+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "serverVersion" do+    it "reports the server version as an integer" \conninfo ->+      differential proxy conninfo serverVersion
+ src/library/Pqi/Conformance/Operation/SetClientEncoding.hs view
@@ -0,0 +1,23 @@+-- | Coverage for 'Pqi.setClientEncoding': changing the client encoding,+-- including rejection of an unknown encoding (which leaves the session intact).+module Pqi.Conformance.Operation.SetClientEncoding+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (execScenario)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "setClientEncoding" do+    it "rejects an unknown encoding and leaves the session usable" \conninfo ->+      differential proxy conninfo \connection -> do+        rejected <- setClientEncoding connection "BOGUS_ENCODING"+        unchanged <- clientEncoding connection+        stillIdle <- transactionStatus connection+        stillWorks <- execScenario "select 1" connection+        pure (rejected, unchanged, stillIdle, stillWorks)
+ src/library/Pqi/Conformance/Operation/SetErrorVerbosity.hs view
@@ -0,0 +1,22 @@+-- | Coverage for 'Pqi.setErrorVerbosity': setting error verbosity, which+-- returns the previous setting.+module Pqi.Conformance.Operation.SetErrorVerbosity+  ( spec,+  )+where++import Pqi (IsConnection (..))+import qualified Pqi as Lq+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "setErrorVerbosity" do+    it "returns the previous setting" \conninfo ->+      differential proxy conninfo \connection -> do+        beforeTerse <- setErrorVerbosity connection Lq.ErrorsTerse+        beforeVerbose <- setErrorVerbosity connection Lq.ErrorsVerbose+        beforeDefault <- setErrorVerbosity connection Lq.ErrorsDefault+        pure (beforeTerse, beforeVerbose, beforeDefault)
+ src/library/Pqi/Conformance/Operation/SetSingleRowMode.hs view
@@ -0,0 +1,29 @@+-- | Coverage for 'Pqi.setSingleRowMode': delivering the rows of the+-- currently executing query one result at a time.+module Pqi.Conformance.Operation.SetSingleRowMode+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (drainResults)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "setSingleRowMode" do+    it "splits a multi-row result into single-row results" \conninfo ->+      differential proxy conninfo \connection -> do+        sent <- sendQuery connection "select i from generate_series (1, 3) as i"+        singleRow <- setSingleRowMode connection+        results <- drainResults connection+        pure (sent, singleRow, results)++    it "handles an empty result" \conninfo ->+      differential proxy conninfo \connection -> do+        sent <- sendQuery connection "select 1 where false"+        singleRow <- setSingleRowMode connection+        results <- drainResults connection+        pure (sent, singleRow, results)
+ src/library/Pqi/Conformance/Operation/Setnonblocking.hs view
@@ -0,0 +1,22 @@+-- | Coverage for 'Pqi.setnonblocking': toggling the connection's+-- non-blocking flag.+module Pqi.Conformance.Operation.Setnonblocking+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "setnonblocking" do+    it "turns the non-blocking flag on and off" \conninfo ->+      differential proxy conninfo \connection -> do+        setOn <- setnonblocking connection True+        nowOn <- isnonblocking connection+        setOff <- setnonblocking connection False+        nowOff <- isnonblocking connection+        pure (setOn, nowOn, setOff, nowOff)
+ src/library/Pqi/Conformance/Operation/Socket.hs view
@@ -0,0 +1,20 @@+-- | Coverage for 'Pqi.socket': the connection's socket file descriptor.+--+-- The descriptor number itself is per-connection identity, so only its+-- presence on an open connection is compared.+module Pqi.Conformance.Operation.Socket+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "socket" do+    it "is present on an open connection" \conninfo ->+      differential proxy conninfo \connection ->+        isJust <$> socket connection
+ src/library/Pqi/Conformance/Operation/Status.hs view
@@ -0,0 +1,16 @@+-- | Coverage for 'Pqi.status': the current connection status.+module Pqi.Conformance.Operation.Status+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "status" do+    it "reports a ready connection as OK" \conninfo ->+      differential proxy conninfo status
+ src/library/Pqi/Conformance/Operation/TransactionStatus.hs view
@@ -0,0 +1,25 @@+-- | Coverage for 'Pqi.transactionStatus': in-transaction status tracking+-- across a successful command, a failed command, and a rollback.+module Pqi.Conformance.Operation.TransactionStatus+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "transactionStatus" do+    it "tracks status through a transaction" \conninfo ->+      differential proxy conninfo \connection -> do+        idle <- transactionStatus connection+        _ <- exec connection "begin"+        inTransaction <- transactionStatus connection+        _ <- exec connection "select 1 / 0"+        inError <- transactionStatus connection+        _ <- exec connection "rollback"+        afterRollback <- transactionStatus connection+        pure (idle, inTransaction, inError, afterRollback)
+ src/library/Pqi/Conformance/Operation/UnsafeFreeResult.hs view
@@ -0,0 +1,26 @@+-- | Coverage for 'Pqi.unsafeFreeResult': freeing a result leaves the+-- connection usable for subsequent commands.+--+-- An observation of a freed result must not be compared: with the C-backed+-- adapter the accessors share the @PGresult@'s storage, so freeing invalidates+-- previously returned bytes. The example therefore frees and then runs a fresh+-- query, observing only the latter.+module Pqi.Conformance.Operation.UnsafeFreeResult+  ( spec,+  )+where++import Pqi (IsConnection (..), IsResult (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario (execScenario)+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "unsafeFreeResult" do+    it "leaves the connection usable" \conninfo ->+      differential proxy conninfo \connection -> do+        result <- exec connection "select 1"+        traverse_ unsafeFreeResult result+        execScenario "select 2" connection
+ src/library/Pqi/Conformance/Operation/User.hs view
@@ -0,0 +1,16 @@+-- | Coverage for 'Pqi.user': the user name of the connection.+module Pqi.Conformance.Operation.User+  ( spec,+  )+where++import Pqi (IsConnection (..))+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import Test.Hspec++spec :: (IsConnection c) => Proxy c -> SpecWith ByteString+spec proxy =+  describe "user" do+    it "reports the user name from the conninfo" \conninfo ->+      differential proxy conninfo user
+ src/library/Pqi/Conformance/Prelude.hs view
@@ -0,0 +1,18 @@+module Pqi.Conformance.Prelude+  ( module Exports,+  )+where++import Control.Applicative as Exports+import Control.Concurrent as Exports (threadDelay)+import Control.Monad as Exports+import Data.ByteString as Exports (ByteString)+import Data.Either as Exports (isLeft, isRight)+import Data.Foldable as Exports (for_, traverse_)+import Data.Int as Exports+import Data.Maybe as Exports+import Data.Proxy as Exports (Proxy (..))+import Data.Text as Exports (Text)+import Data.Traversable as Exports (for)+import Data.Word as Exports+import Prelude as Exports
+ src/library/Pqi/Conformance/Reference.hs view
@@ -0,0 +1,299 @@+-- | The reference connection: a direct @postgresql-libpq@ wrapper used as the+-- ground truth in differential tests. It is intentionally independent of the+-- @pqi-ffi@ adapter so that adapter test suites can depend on+-- @pqi-conformance@ without a circular dependency through @pqi-ffi@.+module Pqi.Conformance.Reference+  ( Reference (..),+    RefResult (..),+    RefCancel (..),+  )+where++import qualified Database.PostgreSQL.LibPQ as LibPQ+import Pqi+  ( IsCancel (..),+    IsConnection (..),+    IsResult (..),+  )+import qualified Pqi+import Pqi.Conformance.Prelude++-- | A direct wrapper over a @postgresql-libpq@ connection, used as the+-- differential-testing reference.+newtype Reference = Reference LibPQ.Connection++-- | A result backed by a C @PGresult@.+newtype RefResult = RefResult LibPQ.Result++-- | A cancellation handle backed by a C @PGcancel@.+newtype RefCancel = RefCancel LibPQ.Cancel++instance IsResult RefResult where+  resultStatus (RefResult r) = fromExecStatus <$> LibPQ.resultStatus r+  resultErrorMessage (RefResult r) = LibPQ.resultErrorMessage r+  resultErrorField (RefResult r) field = LibPQ.resultErrorField r (toFieldCode field)+  unsafeFreeResult (RefResult r) = LibPQ.unsafeFreeResult r+  ntuples (RefResult r) = fromRow <$> LibPQ.ntuples r+  nfields (RefResult r) = fromColumn <$> LibPQ.nfields r+  fname (RefResult r) column = LibPQ.fname r (toColumn column)+  fnumber (RefResult r) name = fmap fromColumn <$> LibPQ.fnumber r name+  ftable (RefResult r) column = fromOid <$> LibPQ.ftable r (toColumn column)+  ftablecol (RefResult r) column = fromColumn <$> LibPQ.ftablecol r (toColumn column)+  fformat (RefResult r) column = fromFormat <$> LibPQ.fformat r (toColumn column)+  ftype (RefResult r) column = fromOid <$> LibPQ.ftype r (toColumn column)+  fmod (RefResult r) column = LibPQ.fmod r (toColumn column)+  fsize (RefResult r) column = LibPQ.fsize r (toColumn column)+  getvalue (RefResult r) row column = LibPQ.getvalue r (toRow row) (toColumn column)+  getvalue' (RefResult r) row column = LibPQ.getvalue' r (toRow row) (toColumn column)+  getisnull (RefResult r) row column = LibPQ.getisnull r (toRow row) (toColumn column)+  getlength (RefResult r) row column = LibPQ.getlength r (toRow row) (toColumn column)+  nparams (RefResult r) = fromIntegral <$> LibPQ.nparams r+  paramtype (RefResult r) index = fromOid <$> LibPQ.paramtype r (fromIntegral index)+  cmdStatus (RefResult r) = LibPQ.cmdStatus r+  cmdTuples (RefResult r) = LibPQ.cmdTuples r++instance IsCancel RefCancel where+  cancel (RefCancel handle) = LibPQ.cancel handle++instance IsConnection Reference where+  type ResultOf Reference = RefResult+  type CancelOf Reference = RefCancel++  connectdb conninfo = Reference <$> LibPQ.connectdb conninfo+  connectStart conninfo = Reference <$> LibPQ.connectStart conninfo+  connectPoll (Reference c) = fromPollingStatus <$> LibPQ.connectPoll c+  newNullConnection = Reference <$> LibPQ.newNullConnection+  isNullConnection (Reference c) = LibPQ.isNullConnection c+  finish (Reference c) = LibPQ.finish c+  reset (Reference c) = LibPQ.reset c+  resetStart (Reference c) = LibPQ.resetStart c+  resetPoll (Reference c) = fromPollingStatus <$> LibPQ.resetPoll c+  db (Reference c) = LibPQ.db c+  user (Reference c) = LibPQ.user c+  pass (Reference c) = LibPQ.pass c+  host (Reference c) = LibPQ.host c+  port (Reference c) = LibPQ.port c+  options (Reference c) = LibPQ.options c+  status (Reference c) = fromConnStatus <$> LibPQ.status c+  transactionStatus (Reference c) = fromTransactionStatus <$> LibPQ.transactionStatus c+  parameterStatus (Reference c) name = LibPQ.parameterStatus c name+  protocolVersion (Reference c) = LibPQ.protocolVersion c+  serverVersion (Reference c) = LibPQ.serverVersion c+  errorMessage (Reference c) = LibPQ.errorMessage c+  socket (Reference c) = LibPQ.socket c+  backendPID (Reference c) = fromIntegral <$> LibPQ.backendPID c+  connectionNeedsPassword (Reference c) = LibPQ.connectionNeedsPassword c+  connectionUsedPassword (Reference c) = LibPQ.connectionUsedPassword c++  exec (Reference c) sql =+    fmap RefResult <$> LibPQ.exec c sql+  execParams (Reference c) sql params resultFormat =+    fmap RefResult <$> LibPQ.execParams c sql (fmap (fmap toParam) params) (toFormat resultFormat)+  prepare (Reference c) name sql paramTypes =+    fmap RefResult <$> LibPQ.prepare c name sql (fmap (fmap toOid) paramTypes)+  execPrepared (Reference c) name params resultFormat =+    fmap RefResult <$> LibPQ.execPrepared c name (fmap (fmap toBoundParam) params) (toFormat resultFormat)+  describePrepared (Reference c) name =+    fmap RefResult <$> LibPQ.describePrepared c name+  describePortal (Reference c) name =+    fmap RefResult <$> LibPQ.describePortal c name++  escapeStringConn (Reference c) = LibPQ.escapeStringConn c+  escapeByteaConn (Reference c) = LibPQ.escapeByteaConn c+  escapeIdentifier (Reference c) = LibPQ.escapeIdentifier c++  sendQuery (Reference c) sql = LibPQ.sendQuery c sql+  sendQueryParams (Reference c) sql params resultFormat =+    LibPQ.sendQueryParams c sql (fmap (fmap toParam) params) (toFormat resultFormat)+  sendPrepare (Reference c) name sql paramTypes =+    LibPQ.sendPrepare c name sql (fmap (fmap toOid) paramTypes)+  sendQueryPrepared (Reference c) name params resultFormat =+    LibPQ.sendQueryPrepared c name (fmap (fmap toBoundParam) params) (toFormat resultFormat)+  sendDescribePrepared (Reference c) name = LibPQ.sendDescribePrepared c name+  sendDescribePortal (Reference c) name = LibPQ.sendDescribePortal c name+  getResult (Reference c) = fmap RefResult <$> LibPQ.getResult c+  consumeInput (Reference c) = LibPQ.consumeInput c+  isBusy (Reference c) = LibPQ.isBusy c+  setnonblocking (Reference c) nonBlocking = LibPQ.setnonblocking c nonBlocking+  isnonblocking (Reference c) = LibPQ.isnonblocking c+  setSingleRowMode (Reference c) = LibPQ.setSingleRowMode c+  flush (Reference c) = fromFlushStatus <$> LibPQ.flush c++  pipelineStatus (Reference c) = fromPipelineStatus <$> LibPQ.pipelineStatus c+  enterPipelineMode (Reference c) = LibPQ.enterPipelineMode c+  exitPipelineMode (Reference c) = LibPQ.exitPipelineMode c+  pipelineSync (Reference c) = LibPQ.pipelineSync c+  sendFlushRequest (Reference c) = LibPQ.sendFlushRequest c++  getCancel (Reference c) = fmap RefCancel <$> LibPQ.getCancel c++  notifies (Reference c) = fmap fromNotify <$> LibPQ.notifies c+  disableNoticeReporting (Reference c) = LibPQ.disableNoticeReporting c+  enableNoticeReporting (Reference c) = LibPQ.enableNoticeReporting c+  getNotice (Reference c) = LibPQ.getNotice c++  putCopyData (Reference c) value = fromCopyInResult <$> LibPQ.putCopyData c value+  putCopyEnd (Reference c) reason = fromCopyInResult <$> LibPQ.putCopyEnd c reason+  getCopyData (Reference c) nonBlocking = fromCopyOutResult <$> LibPQ.getCopyData c nonBlocking++  loCreat (Reference c) = fmap fromOid <$> LibPQ.loCreat c+  loCreate (Reference c) oid = fmap fromOid <$> LibPQ.loCreate c (toOid oid)+  loImport (Reference c) path = fmap fromOid <$> LibPQ.loImport c path+  loImportWithOid (Reference c) path oid = fmap fromOid <$> LibPQ.loImportWithOid c path (toOid oid)+  loExport (Reference c) oid path = LibPQ.loExport c (toOid oid) path+  loOpen (Reference c) oid mode = fmap fromLibPQLoFd <$> LibPQ.loOpen c (toOid oid) mode+  loWrite (Reference c) fd value = LibPQ.loWrite c (toLibPQLoFd fd) value+  loRead (Reference c) fd len = LibPQ.loRead c (toLibPQLoFd fd) len+  loSeek (Reference c) fd mode offset = LibPQ.loSeek c (toLibPQLoFd fd) mode offset+  loTell (Reference c) fd = LibPQ.loTell c (toLibPQLoFd fd)+  loTruncate (Reference c) fd len = LibPQ.loTruncate c (toLibPQLoFd fd) len+  loClose (Reference c) fd = LibPQ.loClose c (toLibPQLoFd fd)+  loUnlink (Reference c) oid = LibPQ.loUnlink c (toOid oid)++  clientEncoding (Reference c) = LibPQ.clientEncoding c+  setClientEncoding (Reference c) encoding = LibPQ.setClientEncoding c encoding+  setErrorVerbosity (Reference c) verbosity =+    fromVerbosity <$> LibPQ.setErrorVerbosity c (toVerbosity verbosity)++-- * Type conversions++toParam :: (Word32, ByteString, Pqi.Format) -> (LibPQ.Oid, ByteString, LibPQ.Format)+toParam (oid, value, format) = (toOid oid, value, toFormat format)++toBoundParam :: (ByteString, Pqi.Format) -> (ByteString, LibPQ.Format)+toBoundParam (value, format) = (value, toFormat format)++toOid :: Word32 -> LibPQ.Oid+toOid = LibPQ.Oid . fromIntegral++fromOid :: LibPQ.Oid -> Word32+fromOid (LibPQ.Oid value) = fromIntegral value++toRow :: Int32 -> LibPQ.Row+toRow = LibPQ.toRow++fromRow :: LibPQ.Row -> Int32+fromRow = fromIntegral . fromEnum++toColumn :: Int32 -> LibPQ.Column+toColumn = LibPQ.toColumn++fromColumn :: LibPQ.Column -> Int32+fromColumn = fromIntegral . fromEnum++toLibPQLoFd :: Int32 -> LibPQ.LoFd+toLibPQLoFd = LibPQ.LoFd . fromIntegral++fromLibPQLoFd :: LibPQ.LoFd -> Int32+fromLibPQLoFd (LibPQ.LoFd fd) = fromIntegral fd++fromNotify :: LibPQ.Notify -> Pqi.Notify+fromNotify notification =+  Pqi.Notify+    { Pqi.relname = LibPQ.notifyRelname notification,+      Pqi.bePid = fromIntegral (LibPQ.notifyBePid notification),+      Pqi.extra = LibPQ.notifyExtra notification+    }++toFormat :: Pqi.Format -> LibPQ.Format+toFormat = \case+  Pqi.Text -> LibPQ.Text+  Pqi.Binary -> LibPQ.Binary++fromFormat :: LibPQ.Format -> Pqi.Format+fromFormat = \case+  LibPQ.Text -> Pqi.Text+  LibPQ.Binary -> Pqi.Binary++fromExecStatus :: LibPQ.ExecStatus -> Pqi.ExecStatus+fromExecStatus = \case+  LibPQ.EmptyQuery -> Pqi.EmptyQuery+  LibPQ.CommandOk -> Pqi.CommandOk+  LibPQ.TuplesOk -> Pqi.TuplesOk+  LibPQ.CopyOut -> Pqi.CopyOut+  LibPQ.CopyIn -> Pqi.CopyIn+  LibPQ.CopyBoth -> Pqi.CopyBoth+  LibPQ.BadResponse -> Pqi.BadResponse+  LibPQ.NonfatalError -> Pqi.NonfatalError+  LibPQ.FatalError -> Pqi.FatalError+  LibPQ.SingleTuple -> Pqi.SingleTuple+  LibPQ.PipelineSync -> Pqi.PipelineSync+  LibPQ.PipelineAbort -> Pqi.PipelineAbort++fromConnStatus :: LibPQ.ConnStatus -> Pqi.ConnStatus+fromConnStatus = \case+  LibPQ.ConnectionOk -> Pqi.ConnectionOk+  LibPQ.ConnectionBad -> Pqi.ConnectionBad+  LibPQ.ConnectionStarted -> Pqi.ConnectionStarted+  LibPQ.ConnectionMade -> Pqi.ConnectionMade+  LibPQ.ConnectionAwaitingResponse -> Pqi.ConnectionAwaitingResponse+  LibPQ.ConnectionAuthOk -> Pqi.ConnectionAuthOk+  LibPQ.ConnectionSetEnv -> Pqi.ConnectionSetEnv+  LibPQ.ConnectionSSLStartup -> Pqi.ConnectionSSLStartup++fromTransactionStatus :: LibPQ.TransactionStatus -> Pqi.TransactionStatus+fromTransactionStatus = \case+  LibPQ.TransIdle -> Pqi.TransIdle+  LibPQ.TransActive -> Pqi.TransActive+  LibPQ.TransInTrans -> Pqi.TransInTrans+  LibPQ.TransInError -> Pqi.TransInError+  LibPQ.TransUnknown -> Pqi.TransUnknown++fromPollingStatus :: LibPQ.PollingStatus -> Pqi.PollingStatus+fromPollingStatus = \case+  LibPQ.PollingFailed -> Pqi.PollingFailed+  LibPQ.PollingReading -> Pqi.PollingReading+  LibPQ.PollingWriting -> Pqi.PollingWriting+  LibPQ.PollingOk -> Pqi.PollingOk++fromPipelineStatus :: LibPQ.PipelineStatus -> Pqi.PipelineStatus+fromPipelineStatus = \case+  LibPQ.PipelineOn -> Pqi.PipelineOn+  LibPQ.PipelineOff -> Pqi.PipelineOff+  LibPQ.PipelineAborted -> Pqi.PipelineAborted++fromFlushStatus :: LibPQ.FlushStatus -> Pqi.FlushStatus+fromFlushStatus = \case+  LibPQ.FlushOk -> Pqi.FlushOk+  LibPQ.FlushFailed -> Pqi.FlushFailed+  LibPQ.FlushWriting -> Pqi.FlushWriting++fromCopyInResult :: LibPQ.CopyInResult -> Pqi.CopyInResult+fromCopyInResult = \case+  LibPQ.CopyInOk -> Pqi.CopyInOk+  LibPQ.CopyInError -> Pqi.CopyInError+  LibPQ.CopyInWouldBlock -> Pqi.CopyInWouldBlock++fromCopyOutResult :: LibPQ.CopyOutResult -> Pqi.CopyOutResult+fromCopyOutResult = \case+  LibPQ.CopyOutRow value -> Pqi.CopyOutRow value+  LibPQ.CopyOutWouldBlock -> Pqi.CopyOutWouldBlock+  LibPQ.CopyOutDone -> Pqi.CopyOutDone+  LibPQ.CopyOutError -> Pqi.CopyOutError++toVerbosity :: Pqi.Verbosity -> LibPQ.Verbosity+toVerbosity = \case+  Pqi.ErrorsTerse -> LibPQ.ErrorsTerse+  Pqi.ErrorsDefault -> LibPQ.ErrorsDefault+  Pqi.ErrorsVerbose -> LibPQ.ErrorsVerbose++fromVerbosity :: LibPQ.Verbosity -> Pqi.Verbosity+fromVerbosity = \case+  LibPQ.ErrorsTerse -> Pqi.ErrorsTerse+  LibPQ.ErrorsDefault -> Pqi.ErrorsDefault+  LibPQ.ErrorsVerbose -> Pqi.ErrorsVerbose++toFieldCode :: Pqi.FieldCode -> LibPQ.FieldCode+toFieldCode = \case+  Pqi.DiagSeverity -> LibPQ.DiagSeverity+  Pqi.DiagSqlstate -> LibPQ.DiagSqlstate+  Pqi.DiagMessagePrimary -> LibPQ.DiagMessagePrimary+  Pqi.DiagMessageDetail -> LibPQ.DiagMessageDetail+  Pqi.DiagMessageHint -> LibPQ.DiagMessageHint+  Pqi.DiagStatementPosition -> LibPQ.DiagStatementPosition+  Pqi.DiagInternalPosition -> LibPQ.DiagInternalPosition+  Pqi.DiagInternalQuery -> LibPQ.DiagInternalQuery+  Pqi.DiagContext -> LibPQ.DiagContext+  Pqi.DiagSourceFile -> LibPQ.DiagSourceFile+  Pqi.DiagSourceLine -> LibPQ.DiagSourceLine+  Pqi.DiagSourceFunction -> LibPQ.DiagSourceFunction
+ src/library/Pqi/Conformance/Scenario.hs view
@@ -0,0 +1,155 @@+-- | Reusable scenario fragments shared by the per-operation spec modules.+--+-- These are the recurring building blocks — running a query and observing its+-- result, draining the asynchronous result stream, collecting a @COPY OUT@+-- stream, driving a polling loop to its terminal status, and the handful of+-- well-known type OIDs — factored out so each operation module stays focused on+-- the one operation it covers.+module Pqi.Conformance.Scenario+  ( -- * Running queries+    execScenario,+    execAllScenario,+    observed,++    -- * Asynchronous result collection+    drainResults,+    takeResult,+    takeCommandResults,++    -- * Copy and polling loops+    collectCopyOut,+    pollUntilDone,+    flushUntilDone,++    -- * Transactions+    inTransaction,++    -- * Well-known type OIDs+    boolOid,+    byteaOid,+    int2Oid,+    int4Oid,+    int8Oid,+    textOid,+    float8Oid,+  )+where++import Pqi+  ( CopyOutResult (..),+    FlushStatus (..),+    IsConnection (..),+    PollingStatus (..),+  )+import qualified Pqi as Lq+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude++-- | Run 'Pqi.exec' and observe its result (if any).+execScenario :: (IsConnection c) => ByteString -> c -> IO (Maybe ResultObservation)+execScenario sql connection = exec connection sql >>= traverse observeResult++-- | Run a sequence of statements with 'Pqi.exec', observing every result.+execAllScenario :: (IsConnection c) => [ByteString] -> c -> IO [Maybe ResultObservation]+execAllScenario sqls connection = traverse (`execScenario` connection) sqls++-- | Run 'Pqi.execParams' and observe its result (if any).+observed ::+  (IsConnection c) =>+  ByteString ->+  [Maybe (Word32, ByteString, Lq.Format)] ->+  Lq.Format ->+  c ->+  IO (Maybe ResultObservation)+observed sql params resultFormat connection =+  execParams connection sql params resultFormat >>= traverse observeResult++-- | Collect and observe results with 'Pqi.getResult' until it reports+-- completion with 'Nothing'.+drainResults :: (IsConnection c) => c -> IO [ResultObservation]+drainResults connection = go []+  where+    go acc =+      getResult connection >>= \case+        Nothing -> pure (reverse acc)+        Just result -> do+          observation <- observeResult result+          go (observation : acc)++-- | One 'Pqi.getResult' step, observed.+takeResult :: (IsConnection c) => c -> IO (Maybe ResultObservation)+takeResult connection = getResult connection >>= traverse observeResult++-- | The results of one pipelined command: its result and the 'Nothing'+-- separator that ends it.+takeCommandResults ::+  (IsConnection c) => c -> IO (Maybe ResultObservation, Maybe ResultObservation)+takeCommandResults connection = do+  result <- takeResult connection+  separator <- takeResult connection+  pure (result, separator)++-- | Collect blocking 'Pqi.getCopyData' outcomes until the stream reports+-- anything other than a row (normally 'CopyOutDone'), including that final+-- outcome.+collectCopyOut :: (IsConnection c) => c -> IO [CopyOutResult]+collectCopyOut connection = go (1000 :: Int) []+  where+    go 0 acc = pure (reverse acc)+    go n acc =+      getCopyData connection False >>= \case+        CopyOutRow row -> go (n - 1) (CopyOutRow row : acc)+        terminal -> pure (reverse (terminal : acc))++-- | Drive a polling loop to its terminal status, spinning with a small delay+-- instead of waiting on the socket. Bails out as failed after ten seconds.+pollUntilDone :: IO PollingStatus -> IO PollingStatus+pollUntilDone poll = go (10000 :: Int)+  where+    go 0 = pure PollingFailed+    go n =+      poll >>= \case+        PollingReading -> threadDelay 1000 >> go (n - 1)+        PollingWriting -> threadDelay 1000 >> go (n - 1)+        terminal -> pure terminal++-- | Drive 'Pqi.flush' to a terminal status, spinning while it reports+-- 'FlushWriting'. Bails out as failed after ten seconds.+flushUntilDone :: IO FlushStatus -> IO FlushStatus+flushUntilDone doFlush = go (10000 :: Int)+  where+    go 0 = pure FlushFailed+    go n =+      doFlush >>= \case+        FlushWriting -> threadDelay 1000 >> go (n - 1)+        terminal -> pure terminal++-- | Run an action between @begin@ and @commit@. Large-object operations must+-- run inside a transaction block.+inTransaction :: (IsConnection c) => c -> IO a -> IO a+inTransaction connection action = do+  _ <- exec connection "begin"+  result <- action+  _ <- exec connection "commit"+  pure result++boolOid :: Word32+boolOid = 16++byteaOid :: Word32+byteaOid = 17++int2Oid :: Word32+int2Oid = 21++int4Oid :: Word32+int4Oid = 23++int8Oid :: Word32+int8Oid = 20++textOid :: Word32+textOid = 25++float8Oid :: Word32+float8Oid = 701