hasql 1.10.3 → 1.10.3.1
raw patch · 4 files changed
+142/−3 lines, 4 files
Files
- hasql.cabal +2/−2
- src/library-tests/Sharing/ByFeature/PreparedStatementCacheSpec.hs +124/−0
- src/library/Hasql/Engine/Contexts/Pipeline.hs +5/−1
- src/library/Hasql/Engine/Structures/StatementCache.hs +11/−0
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.