packages feed

sqlite-simple-effectful-0.1.0.0: src/Effectful/SQLite/Simple/Labeled.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# OPTIONS_GHC -Wno-redundant-constraints #-}

{- ORMOLU_DISABLE -}
{- | A `SQLite` effect with a label attached, allowing multiple SQLite databases to be used in the same program.

Supported interpreters:

  * 'runSQLiteUnsync' - Runs a single-threaded action with a single connection.
  * 'runSQLiteSync' - Runs an action with a single connection shared across multiple threads.

>>> :set -XTypeApplications
>>> import Effectful
>>> import Effectful.Concurrent (runConcurrent)
>>> import Effectful.SQLite.Simple.Labeled (Labeled, SQLite)
>>> import Effectful.SQLite.Simple.Labeled qualified as SQL
>>> import Control.Concurrent.MVar qualified as MVar

>>> :{
app ::
  (Labeled "users" SQLite :> es)  =>
  (Labeled "products" SQLite :> es)  =>
  Eff es ()
app = do
  users <- SQL.useConnection @"users" \usersConn -> do
    SQL.query_ @User usersConn "SELECT * FROM users"
  products <- SQL.useConnection @"products" \productsConn -> do
    SQL.query_ @Product productsConn "SELECT * FROM products"
  pure ()
:}

>>> :{
main :: IO ()
main =
  SQL.withConnection "users.db" \usersConn -> do
    SQL.withConnection "products.db" \productsConn -> do
      usersConnVar <- MVar.newMVar usersConn
      productsConnVar <- MVar.newMVar productsConn
      app
        & SQL.runSQLiteSync @"users" usersConnVar
        & SQL.runSQLiteSync @"products" productsConnVar
        & runConcurrent
        & runEff
:}

-}
{- ORMOLU_ENABLE -}
module Effectful.SQLite.Simple.Labeled
  ( -- * Effects
    Labeled,
    SQLite (..),
    useConnection,
    LConnection (..),

    -- * Interpreters
    runSQLiteUnsync,
    runSQLiteSync,

    -- * Connections
    S.open,
    S.close,
    S.withConnection,
    S.setTrace,

    -- * Queries that return results
    query,
    query_,
    queryWith,
    queryWith_,
    queryNamed,
    lastInsertRowId,
    changes,
    totalChanges,

    -- * Queries that stream results
    fold,
    fold_,
    foldNamed,

    -- * Statements that do not return results
    execute,
    execute_,
    executeMany,
    executeNamed,
    S.field,

    -- * Transactions
    withTransaction,
    withImmediateTransaction,
    withExclusiveTransaction,
    withSavepoint,

    -- * Low-level statement API for stream access and prepared statements
    openStatement,
    closeStatement,
    withStatement,
    bind,
    bindNamed,
    reset,
    columnName,
    columnCount,
    withBind,
    nextRow,

    -- ** Exceptions
    S.FormatError (..),
    S.ResultError (..),
    S.SQLError (..),
    S.Error (..),

    -- * Types
    S.Query (..),
    S.Connection (..),
    S.ToRow (..),
    S.FromRow (..),
    S.Only (..),
    (S.:.) (..),
    S.SQLData (..),
    S.Statement (..),
    S.ColumnIndex (..),
    S.NamedParam (..),
  )
where

import Data.Int (Int64)
import Data.Text (Text)
import Database.SQLite.Simple (ColumnIndex, FromRow, NamedParam, Query, Statement, ToRow)
import Database.SQLite.Simple qualified as S
import Database.SQLite.Simple.FromRow (RowParser)
import Effectful
import Effectful.Concurrent (Concurrent)
import Effectful.Concurrent.MVar (MVar)
import Effectful.Dispatch.Dynamic (send)
import Effectful.Dispatch.Static (seqUnliftIO, unsafeEff, unsafeEff_)
import Effectful.Labeled
import Effectful.SQLite.Simple (SQLite)
import Effectful.SQLite.Simple qualified as SQL
import GHC.Stack (HasCallStack)

-- $setup
-- Clear all imports before running doctests
-- >>> :m
-- >>> :set -XOverloadedStrings
-- >>> import Data.Function ((&))
-- >>> import Effectful.SQLite.Simple (FromRow(fromRow))
-- >>> data User
-- >>> instance FromRow User where fromRow = undefined
-- >>> data Product
-- >>> instance FromRow Product where fromRow = undefined

----------------------------------------------------------------------------
-- Effect
----------------------------------------------------------------------------

-- | A labeled connection.
newtype LConnection (label :: k) = LConnection {getConn :: S.Connection}

-- | Retrieve the connection from the context and run the given action with it.
--
-- __WARNING__:
--
-- * The connection must not escape the scope of `useConnection`.
-- * `useConnection` calls must not be nested.
-- * When used together with other locking primitives, the locks must always be acquired in the same order to avoid deadlocks.
--
-- E.g., in the example below, the connections to the 2 databases are acquired out of order, which could lead to a deadlock.
--
-- >>> :{
-- import Effectful
-- import Effectful.SQLite.Simple.Labeled (Labeled, SQLite)
-- import Effectful.SQLite.Simple.Labeled qualified as SQL
-- f, g :: (Labeled "users" SQLite :> es, Labeled "products" SQLite :> es) => Eff es ()
-- f = do
--   SQL.useConnection @"users" \usersConn -> do
--     SQL.useConnection @"products" \productsConn -> do
--       pure ()
-- g = do
--   SQL.useConnection @"products" \productsConn -> do
--    SQL.useConnection @"users" \usersConn -> do
--       pure ()
-- :}
useConnection :: forall label es a. (Labeled label SQLite :> es) => (LConnection label -> Eff es a) -> Eff es a
useConnection use = send $ Labeled @label $ SQL.UseConnection \conn -> use (LConnection conn)

----------------------------------------------------------------------------
-- Interpreters
----------------------------------------------------------------------------

-- | Runs a single-threaded action with a single connection.
runSQLiteUnsync ::
  forall label es a.
  (HasCallStack, IOE :> es) =>
  S.Connection -> Eff (Labeled label SQLite ': es) a -> Eff es a
runSQLiteUnsync = runLabeled @label . SQL.runSQLiteUnsync

-- | Runs an action with a single connection shared across multiple threads.
--
-- __WARNING__: Since this interpreter is backed by an `MVar`, the usual caveats apply:
--
-- * The connection must not escape the scope of `useConnection`.
-- * `useConnection` calls must not be nested.
-- * When used together with other locking primitives, the locks must always be acquired in the same order to avoid deadlocks.
runSQLiteSync ::
  forall label es a.
  (HasCallStack, IOE :> es, Concurrent :> es) =>
  MVar S.Connection -> Eff (Labeled label SQLite ': es) a -> Eff es a
runSQLiteSync = runLabeled @label . SQL.runSQLiteSync

----------------------------------------------------------------------------
-- Operations
----------------------------------------------------------------------------

query :: forall q r label es. (Labeled label SQLite :> es) => (ToRow q, FromRow r) => LConnection label -> Query -> q -> Eff es [r]
query conn q params = unsafeEff_ $ S.query conn.getConn q params

query_ :: forall r label es. (Labeled label SQLite :> es) => (FromRow r) => LConnection label -> Query -> Eff es [r]
query_ conn q = unsafeEff_ $ S.query_ conn.getConn q

queryWith :: forall q r label es. (Labeled label SQLite :> es) => (ToRow q) => RowParser r -> LConnection label -> Query -> q -> Eff es [r]
queryWith parser conn q params = unsafeEff_ $ S.queryWith parser conn.getConn q params

queryWith_ :: forall r label es. (Labeled label SQLite :> es) => RowParser r -> LConnection label -> Query -> Eff es [r]
queryWith_ parser conn q = unsafeEff_ $ S.queryWith_ parser conn.getConn q

queryNamed :: forall r label es. (Labeled label SQLite :> es) => (FromRow r) => LConnection label -> Query -> [NamedParam] -> Eff es [r]
queryNamed conn q params = unsafeEff_ $ S.queryNamed conn.getConn q params

lastInsertRowId :: forall label es. (Labeled label SQLite :> es) => LConnection label -> Eff es Int64
lastInsertRowId = unsafeEff_ . S.lastInsertRowId . getConn

changes :: forall label es. (Labeled label SQLite :> es) => LConnection label -> Eff es Int
changes = unsafeEff_ . S.changes . getConn

totalChanges :: forall label es. (Labeled label SQLite :> es) => LConnection label -> Eff es Int
totalChanges = unsafeEff_ . S.totalChanges . getConn

fold :: forall row params a label es. (Labeled label SQLite :> es, FromRow row, ToRow params) => LConnection label -> Query -> params -> a -> (a -> row -> Eff es a) -> Eff es a
fold conn q params initialState action =
  unsafeEffWithUnlift @label \unlift -> do
    S.fold conn.getConn q params initialState \a row ->
      unlift $ action a row

fold_ :: forall row a label es. (Labeled label SQLite :> es) => (FromRow row) => LConnection label -> Query -> a -> (a -> row -> Eff es a) -> Eff es a
fold_ conn q initialState action =
  unsafeEffWithUnlift @label \unlift -> do
    S.fold_ conn.getConn q initialState \a row ->
      unlift $ action a row

foldNamed :: forall row a label es. (Labeled label SQLite :> es) => (FromRow row) => LConnection label -> Query -> [NamedParam] -> a -> (a -> row -> Eff es a) -> Eff es a
foldNamed conn q params initialState action =
  unsafeEffWithUnlift @label \unlift -> do
    S.foldNamed conn.getConn q params initialState \a row ->
      unlift $ action a row

execute :: forall q label es. (Labeled label SQLite :> es) => (ToRow q) => LConnection label -> Query -> q -> Eff es ()
execute conn q params = unsafeEff_ $ S.execute conn.getConn q params

execute_ :: forall label es. (Labeled label SQLite :> es) => LConnection label -> Query -> Eff es ()
execute_ conn q = unsafeEff_ $ S.execute_ conn.getConn q

executeMany :: forall q label es. (Labeled label SQLite :> es) => (ToRow q) => LConnection label -> Query -> [q] -> Eff es ()
executeMany conn q params = unsafeEff_ $ S.executeMany conn.getConn q params

executeNamed :: forall label es. (Labeled label SQLite :> es) => LConnection label -> Query -> [NamedParam] -> Eff es ()
executeNamed conn q params = unsafeEff_ $ S.executeNamed conn.getConn q params

withTransaction :: forall a label es. (Labeled label SQLite :> es) => LConnection label -> Eff es a -> Eff es a
withTransaction conn action =
  unsafeEffWithUnlift @label \unlift -> do
    S.withTransaction conn.getConn $ unlift action

withImmediateTransaction :: forall a label es. (Labeled label SQLite :> es) => LConnection label -> Eff es a -> Eff es a
withImmediateTransaction conn action =
  unsafeEffWithUnlift @label \unlift -> do
    S.withImmediateTransaction conn.getConn $ unlift action

withExclusiveTransaction :: forall a label es. (Labeled label SQLite :> es) => LConnection label -> Eff es a -> Eff es a
withExclusiveTransaction conn action =
  unsafeEffWithUnlift @label \unlift -> do
    S.withExclusiveTransaction conn.getConn $ unlift action

withSavepoint :: forall a label es. (Labeled label SQLite :> es) => LConnection label -> Eff es a -> Eff es a
withSavepoint conn action =
  unsafeEffWithUnlift @label \unlift -> do
    S.withSavepoint conn.getConn $ unlift action

openStatement :: forall label es. (Labeled label SQLite :> es) => LConnection label -> Query -> Eff es Statement
openStatement conn q = unsafeEff_ $ S.openStatement conn.getConn q

closeStatement :: forall label es. (Labeled label SQLite :> es) => Statement -> Eff es ()
closeStatement stmt = unsafeEff_ $ S.closeStatement stmt

withStatement :: forall a label es. (Labeled label SQLite :> es) => LConnection label -> Query -> (Statement -> Eff es a) -> Eff es a
withStatement conn q action =
  unsafeEffWithUnlift @label \unlift -> do
    S.withStatement conn.getConn q (unlift . action)

bind :: forall label params es. (Labeled label SQLite :> es) => (ToRow params) => Statement -> params -> Eff es ()
bind stmt params = unsafeEff_ $ S.bind stmt params

bindNamed :: forall label es. (Labeled label SQLite :> es) => Statement -> [NamedParam] -> Eff es ()
bindNamed stmt params = unsafeEff_ $ S.bindNamed stmt params

reset :: forall label es. (Labeled label SQLite :> es) => Statement -> Eff es ()
reset stmt = unsafeEff_ $ S.reset stmt

columnName :: forall label es. (Labeled label SQLite :> es) => Statement -> ColumnIndex -> Eff es Text
columnName stmt idx = unsafeEff_ $ S.columnName stmt idx

columnCount :: forall label es. (Labeled label SQLite :> es) => Statement -> Eff es ColumnIndex
columnCount stmt = unsafeEff_ $ S.columnCount stmt

withBind :: forall label params a es. (Labeled label SQLite :> es) => (ToRow params) => Statement -> params -> Eff es a -> Eff es a
withBind stmt params action =
  unsafeEffWithUnlift @label \unlift -> do
    S.withBind stmt params $ unlift action

nextRow :: forall label r es. (Labeled label SQLite :> es) => (FromRow r) => Statement -> Eff es (Maybe r)
nextRow stmt = unsafeEff_ $ S.nextRow stmt

----------------------------------------------------------------------------
-- Utils
----------------------------------------------------------------------------

unsafeEffWithUnlift :: forall label a es. (Labeled label SQLite :> es) => ((forall x. Eff es x -> IO x) -> IO a) -> Eff es a
unsafeEffWithUnlift action =
  unsafeEff \env -> do
    seqUnliftIO env \unlift -> do
      action unlift