pqi-conformance 1.0.16.1 → 1.0.17.0
raw patch · 8 files changed
+208/−2 lines, 8 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +7/−0
- pqi-conformance.cabal +4/−1
- src/library/Pqi/Conformance/Operation/Exec.hs +2/−0
- src/library/Pqi/Conformance/Operation/Exec/Interrupted.hs +65/−0
- src/library/Pqi/Conformance/Operation/ExecParams.hs +2/−0
- src/library/Pqi/Conformance/Operation/ExecParams/Interrupted.hs +49/−0
- src/library/Pqi/Conformance/Operation/Getvalue.hs +4/−1
- src/library/Pqi/Conformance/Operation/Getvalue/ManyRows.hs +75/−0
CHANGELOG.md view
@@ -1,3 +1,10 @@+# v1.0.17.0++## Non-breaking++- Added a `getvalue` spec (`Pqi.Conformance.Operation.Getvalue.ManyRows`) that scans 200,000 single-column rows with `getisnull`, `getlength` and `getvalue` under a 5 s deadline, checking every cell. The divergence is in cost rather than in any returned value, so the observation is whether the scan finished. `libpq` finishes in about 0.1 s. Found `pqi-native` reaching each row by walking its row list from the head, which makes a scan quadratic in the number of rows, so it ran past the deadline. Found via `pqi-native` issue #15.+- Added `execParams` and `exec` specs (`Pqi.Conformance.Operation.ExecParams.Interrupted`, `Pqi.Conformance.Operation.Exec.Interrupted`) covering a connection that is reused while results of an earlier query are still unread: a slow query interrupted by `timeout`, and a `sendQuery` or `sendQueryParams` whose results are never fetched. They observe the results of the synchronous calls that follow, what `getResult` still returns, and the transaction status. Whether `timeout` returns early is not compared, since `libpq`'s blocking call cannot be interrupted. `libpq` discards leftover results at the start of each synchronous call (`PQexecStart`), so each call gets its own result. Found `pqi-native` returning the earlier query's result instead, so that each later call returned the result of the one before it. Found via `pqi-native` issue #16.+ # v1.0.16.1 Doc corrections: the README and package description describe adapters as `Pqi.Adapter` values instead of `Pqi.IsConnection` instances, and use the `notifyBePid` field name.
pqi-conformance.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: pqi-conformance-version: 1.0.16.1+version: 1.0.17.0 category: Database, PostgreSQL, Testing synopsis: Differential conformance tests for pqi adapters description:@@ -118,8 +118,10 @@ Pqi.Conformance.Operation.EscapeStringConn Pqi.Conformance.Operation.Exec Pqi.Conformance.Operation.Exec.ConnectionLostMidQuery+ Pqi.Conformance.Operation.Exec.Interrupted Pqi.Conformance.Operation.Exec.LostConnection Pqi.Conformance.Operation.ExecParams+ Pqi.Conformance.Operation.ExecParams.Interrupted Pqi.Conformance.Operation.ExecParams.LargeField Pqi.Conformance.Operation.ExecParams.LostConnection Pqi.Conformance.Operation.ExecPrepared@@ -143,6 +145,7 @@ Pqi.Conformance.Operation.GetResult Pqi.Conformance.Operation.GetResult.LostConnection Pqi.Conformance.Operation.Getvalue+ Pqi.Conformance.Operation.Getvalue.ManyRows Pqi.Conformance.Operation.GetvalueCopy Pqi.Conformance.Operation.Host Pqi.Conformance.Operation.IsBusy
src/library/Pqi/Conformance/Operation/Exec.hs view
@@ -8,6 +8,7 @@ import qualified Pqi import Pqi.Conformance.Harness import qualified Pqi.Conformance.Operation.Exec.ConnectionLostMidQuery as ConnectionLostMidQuery+import qualified Pqi.Conformance.Operation.Exec.Interrupted as Interrupted import qualified Pqi.Conformance.Operation.Exec.LostConnection as LostConnection import Pqi.Conformance.Prelude import Pqi.Conformance.Scenario@@ -16,6 +17,7 @@ spec :: Pqi.Adapter -> SpecWith ByteString spec adapter = do ConnectionLostMidQuery.spec adapter+ Interrupted.spec adapter LostConnection.spec adapter describe "exec" do
+ src/library/Pqi/Conformance/Operation/Exec/Interrupted.hs view
@@ -0,0 +1,65 @@+-- | Coverage for a connection reused after its results were left unread: a+-- slow 'Pqi.exec' cut short by 'timeout', and a 'Pqi.sendQuery' or+-- 'Pqi.sendQueryParams' whose results are never fetched. Each is followed by+-- synchronous calls on the same connection.+--+-- The divergence: @pqi-native@'s synchronous flows sent their query and read+-- up to the first @ReadyForQuery@, which belonged to the earlier query+-- whenever its messages were still unread. The call then returned the+-- earlier query's result, every call after it the result of the one before,+-- and 'Pqi.getResult' afterwards the last call's. @libpq@ discards any+-- results left over from a previous query at the start of each synchronous+-- call (@PQexecStart@ in @fe-exec.c@), and its @PQexec@ cannot be+-- interrupted at all (it is a blocking foreign call, so the exception is+-- delivered once it returns), so each call gets its own result.+--+-- Where it surfaced: @pqi-native@ issue #16, which reported the interrupted+-- case for 'Pqi.execParams' (covered by+-- 'Pqi.Conformance.Operation.ExecParams.Interrupted').+--+-- Why this observation: whether 'timeout' returns early differs by design+-- (the reference cannot be interrupted), so only what the connection does+-- afterwards is compared.+module Pqi.Conformance.Operation.Exec.Interrupted+ ( spec,+ )+where++import qualified Pqi+import Pqi.Conformance.Harness+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario+import System.Timeout (timeout)+import Test.Hspec++spec :: Pqi.Adapter -> SpecWith ByteString+spec adapter =+ describe "exec" do+ describe "after unread results" do+ it "of an interrupted exec, the next calls return their own results" \conninfo ->+ differential adapter conninfo \connection -> do+ _ <- timeout 200_000 (Pqi.exec connection "select 'slow' from pg_sleep (1)")+ afterwards connection+ it "of a sendQuery, the next calls return their own results" \conninfo ->+ differential adapter conninfo \connection -> do+ sent <- Pqi.sendQuery connection "select 'pending'"+ (sent,) <$> afterwards connection+ it "of a sendQueryParams, execParams returns its own result" \conninfo ->+ differential adapter conninfo \connection -> do+ sent <- Pqi.sendQueryParams connection "select 'pending'" [] Pqi.Text+ next <- observed "select 'next'" [] Pqi.Text connection+ remaining <- takeResult connection+ pure (sent, next, remaining)++-- | Two synchronous calls, then whatever 'Pqi.getResult' still has, and the+-- transaction status.+afterwards ::+ Pqi.Connection ->+ IO (Maybe ResultObservation, Maybe ResultObservation, Maybe ResultObservation, Pqi.TransactionStatus)+afterwards connection = do+ one <- execScenario "select 'one'" connection+ two <- execScenario "select 'two'" connection+ remaining <- takeResult connection+ transactionStatus <- Pqi.transactionStatus connection+ pure (one, two, remaining, transactionStatus)
src/library/Pqi/Conformance/Operation/ExecParams.hs view
@@ -9,6 +9,7 @@ import qualified Pqi as Lq import Pqi.Conformance.Harness import Pqi.Conformance.Observation+import qualified Pqi.Conformance.Operation.ExecParams.Interrupted as Interrupted import qualified Pqi.Conformance.Operation.ExecParams.LargeField as LargeField import qualified Pqi.Conformance.Operation.ExecParams.LostConnection as LostConnection import Pqi.Conformance.Prelude@@ -17,6 +18,7 @@ spec :: Lq.Adapter -> SpecWith ByteString spec adapter = do+ Interrupted.spec adapter LargeField.spec adapter LostConnection.spec adapter
+ src/library/Pqi/Conformance/Operation/ExecParams/Interrupted.hs view
@@ -0,0 +1,49 @@+-- | Coverage for a connection reused after an asynchronous exception+-- interrupted 'Pqi.execParams' while it waited for its results: a slow+-- query is cut short by 'timeout', then two quick queries run on the same+-- connection.+--+-- The divergence: @pqi-native@'s synchronous flows sent the query and read+-- up to the next @ReadyForQuery@ without recording that a query was in+-- flight. Once the read was interrupted, the slow query's messages stayed+-- unread, so the next call read them as its own and returned the slow+-- query's result, and every call after that returned the result of the one+-- before it, with no error. @libpq@ cannot be interrupted there at all+-- (@PQexecParams@ is a blocking foreign call, so the exception is delivered+-- once it returns), and the next call gets its own result. @libpq@ also+-- discards any results left over from a previous query at the start of each+-- synchronous call (@PQexecStart@ in @fe-exec.c@).+--+-- Where it surfaced: @pqi-native@ issue #16.+--+-- Why this observation: whether 'timeout' returns early differs by design+-- (the reference cannot be interrupted), so only what the connection does+-- afterwards is compared: the results of the two later queries and the+-- transaction status.+module Pqi.Conformance.Operation.ExecParams.Interrupted+ ( spec,+ )+where++import qualified Pqi+import Pqi.Conformance.Harness+import Pqi.Conformance.Observation+import Pqi.Conformance.Prelude+import Pqi.Conformance.Scenario+import System.Timeout (timeout)+import Test.Hspec++spec :: Pqi.Adapter -> SpecWith ByteString+spec adapter =+ describe "execParams" do+ describe "interrupted" do+ it "the next calls return their own results" \conninfo ->+ differential adapter conninfo scenario++scenario :: Pqi.Connection -> IO (Maybe ResultObservation, Maybe ResultObservation, Pqi.TransactionStatus)+scenario connection = do+ _ <- timeout 200_000 (Pqi.execParams connection "select 'slow' from pg_sleep (1)" [] Pqi.Text)+ one <- observed "select 'one'" [] Pqi.Text connection+ two <- observed "select 'two'" [] Pqi.Text connection+ transactionStatus <- Pqi.transactionStatus connection+ pure (one, two, transactionStatus)
src/library/Pqi/Conformance/Operation/Getvalue.hs view
@@ -7,11 +7,14 @@ import qualified Pqi import Pqi.Conformance.Harness+import qualified Pqi.Conformance.Operation.Getvalue.ManyRows as ManyRows import Pqi.Conformance.Prelude import Test.Hspec spec :: Pqi.Adapter -> SpecWith ByteString-spec adapter =+spec adapter = do+ ManyRows.spec adapter+ describe "getvalue" do it "reads cells, nulls, and degrades out of range" \conninfo -> differential adapter conninfo \connection -> do
+ src/library/Pqi/Conformance/Operation/Getvalue/ManyRows.hs view
@@ -0,0 +1,75 @@+-- | Coverage for reading every cell of a large result: 200,000 single-column+-- rows, fetched through 'Pqi.execParams' and scanned row by row with+-- 'Pqi.getisnull', 'Pqi.getlength' and 'Pqi.getvalue'.+--+-- The divergence: @pqi-native@'s 'Pqi.Native.Types.mkResult' kept the rows+-- as a list and reached the requested row with @drop@ from its head on+-- every lookup, so scanning a result through the public accessors was+-- quadratic in its rows. @libpq@ indexes an array, so each lookup is+-- constant time. The values themselves came back intact, only slowly: the+-- issue measured 3.9 s to scan 40,000 rows against 17 ms with @libpq@.+--+-- Where it surfaced: @pqi-native@ issue #15.+--+-- Why this observation: the divergence is in cost, not in any returned+-- value, so the scan runs under a 'deadline' and the observation records+-- whether it finished. The reference scans these rows in about 0.1 s, and+-- the quadratic candidate would need over a minute, so the deadline sits+-- far from both. Every cell is checked as well, so that a fix cannot pass+-- by returning the wrong one.+module Pqi.Conformance.Operation.Getvalue.ManyRows+ ( spec,+ )+where++import qualified Data.ByteString as ByteString+import qualified Data.ByteString.Char8 as ByteString.Char8+import qualified Pqi+import Pqi.Conformance.Harness+import Pqi.Conformance.Prelude+import System.Timeout (timeout)+import Test.Hspec++rowCount :: Int32+rowCount = 200_000++-- | How long the scan may take, in microseconds.+deadline :: Int+deadline = 5_000_000++spec :: Pqi.Adapter -> SpecWith ByteString+spec adapter =+ describe "getvalue" do+ describe "many rows" do+ it "scans 200,000 rows within the deadline" \conninfo ->+ differential adapter conninfo scenario++-- | Fetch the rows, then report the result status, the row count, and+-- whether every cell checked out within the deadline (@Nothing@ when the+-- deadline passed).+scenario :: Pqi.Connection -> IO (Maybe (Pqi.ExecStatus, Int32, Maybe Bool))+scenario connection = do+ result <-+ Pqi.execParams+ connection+ "select i :: int4 from generate_series (1, $1 :: int4) as i"+ [Just (23, ByteString.Char8.pack (show rowCount), Pqi.Text)]+ Pqi.Binary+ for result \r -> do+ status <- Pqi.resultStatus r+ count <- Pqi.ntuples r+ scanned <- timeout deadline (scan r count)+ pure (status, count, scanned)+ where+ scan r count = go 0+ where+ go row+ | row >= count = pure True+ | otherwise = do+ isNull <- Pqi.getisnull r row 0+ size <- Pqi.getlength r row 0+ value <- Pqi.getvalue r row 0+ let decoded = fmap (ByteString.foldl' (\n byte -> n * 256 + fromIntegral byte) 0) value+ if not isNull && size == 4 && decoded == Just (row + 1)+ then go (row + 1)+ else pure False