packages feed

hasql 1.10.3 → 1.10.3.1

raw patch · 4 files changed

+142/−3 lines, 4 files

Files

hasql.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: hasql-version: 1.10.3+version: 1.10.3.1 category: Hasql, Database, PostgreSQL synopsis: Fast PostgreSQL driver with a flexible mapping API description:@@ -358,11 +358,11 @@     hspec-discover:hspec-discover ^>=2.11.12    build-depends:-    QuickCheck,     aeson,     hasql,     hspec ^>=2.11.12,     iproute,+    QuickCheck,     quickcheck-instances >=0.3.11 && <0.4,     random >=1.3 && <1.4,     random ^>=1.3.1,
src/library-tests/Sharing/ByFeature/PreparedStatementCacheSpec.hs view
@@ -3,6 +3,7 @@ import Data.Either import Hasql.Connection qualified as Connection import Hasql.Decoders qualified as Decoders+import Hasql.Errors qualified as Errors import Hasql.Pipeline qualified as Pipeline import Hasql.Session qualified as Session import Hasql.Statement qualified as Statement@@ -41,6 +42,29 @@           Left result ->             expectationFailure ("Unexpected error: " <> show result) +    it "Syntax errors in prepared statements don't corrupt the cache for subsequent uses of the same statement" \config -> do+      Scripts.onPreparableConnection config \connection -> do+        let brokenStatement =+              Statement.preparable+                "S"+                mempty+                Decoders.noResult+        -- First run: syntax error.+        result1 <- Connection.use connection do+          Session.statement () brokenStatement+        error1 <- case result1 of+          Left error1 -> pure error1+          Right _ -> fail "First run unexpectedly succeeded"++        -- Second run of the same statement: should also produce a syntax error,+        -- not "prepared statement does not exist".+        result2 <- Connection.use connection do+          Session.statement () brokenStatement+        error2 <- case result2 of+          Left error2 -> pure error2+          Right _ -> fail "Second run unexpectedly succeeded"+        shouldBe error2 error1+   describe "Pipeline" do     it "Failing pipeline statements don't cause misses in updates of the prepared statement cache" \config -> do       Scripts.onPreparableConnection config \connection -> do@@ -76,3 +100,103 @@             pure ()           Left result ->             expectationFailure ("Unexpected error: " <> show result)++    it "Syntax errors in pipeline prepared statements don't corrupt the cache for subsequent uses of the same statement" \config -> do+      Scripts.onPreparableConnection config \connection -> do+        let brokenStatement =+              Statement.preparable+                "S"+                mempty+                Decoders.noResult+        -- First run: syntax error.+        result1 <- Connection.use connection do+          Session.pipeline (Pipeline.statement () brokenStatement)+        shouldBe (isLeft result1) True+        -- Second run of the same statement: should also produce a syntax error,+        -- not "prepared statement does not exist".+        result2 <- Connection.use connection do+          Session.pipeline (Pipeline.statement () brokenStatement)+        case result2 of+          Left (Errors.StatementSessionError _ _ _ _ _ (Errors.ServerStatementError (Errors.ServerError "42601" _ _ _ _))) ->+            pure ()+          Left other ->+            expectationFailure ("Unexpected error on second run: " <> show other)+          Right _ ->+            expectationFailure "Second run unexpectedly succeeded"++    it "A pipeline with successful statements followed by a broken one can be retried without 'already exists' errors" \config -> do+      Scripts.onPreparableConnection config \connection -> do+        let ok1 = Statement.preparable "select 1" mempty (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))+            ok2 = Statement.preparable "select 2" mempty (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))+            broken = Statement.preparable "S" mempty Decoders.noResult+        -- First run: pipeline with two OK statements and a broken one at the end.+        result1 <- Connection.use connection do+          Session.pipeline do+            (,,)+              <$> Pipeline.statement () ok1+              <*> Pipeline.statement () ok2+              <*> Pipeline.statement () broken+        error1 <- case result1 of+          Left error1 -> pure error1+          Right _ -> fail "First run unexpectedly succeeded"++        -- Second run of the same pipeline: must fail with the SAME syntax error,+        -- not "prepared statement already exists".+        result2 <- Connection.use connection do+          Session.pipeline do+            (,,)+              <$> Pipeline.statement () ok1+              <*> Pipeline.statement () ok2+              <*> Pipeline.statement () broken+        error2 <- case result2 of+          Left error2 -> pure error2+          Right _ -> fail "Second run unexpectedly succeeded"+        shouldBe error2 error1++        -- Also, a standalone valid statement should still work afterwards.+        result3 <- Connection.use connection do+          Session.statement () ok1+        case result3 of+          Right val -> val `shouldBe` 1+          Left err -> expectationFailure ("Unexpected error on standalone statement: " <> show err)++    it "A pipeline with a broken statement in the middle can be retried without 'already exists' errors" \config -> do+      Scripts.onPreparableConnection config \connection -> do+        let ok1 = Statement.preparable "select 1" mempty (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))+            broken = Statement.preparable "S" mempty Decoders.noResult+            ok2 = Statement.preparable "select 2" mempty (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))+        -- First run: pipeline with broken statement in the middle.+        result1 <- Connection.use connection do+          Session.pipeline do+            (,,)+              <$> Pipeline.statement () ok1+              <*> Pipeline.statement () broken+              <*> Pipeline.statement () ok2+        shouldBe (isLeft result1) True++        -- Second run of the same pipeline: must fail with the same syntax error.+        result2 <- Connection.use connection do+          Session.pipeline do+            (,,)+              <$> Pipeline.statement () ok1+              <*> Pipeline.statement () broken+              <*> Pipeline.statement () ok2+        case result2 of+          Left (Errors.StatementSessionError _ _ _ _ _ (Errors.ServerStatementError (Errors.ServerError "42601" _ _ _ _))) ->+            pure ()+          Left other ->+            expectationFailure ("Unexpected error on second run: " <> show other)+          Right _ ->+            expectationFailure "Second run unexpectedly succeeded"++        -- Standalone valid statements should still work afterwards.+        result3 <- Connection.use connection do+          Session.statement () ok1+        case result3 of+          Right val -> val `shouldBe` 1+          Left err -> expectationFailure ("Unexpected error on standalone ok1: " <> show err)+        result4 <- Connection.use connection do+          Session.statement () ok2+        case result4 of+          Right val -> val `shouldBe` 2+          Left err -> expectationFailure ("Unexpected error on standalone ok2: " <> show err)
src/library/Hasql/Engine/Contexts/Pipeline.hs view
@@ -62,7 +62,11 @@                         )                     ) -          pure (result, newOidCache, newStatementCache)+          let finalStatementCache = case result of+                Left _ -> StatementCache.revertTo statementCache newStatementCache+                Right _ -> newStatementCache++          pure (result, newOidCache, finalStatementCache)  -- | -- Composable abstraction over the execution of queries in [the pipeline mode](https://www.postgresql.org/docs/current/libpq-pipeline-mode.html).
src/library/Hasql/Engine/Structures/StatementCache.hs view
@@ -5,6 +5,7 @@     lookup,     insert,     reset,+    revertTo,   ) where @@ -43,6 +44,16 @@ {-# INLINEABLE reset #-} reset :: StatementCache -> StatementCache reset _ = StatementCache HashMap.empty 0++-- | Revert the entries to those of the provided cache, preserving the counter of the receiver.+--+-- This is useful when a pipeline fails and we want to discard tentative entries+-- (which might not have been successfully prepared on the server)+-- while keeping the counter advanced to avoid name collisions with orphaned server-side statements.+{-# INLINEABLE revertTo #-}+revertTo :: StatementCache -> StatementCache -> StatementCache+revertTo (StatementCache oldHashMap _) (StatementCache _ newCounter) =+  StatementCache oldHashMap newCounter  -- | -- Local statement key.