hasql-1.10.3.1: src/library-tests/Sharing/ByFeature/PreparedStatementCacheSpec.hs
module Sharing.ByFeature.PreparedStatementCacheSpec (spec) where
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
import Helpers.Scripts qualified as Scripts
import Test.Hspec
import Prelude
spec :: SpecWith (Text, Word16)
spec = do
describe "Session" do
it "Failing statements don't cause misses in updates of the prepared statement cache" \config -> do
Scripts.onPreparableConnection config \connection -> do
-- Run an intentionally failing prepared statement to set the condition of the bug.
result <- Connection.use connection do
Session.statement
()
( Statement.preparable
"select null"
mempty
(Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))
)
shouldBe (isLeft result) True
-- Run a succeeding prepared statement to see if the cache is still in a good state.
result <- Connection.use connection do
Session.statement
()
( Statement.preparable
"select 1"
mempty
(Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))
)
-- If there is an error the cache got corrupted.
case result of
Right _ ->
pure ()
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
-- Run an intentionally failing prepared statement in a pipeline to set the condition of the bug.
result <- Connection.use connection do
Session.pipeline do
Pipeline.statement
()
( Statement.preparable
"select null :: int4"
mempty
(Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))
)
case result of
Right val ->
expectationFailure ("First statement succeeded unexpectedly: " <> show val)
Left _ ->
pure ()
-- Run a succeeding prepared statement in a pipeline to see if the cache is still in a good state.
result <- Connection.use connection do
Session.pipeline do
Pipeline.statement
()
( Statement.preparable
"select 1"
mempty
(Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))
)
-- If there is an error the cache got corrupted.
case result of
Right _ ->
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)