packages feed

pqi-conformance-1.0.6.0: src/library/Pqi/Conformance/Operation/PipelineSync/Desync.hs

-- | Reproduces
-- <https://github.com/nikita-volkov/pqi-native/issues/9>: after a pipelined
-- statement fails with a server error, a command still queued behind it in
-- that same pipeline is discarded by the server without any response at
-- all (per the pipelining protocol, everything after the failure is
-- skipped until the next @Sync@). A later pipeline sent on the same
-- connection can then misattribute its own @ParseComplete@ to that silently
-- discarded command instead of to itself.
--
-- The discarded command needs to be sent via 'Lq.sendPrepare' specifically:
-- a discarded 'Lq.sendQueryParams' command leaks the same way internally,
-- but every leaked entry it produces is indistinguishable from a correctly
-- popped one, so the symptom never surfaces. A discarded 'Lq.sendPrepare'
-- leaks an entry that, if later popped by an unrelated command's
-- @ParseComplete@, makes that command terminate immediately as
-- 'Lq.CommandOk' instead of collecting its real result - observable here as
-- the second pipeline's plain @SELECT@ coming back 'Lq.CommandOk' instead of
-- 'Lq.TuplesOk' with an empty row list.
module Pqi.Conformance.Operation.PipelineSync.Desync
  ( spec,
  )
where

import qualified Pqi
import qualified Pqi as Lq
import Pqi.Conformance.Harness
import Pqi.Conformance.Prelude
import Pqi.Conformance.Scenario (takeCommandResults, takeResult)
import Test.Hspec

spec :: Pqi.Adapter -> SpecWith ByteString
spec adapter =
  describe "desync after a mid-pipeline server error" do
    it "a later pipeline's results are not misattributed" \conninfo ->
      differential adapter conninfo \connection -> do
        entered <- Lq.enterPipelineMode connection

        -- First pipeline: a statement that fails with a server error,
        -- followed by a named-statement prepare that gets silently
        -- discarded as a consequence (the server sends nothing for it).
        failingSent <- Lq.sendQueryParams connection "select 1 / 0" [] Lq.Text
        discardedSent <- Lq.sendPrepare connection "" "select 99" Nothing
        firstSynced <- Lq.pipelineSync connection
        failed <- takeCommandResults connection
        discarded <- takeCommandResults connection
        firstSyncResult <- takeResult connection

        -- Second pipeline, on the same connection: a plain SELECT that must
        -- come back as TuplesOk with an empty row list, not CommandOk.
        selectSent <- Lq.sendQueryParams connection "select 1 where false" [] Lq.Text
        secondSynced <- Lq.pipelineSync connection
        selected <- takeCommandResults connection
        secondSyncResult <- takeResult connection

        exited <- Lq.exitPipelineMode connection
        pure
          ( entered,
            failingSent,
            discardedSent,
            firstSynced,
            failed,
            discarded,
            firstSyncResult,
            selectSent,
            secondSynced,
            selected,
            secondSyncResult,
            exited
          )