pg-migrate-test-support 1.0.0.0 → 1.1.0.0
raw patch · 4 files changed
+57/−20 lines, 4 filesdep ~pg-migratePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: pg-migrate
API changes (from Hackage documentation)
- Database.PostgreSQL.Migrate.Test: MigratedDatabaseCallbackCleanupFailed :: !SomeException -> MigratedDatabaseError
Files
- CHANGELOG.md +19/−0
- pg-migrate-test-support.cabal +2/−2
- src/Database/PostgreSQL/Migrate/Test.hs +25/−18
- test/Main.hs +11/−0
CHANGELOG.md view
@@ -1,5 +1,24 @@ # Changelog +## 1.1.0.0 — 2026-07-13++Major release: this set removes a public error constructor and changes callback exception+behavior.++### Breaking changes++- Removed `MigratedDatabaseCallbackCleanupFailed`; a successful callback value now wins+ when only its connection release fails.+- Asynchronous callback exceptions now propagate after cleanup instead of being returned as+ `MigratedDatabaseCallbackFailed`.++### Fixes and behavior changes++- Acquire the callback connection inside the same masked bracket that runs the callback and+ releases the connection, closing the interruption leak window.+- Preserve both exceptions in `MigratedDatabaseCallbackAndCleanupFailed` when a synchronous+ callback failure and connection-release failure occur together.+ ## 1.0.0.0 — 2026-07-10 - Initial stable release of the `ephemeral-pg` helper with fresh Hasql callback connections
pg-migrate-test-support.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.8 name: pg-migrate-test-support-version: 1.0.0.0+version: 1.1.0.0 synopsis: Ephemeral PostgreSQL helpers for pg-migrate tests category: Database maintainer: nadeem@gmail.com@@ -31,7 +31,7 @@ , base >=4.20 && <4.22 , ephemeral-pg >=0.2 && <0.3 , hasql >=1.10 && <1.11- , pg-migrate >=1.0 && <1.1+ , pg-migrate >=1.1 && <1.2 test-suite pg-migrate-test-support-test import: common
src/Database/PostgreSQL/Migrate/Test.hs view
@@ -1,7 +1,7 @@ -- | Test-only ephemeral PostgreSQL lifecycle. A validated plan is applied before a fresh -- Hasql connection is bracketed around the caller's assertion callback. module Database.PostgreSQL.Migrate.Test- ( -- | Structured startup, migration, callback, and cleanup failures.+ ( -- | Structured startup, migration, callback, and callback-plus-cleanup failures. MigratedDatabaseError (..), -- | Start a database, apply a plan with default runner options, and run a callback. withMigratedDatabase,@@ -17,15 +17,17 @@ import Database.PostgreSQL.Migrate import EphemeralPg qualified import Hasql.Connection qualified as Connection+import Hasql.Connection.Settings qualified as Settings import Hasql.Errors qualified as Errors --- | Structured failures from the ephemeral database, migration, callback, or cleanup stages.+-- | Structured failures from the ephemeral database, migration, callback, or a callback+-- failure accompanied by a connection-release failure. A release failure after a+-- successful callback does not replace its value. data MigratedDatabaseError = MigratedDatabaseStartupFailed !EphemeralPg.StartError | MigratedDatabaseMigrationFailed !MigrationError | MigratedDatabaseCallbackAcquisitionFailed !Errors.ConnectionError | MigratedDatabaseCallbackFailed !SomeException- | MigratedDatabaseCallbackCleanupFailed !SomeException | MigratedDatabaseCallbackAndCleanupFailed !SomeException !SomeException deriving stock (Show) @@ -45,6 +47,7 @@ withMigratedDatabaseOptions = withMigratedDatabaseConfig EphemeralPg.defaultConfig -- | Fully configurable variant accepting both ephemeral database and migration options.+-- Asynchronous callback exceptions are rethrown after releasing the callback connection. withMigratedDatabaseConfig :: EphemeralPg.Config -> RunOptions ->@@ -58,26 +61,30 @@ migrated <- runMigrationPlan options settings plan case migrated of Left migrationError -> pure (Left (MigratedDatabaseMigrationFailed migrationError))- Right _ -> do- acquired <- Connection.acquire settings- case acquired of- Left connectionError -> pure (Left (MigratedDatabaseCallbackAcquisitionFailed connectionError))- Right connection -> runCallback connection callback+ Right _ -> runCallback settings callback pure $ case started of Left startError -> Left (MigratedDatabaseStartupFailed startError) Right result -> result runCallback ::- Connection.Connection ->+ Settings.Settings -> (Connection.Connection -> IO value) -> IO (Either MigratedDatabaseError value)-runCallback connection callback =+runCallback settings callback = Exception.mask $ \restore -> do- callbackResult <- Exception.try (restore (callback connection))- cleanupResult <- Exception.try (Connection.release connection)- pure $ case (callbackResult, cleanupResult) of- (Right value, Right ()) -> Right value- (Left callbackError, Right ()) -> Left (MigratedDatabaseCallbackFailed callbackError)- (Right _, Left cleanupError) -> Left (MigratedDatabaseCallbackCleanupFailed cleanupError)- (Left callbackError, Left cleanupError) ->- Left (MigratedDatabaseCallbackAndCleanupFailed callbackError cleanupError)+ acquired <- Connection.acquire settings+ case acquired of+ Left connectionError -> pure (Left (MigratedDatabaseCallbackAcquisitionFailed connectionError))+ Right connection -> do+ callbackResult <- Exception.try @SomeException (restore (callback connection))+ cleanupResult <- Exception.try @SomeException (Connection.release connection)+ case callbackResult of+ Left callbackError+ | Just _ <- Exception.fromException @Exception.SomeAsyncException callbackError ->+ Exception.throwIO callbackError+ _ ->+ pure $ case (callbackResult, cleanupResult) of+ (Right value, _) -> Right value+ (Left callbackError, Right ()) -> Left (MigratedDatabaseCallbackFailed callbackError)+ (Left callbackError, Left cleanupError) ->+ Left (MigratedDatabaseCallbackAndCleanupFailed callbackError cleanupError)
test/Main.hs view
@@ -24,6 +24,7 @@ [ testCase "plan is migrated before a fresh callback connection" testMigratedDatabase, testCase "migration failures are structurally distinct" testMigrationFailure, testCase "callback failures are structurally distinct" testCallbackFailure,+ testCase "asynchronous callback exceptions propagate" testAsyncCallbackException, testCase "startup failures are structurally distinct" testStartupFailure ] )@@ -51,6 +52,16 @@ case result of Left MigratedDatabaseCallbackFailed {} -> pure () other -> assertFailure ("expected callback failure, received: " <> show other)++testAsyncCallbackException :: Assertion+testAsyncCallbackException = do+ result <-+ Exception.try @Exception.AsyncException+ (withMigratedDatabase fixturePlan (const (Exception.throwIO Exception.UserInterrupt) :: Connection.Connection -> IO ()))+ case result of+ Left Exception.UserInterrupt -> pure ()+ Left exception -> assertFailure ("expected UserInterrupt, received: " <> show exception)+ Right callbackResult -> assertFailure ("expected propagated async exception, received: " <> show callbackResult) testStartupFailure :: Assertion testStartupFailure = do