packages feed

valiant-effectful (empty) → 0.1.0.0

raw patch · 7 files changed

+534/−0 lines, 7 filesdep +basedep +bytestringdep +effectful-core

Dependencies added: base, bytestring, effectful-core, hspec, pg-wire, text, valiant, valiant-effectful, vector

Files

+ CHANGELOG.md view
@@ -0,0 +1,15 @@+# Changelog++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).++## [0.1.0.0] - 2026-04-25++Initial release.+[Effectful](https://hackage.haskell.org/package/effectful) adapter for+[`valiant`](https://hackage.haskell.org/package/valiant). Provides a+dynamic `Valiant` effect with pool-based (`runValiant`) and custom+(`runValiantWith`) handlers, plus smart constructors covering query,+command, and transaction operations.++[0.1.0.0]: https://github.com/joshburgess/valiant/releases/tag/v0.1.0.0
+ LICENSE view
@@ -0,0 +1,27 @@+Copyright (c) 2026 Josh Burgess++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice,+   this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived from this+   software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,43 @@+# valiant-effectful++[Effectful](https://hackage.haskell.org/package/effectful) adapter for+[`valiant`](https://hackage.haskell.org/package/valiant).++Provides a dynamic `Valiant` effect that any `Eff` action can request+via `Valiant :> es`, plus a pool-based handler.++## Quick start++```haskell+import Effectful+import Valiant (newPool, defaultPoolConfig, poolConnString)+import Valiant.Effectful++myApp :: (Valiant :> es, IOE :> es) => Eff es [User]+myApp = do+  users <- fetchAllEff listUsers ()+  count <- fetchScalarEff countUsers ()+  liftIO $ putStrLn $ "got " <> show count <> " users"+  pure users++main :: IO ()+main = do+  pool <- newPool defaultPoolConfig { poolConnString = "postgres://..." }+  users <- runEff . runValiant pool $ myApp+  print users+```++## What you get++- Effect: `data Valiant :: Effect`+- Handlers: `runValiant pool` and `runValiantWith` (custom handler,+  useful for tests)+- Query operations: `fetchOneEff`, `fetchAllEff`, `fetchScalarEff`,+  `fetchOneOrThrowEff`, `fetchExistsEff`+- Command operations: `executeEff`, `executeReturningEff`,+  `executeBatchEff`+- Transactions: `withTransactionEff`, `withTransactionLevelEff`+- Raw access: `withConnectionEff`++See the [valiant tutorial](https://github.com/joshburgess/valiant/blob/main/docs/TUTORIAL.md)+for the underlying `Statement` and `valiant prepare` workflow.
+ src/Valiant/Effectful.hs view
@@ -0,0 +1,198 @@+-- | Effectful adapter for valiant.+--+-- Provides an @Valiant@ effect for database operations, with a pool-based+-- handler that manages connection acquisition and release automatically.+--+-- @+-- import Effectful+-- import Valiant.Effectful+--+-- myApp :: (Valiant :> es, IOE :> es) => Eff es [User]+-- myApp = do+--   users <- fetchAllEff listUsers ()+--   count <- fetchScalarEff countUsers ()+--   pure users+--+-- main :: IO ()+-- main = do+--   pool <- newPool defaultPoolConfig { poolConnString = "..." }+--   result <- runEff . runValiant pool $ myApp+--   print result+-- @+module Valiant.Effectful+  ( -- * Effect+    Valiant (..)++    -- * Handler+  , runValiant+  , runValiantWith++    -- * Query operations+  , fetchOneEff+  , fetchAllEff+  , fetchScalarEff+  , fetchOneOrThrowEff+  , fetchExistsEff++    -- * Command operations+  , executeEff+  , executeReturningEff+  , executeBatchEff++    -- * Transaction operations+  , withTransactionEff+  , withTransactionLevelEff++    -- * Raw access+  , withConnectionEff+  ) where++import Data.Int (Int64)+import Effectful+import Effectful.Dispatch.Dynamic+import Valiant (Connection, IsolationLevel, Pool, Statement, Transaction (..))+import Valiant qualified+import PgWire.Pool (withResource)++------------------------------------------------------------------------+-- Effect definition+------------------------------------------------------------------------++-- | The Valiant database effect.+--+-- All operations acquire a connection from the pool, execute, and release.+-- Transactions hold a connection for the duration of the callback.+data Valiant :: Effect where+  FetchOneEff :: Statement p r -> p -> Valiant m (Maybe r)+  FetchAllEff :: Statement p r -> p -> Valiant m [r]+  FetchScalarEff :: Statement p r -> p -> Valiant m r+  FetchOneOrThrowEff :: Statement p r -> p -> Valiant m r+  FetchExistsEff :: Statement p r -> p -> Valiant m Bool+  ExecuteEff :: Statement p () -> p -> Valiant m Int64+  ExecuteReturningEff :: Statement p r -> p -> Valiant m (Int64, [r])+  ExecuteBatchEff :: Statement p () -> [p] -> Valiant m Int64+  WithTransactionEff :: (Transaction -> IO a) -> Valiant m a+  WithTransactionLevelEff :: IsolationLevel -> (Transaction -> IO a) -> Valiant m a+  WithConnectionEff :: (Connection -> IO a) -> Valiant m a++type instance DispatchOf Valiant = 'Dynamic++------------------------------------------------------------------------+-- Handler+------------------------------------------------------------------------++-- | Run the Valiant effect with a connection pool.+--+-- @+-- main = do+--   pool <- newPool defaultPoolConfig { poolConnString = "..." }+--   runEff . runValiant pool $ do+--     users <- fetchAllEff listUsers ()+--     liftIO $ print users+-- @+runValiant :: (IOE :> es) => Pool -> Eff (Valiant : es) a -> Eff es a+runValiant pool = interpret $ \_ -> \case+  FetchOneEff stmt params ->+    liftIO $ withResource pool $ \conn -> Valiant.fetchOne conn stmt params+  FetchAllEff stmt params ->+    liftIO $ withResource pool $ \conn -> Valiant.fetchAll conn stmt params+  FetchScalarEff stmt params ->+    liftIO $ withResource pool $ \conn -> Valiant.fetchScalar conn stmt params+  FetchOneOrThrowEff stmt params ->+    liftIO $ withResource pool $ \conn -> Valiant.fetchOneOrThrow conn stmt params+  FetchExistsEff stmt params ->+    liftIO $ withResource pool $ \conn -> Valiant.fetchExists conn stmt params+  ExecuteEff stmt params ->+    liftIO $ withResource pool $ \conn -> Valiant.execute conn stmt params+  ExecuteReturningEff stmt params ->+    liftIO $ withResource pool $ \conn -> Valiant.executeReturning conn stmt params+  ExecuteBatchEff stmt paramsList ->+    liftIO $ withResource pool $ \conn -> Valiant.executeBatch conn stmt paramsList+  WithTransactionEff action ->+    liftIO $ Valiant.withTransaction pool action+  WithTransactionLevelEff level action ->+    liftIO $ Valiant.withTransactionLevel level pool action+  WithConnectionEff action ->+    liftIO $ withResource pool action++-- | Run the Valiant effect with a custom handler function.+-- Useful for testing with mock connections.+runValiantWith+  :: (IOE :> es)+  => (forall x. (Connection -> IO x) -> IO x)+  -- ^ Connection provider (e.g., 'withResource pool' or a mock)+  -> (forall x. (Transaction -> IO x) -> IO x)+  -- ^ Transaction provider+  -> Eff (Valiant : es) a+  -> Eff es a+runValiantWith withConn withTxn = interpret $ \_ -> \case+  FetchOneEff stmt params ->+    liftIO $ withConn $ \conn -> Valiant.fetchOne conn stmt params+  FetchAllEff stmt params ->+    liftIO $ withConn $ \conn -> Valiant.fetchAll conn stmt params+  FetchScalarEff stmt params ->+    liftIO $ withConn $ \conn -> Valiant.fetchScalar conn stmt params+  FetchOneOrThrowEff stmt params ->+    liftIO $ withConn $ \conn -> Valiant.fetchOneOrThrow conn stmt params+  FetchExistsEff stmt params ->+    liftIO $ withConn $ \conn -> Valiant.fetchExists conn stmt params+  ExecuteEff stmt params ->+    liftIO $ withConn $ \conn -> Valiant.execute conn stmt params+  ExecuteReturningEff stmt params ->+    liftIO $ withConn $ \conn -> Valiant.executeReturning conn stmt params+  ExecuteBatchEff stmt paramsList ->+    liftIO $ withConn $ \conn -> Valiant.executeBatch conn stmt paramsList+  WithTransactionEff action ->+    liftIO $ withTxn action+  WithTransactionLevelEff _level action ->+    liftIO $ withTxn action  -- level ignored in custom handler+  WithConnectionEff action ->+    liftIO $ withConn action++------------------------------------------------------------------------+-- Smart constructors+------------------------------------------------------------------------++-- | Fetch zero or one row.+fetchOneEff :: (Valiant :> es) => Statement p r -> p -> Eff es (Maybe r)+fetchOneEff stmt params = send (FetchOneEff stmt params)++-- | Fetch all rows.+fetchAllEff :: (Valiant :> es) => Statement p r -> p -> Eff es [r]+fetchAllEff stmt params = send (FetchAllEff stmt params)++-- | Fetch exactly one scalar value. Throws on zero or multiple rows.+fetchScalarEff :: (Valiant :> es) => Statement p r -> p -> Eff es r+fetchScalarEff stmt params = send (FetchScalarEff stmt params)++-- | Fetch one row, throwing if none returned.+fetchOneOrThrowEff :: (Valiant :> es) => Statement p r -> p -> Eff es r+fetchOneOrThrowEff stmt params = send (FetchOneOrThrowEff stmt params)++-- | Check if a query returns any rows.+fetchExistsEff :: (Valiant :> es) => Statement p r -> p -> Eff es Bool+fetchExistsEff stmt params = send (FetchExistsEff stmt params)++-- | Execute a command. Returns rows affected.+executeEff :: (Valiant :> es) => Statement p () -> p -> Eff es Int64+executeEff stmt params = send (ExecuteEff stmt params)++-- | Execute with RETURNING. Returns rows affected and decoded rows.+executeReturningEff :: (Valiant :> es) => Statement p r -> p -> Eff es (Int64, [r])+executeReturningEff stmt params = send (ExecuteReturningEff stmt params)++-- | Execute a batch of commands (pipelined). Returns total rows affected.+executeBatchEff :: (Valiant :> es) => Statement p () -> [p] -> Eff es Int64+executeBatchEff stmt paramsList = send (ExecuteBatchEff stmt paramsList)++-- | Run an action in a transaction.+withTransactionEff :: (Valiant :> es) => (Transaction -> IO a) -> Eff es a+withTransactionEff action = send (WithTransactionEff action)++-- | Run an action in a transaction with a specific isolation level.+withTransactionLevelEff :: (Valiant :> es) => IsolationLevel -> (Transaction -> IO a) -> Eff es a+withTransactionLevelEff level action = send (WithTransactionLevelEff level action)++-- | Run an action with a raw connection from the pool.+withConnectionEff :: (Valiant :> es) => (Connection -> IO a) -> Eff es a+withConnectionEff action = send (WithConnectionEff action)
+ test/Main.hs view
@@ -0,0 +1,88 @@+module Main where++import Data.ByteString (ByteString)+import Data.ByteString.Char8 qualified as BS8+import Data.Int (Int32)+import Data.Text (Text)+import Effectful+import Valiant (defaultPoolConfig, newPool, closePool, txConn)+import Valiant qualified+import Valiant.Effectful+import PgWire.Pool.Config (PoolConfig (..))+import System.Environment (lookupEnv)+import Test.Hspec+import TestSupport++main :: IO ()+main = hspec $ do+  describe "Valiant.Effectful" $ do+    describe "runValiant" $ do+      it "fetchAllEff returns all rows" $ do+        withTestConnection $ \conn -> withSchema conn $ do+          insertTestUsers conn+          pool <- mkPool+          result <- runEff . runValiant pool $ fetchAllEff stmtListAll ()+          closePool pool+          length result `shouldBe` 5++      it "fetchOneEff returns Just for existing row" $ do+        withTestConnection $ \conn -> withSchema conn $ do+          insertTestUsers conn+          pool <- mkPool+          result <- runEff . runValiant pool $ fetchOneEff stmtSelectOne 1+          closePool pool+          case result of+            Just (_, name, _) -> name `shouldBe` "Alice"+            Nothing -> expectationFailure "Expected a row"++      it "fetchOneEff returns Nothing for missing row" $ do+        withTestConnection $ \conn -> withSchema conn $ do+          pool <- mkPool+          result <- runEff . runValiant pool $ fetchOneEff stmtSelectOne (-1)+          closePool pool+          result `shouldBe` (Nothing :: Maybe (Int32, Text, Maybe Text))++      it "executeEff returns rows affected" $ do+        withTestConnection $ \conn -> withSchema conn $ do+          insertTestUsers conn+          pool <- mkPool+          let stmtDelete = Valiant.mkStatement "DELETE FROM users_effectful WHERE id = $1" [23] [] "<test>" :: Valiant.Statement Int32 ()+          n <- runEff . runValiant pool $ executeEff stmtDelete (1 :: Int32)+          closePool pool+          n `shouldBe` 1++      it "withTransactionEff commits" $ do+        withTestConnection $ \conn -> withSchema conn $ do+          pool <- mkPool+          let stmtInsert = Valiant.mkStatement "INSERT INTO users_effectful (name, email) VALUES ($1, $2)" [25, 25] [] "<test>"+              stmtCount = Valiant.mkStatement "SELECT count(*)::int4 FROM users_effectful" [] ["count"] "<test>" :: Valiant.Statement () Int32+          _ <- runEff . runValiant pool $+            withTransactionEff $ \tx ->+              Valiant.execute (txConn tx) stmtInsert ("EffUser" :: Text, Just ("eff@test.com" :: Text))+          count <- runEff . runValiant pool $ fetchScalarEff stmtCount ()+          closePool pool+          count `shouldBe` 1++      it "composes multiple operations" $ do+        withTestConnection $ \conn -> withSchema conn $ do+          insertTestUsers conn+          pool <- mkPool+          (users, exists) <- runEff . runValiant pool $ do+            us <- fetchAllEff stmtListAll ()+            ex <- fetchExistsEff stmtSelectOne 1+            pure (us, ex)+          closePool pool+          length users `shouldBe` 5+          exists `shouldBe` True++mkPool :: IO Valiant.Pool+mkPool = do+  url <- requireDatabaseUrl'+  newPool defaultPoolConfig { poolConnString = url, poolSize = 2, poolAcquireTimeout = 5 }++requireDatabaseUrl' :: IO ByteString+requireDatabaseUrl' = do+  mUrl <- lookupEnv "DATABASE_URL"+  case mUrl of+    Just url -> pure (BS8.pack url)+    Nothing -> error "DATABASE_URL is not set."
+ test/TestSupport.hs view
@@ -0,0 +1,63 @@+module TestSupport+  ( withTestConnection+  , withSchema+  , insertTestUsers+  , stmtListAll+  , stmtSelectOne+  ) where++import Data.ByteString (ByteString)+import Data.ByteString.Char8 qualified as BS8+import Data.Int (Int32)+import Data.Text (Text)+import Valiant+import System.Environment (lookupEnv)++requireDatabaseUrl :: IO ByteString+requireDatabaseUrl = do+  mUrl <- lookupEnv "DATABASE_URL"+  case mUrl of+    Just url -> pure (BS8.pack url)+    Nothing -> error "DATABASE_URL is not set."++withTestConnection :: (Connection -> IO a) -> IO a+withTestConnection action = do+  url <- requireDatabaseUrl+  conn <- connectString url+  result <- action conn+  close conn+  pure result++withSchema :: Connection -> IO a -> IO a+withSchema conn action = do+  _ <- simpleQuery conn "DROP TABLE IF EXISTS users_effectful CASCADE"+  _ <- simpleQuery conn+    "CREATE TABLE users_effectful (\+    \  id SERIAL PRIMARY KEY,\+    \  name TEXT NOT NULL,\+    \  email TEXT\+    \)"+  result <- action+  _ <- simpleQuery conn "DROP TABLE IF EXISTS users_effectful CASCADE"+  pure result++insertTestUsers :: Connection -> IO ()+insertTestUsers conn = do+  _ <- simpleQuery conn+    "INSERT INTO users_effectful (name, email) VALUES \+    \('Alice', 'alice@example.com'),\+    \('Bob', 'bob@example.com'),\+    \('Carol', NULL),\+    \('Dave', 'dave@example.com'),\+    \('Eve', 'eve@example.com')"+  pure ()++stmtListAll :: Statement () (Int32, Text)+stmtListAll = mkStatement+  "SELECT id, name FROM users_effectful ORDER BY id"+  [] ["id", "name"] "<test>"++stmtSelectOne :: Statement Int32 (Int32, Text, Maybe Text)+stmtSelectOne = mkStatement+  "SELECT id, name, email FROM users_effectful WHERE id = $1"+  [23] ["id", "name", "email"] "<test>"
+ valiant-effectful.cabal view
@@ -0,0 +1,100 @@+cabal-version:   3.0+name:            valiant-effectful+version:         0.1.0.0+synopsis:        Effectful adapter for valiant+description:+  Provides an @Valiant@ effect for the @effectful@ effect system, enabling+  compile-time checked SQL queries as an effect with pool-based handlers.++  @+  import Valiant.Effectful++  myApp :: (Valiant :> es, IOE :> es) => Eff es [User]+  myApp = do+    users <- fetchAllEff listUsers ()+    forM_ users $ \\u -> executeEff updateLastSeen (userId u)+    pure users++  main :: IO ()+  main = do+    pool <- newPool defaultPoolConfig { poolConnString = "..." }+    runEff . runValiant pool $ myApp+  @+license:         BSD-3-Clause+license-file:    LICENSE+author:          Josh Burgess+maintainer:      joshburgess.webdev@gmail.com+category:        Database+homepage:        https://github.com/joshburgess/valiant+bug-reports:     https://github.com/joshburgess/valiant/issues+build-type:      Simple+extra-doc-files:+  README.md+  CHANGELOG.md+tested-with:     GHC ==9.10.3++source-repository head+  type:     git+  location: https://github.com/joshburgess/valiant+  subdir:   adapters/valiant-effectful++flag werror+  description: Enable -Werror for development builds.+  default:     False+  manual:      True++common warnings+  ghc-options: -Wall -Wcompat -Wno-unticked-promoted-constructors -funbox-strict-fields -fspecialise-aggressively+  if flag(werror)+    ghc-options: -Werror++library+  import:           warnings+  hs-source-dirs:   src+  default-language: GHC2021+  default-extensions:+    DataKinds+    DerivingStrategies+    GADTs+    LambdaCase+    OverloadedStrings+    StrictData+    TypeFamilies+    TypeOperators++  exposed-modules:+    Valiant.Effectful++  build-depends:+    , base            >=4.17 && <5+    , effectful-core  >=2.3  && <2.6+    , valiant         >=0.1  && <0.2+    , pg-wire         >=0.1  && <0.2+    , vector          >=0.12 && <0.14++test-suite valiant-effectful-test+  import:           warnings+  type:             exitcode-stdio-1.0+  hs-source-dirs:   test+  main-is:          Main.hs+  default-language: GHC2021+  default-extensions:+    DataKinds+    GADTs+    OverloadedStrings+    TypeOperators++  ghc-options: -rtsopts "-with-rtsopts=-K8K"++  build-depends:+    , base            >=4.17 && <5+    , bytestring      >=0.11 && <0.13+    , effectful-core   >=2.3  && <2.6+    , hspec           >=2.11 && <2.13+    , valiant+    , valiant-effectful+    , pg-wire+    , text            >=2.0  && <2.2++  other-modules:+    TestSupport