kiroku-store 0.3.0.0 → 0.3.0.1
raw patch · 4 files changed
+61/−4 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +13/−0
- kiroku-store.cabal +1/−1
- src/Kiroku/Store/Error.hs +3/−3
- test/Test/Transaction.hs +44/−0
CHANGELOG.md view
@@ -2,6 +2,19 @@ ## Unreleased +## 0.3.0.1 — 2026-07-14++### Bug Fixes++* A failure raised inside an opaque `runTransaction` body now preserves its+ SQLSTATE and server message. Anything that was not a `stream_events_pkey`+ duplicate previously went through the append-shaped mapper with a+ `<transaction>` sentinel stream, so e.g. a foreign-key violation surfaced as+ `StreamNotFound (StreamName "<transaction>")` and lost the real diagnostics.+ Such failures now map through the generic mapper — the foreign-key case above+ becomes `UnexpectedServerError "23503"` with the server's message intact.+ Duplicate-event mapping is unchanged.+ ## 0.3.0.0 — 2026-07-11 This release requires migrations `0003`–`0007` from `kiroku-store-migrations`.
kiroku-store.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: kiroku-store-version: 0.3.0.0+version: 0.3.0.1 synopsis: High-performance PostgreSQL event store description: Kiroku is a PostgreSQL-backed event store for Haskell applications. It
src/Kiroku/Store/Error.hs view
@@ -183,8 +183,8 @@ {- | Map a hasql 'UsageError' raised inside an opaque transaction body. The body has no stream context, so append duplicate violations are mapped-directly to 'DuplicateEvent' and all other failures fall back to the standard-append-shaped mapper with sentinel stream context.+directly to 'DuplicateEvent'. Every other failure uses the generic mapper so+its SQLSTATE and server message survive without inventing stream context. -} mapTransactionUsageError :: UsageError -> StoreError mapTransactionUsageError usageErr =@@ -195,7 +195,7 @@ | containsConstraint "stream_events_pkey" message detail -> DuplicateEvent (EventId <$> (detail >>= extractFirstUuidFromCompositeDetail)) _ ->- mapUsageError "<transaction>" AnyVersion usageErr+ mapGenericUsageError usageErr where containsConstraint name message detail = name `T.isInfixOf` message || maybe False (T.isInfixOf name) detail
test/Test/Transaction.hs view
@@ -38,6 +38,21 @@ Right n -> n `shouldBe` (1 :: Int) Left err -> expectationFailure ("Expected Right 1, got: " <> show err) + it "preserves SQLSTATE and message for an opaque foreign-key failure" $ \store -> do+ createForeignKeyTables (store ^. #pool)+ result <-+ runStoreIO store $+ runTransaction $+ Tx.statement () insertMissingForeignKeyStmt+ case result of+ Left (UnexpectedServerError code message) -> do+ code `shouldBe` "23503"+ T.toLower message `shouldSatisfy` T.isInfixOf "foreign key"+ Left (StreamNotFound (StreamName "<transaction>")) ->+ expectationFailure "foreign-key failure was misclassified as a missing <transaction> stream"+ other ->+ expectationFailure ("Expected UnexpectedServerError 23503, got: " <> show other)+ describe "appendToStreamTx (driven inside runTransaction)" $ do it "appends events and a side-table row in one atomic transaction" $ \store -> do createSideTable (store ^. #pool)@@ -326,6 +341,27 @@ E.noParams D.noResult +createForeignKeyParentStmt :: Statement () ()+createForeignKeyParentStmt =+ unpreparable+ "CREATE TABLE IF NOT EXISTS test_tx_parent (id BIGINT PRIMARY KEY)"+ E.noParams+ D.noResult++createForeignKeyChildStmt :: Statement () ()+createForeignKeyChildStmt =+ unpreparable+ "CREATE TABLE IF NOT EXISTS test_tx_child (parent_id BIGINT NOT NULL REFERENCES test_tx_parent(id))"+ E.noParams+ D.noResult++insertMissingForeignKeyStmt :: Statement () ()+insertMissingForeignKeyStmt =+ preparable+ "INSERT INTO test_tx_child (parent_id) VALUES (999999)"+ E.noParams+ D.noResult+ {- | Insert a row into the side table. The @id@ doubles as the foreign key onto the stream the transaction appended to. -}@@ -353,6 +389,14 @@ case r of Left err -> error ("createSideTable failed: " <> show err) Right () -> pure ()++createForeignKeyTables :: Pool -> IO ()+createForeignKeyTables pool = do+ parent <- Pool.use pool (Session.statement () createForeignKeyParentStmt)+ child <- Pool.use pool (Session.statement () createForeignKeyChildStmt)+ case (parent, child) of+ (Right (), Right ()) -> pure ()+ other -> error ("createForeignKeyTables failed: " <> show other) countSideTable :: Pool -> IO Int64 countSideTable pool = do