purescript-postgresql-client/src/Database/PostgreSQL.purs

351 lines
10 KiB
Haskell
Raw Normal View History

2016-12-22 18:12:38 +00:00
module Database.PostgreSQL
( module Row
, module Value
, PG
, PGError(..)
, PGErrorDetail
, Database
2016-12-22 18:12:38 +00:00
, PoolConfiguration
, Pool
, Connection
2016-12-22 19:25:17 +00:00
, Query(..)
2016-12-22 18:12:38 +00:00
, newPool
, withConnection
, withTransaction
, defaultPoolConfiguration
, command
2016-12-22 18:12:38 +00:00
, execute
, query
2017-01-12 16:12:59 +00:00
, scalar
, onIntegrityError
2016-12-22 18:12:38 +00:00
) where
2017-12-04 21:43:36 +00:00
import Prelude
import Control.Monad.Error.Class (catchError, throwError, try)
import Control.Monad.Except.Trans (ExceptT, except, runExceptT)
import Control.Monad.Trans.Class (lift)
import Data.Array (head)
2016-12-22 18:12:38 +00:00
import Data.Either (Either(..))
import Data.Generic.Rep (class Generic)
import Data.Generic.Rep.Show (genericShow)
import Data.Maybe (Maybe(..), maybe)
2016-12-22 19:25:17 +00:00
import Data.Newtype (class Newtype)
import Data.Nullable (Nullable, toMaybe, toNullable)
import Data.String (Pattern(..))
import Data.String as String
2016-12-22 18:12:38 +00:00
import Data.Traversable (traverse)
2018-07-15 17:51:17 +00:00
import Database.PostgreSQL.Row (class FromSQLRow, class ToSQLRow, Row0(..), Row1(..), Row10(..), Row11(..), Row12(..), Row13(..), Row14(..), Row15(..), Row16(..), Row17(..), Row18(..), Row19(..), Row2(..), Row3(..), Row4(..), Row5(..), Row6(..), Row7(..), Row8(..), Row9(..), fromSQLRow, toSQLRow) as Row
import Database.PostgreSQL.Row (class FromSQLRow, class ToSQLRow, Row0(..), Row1(..), fromSQLRow, toSQLRow)
import Database.PostgreSQL.Value (class FromSQLValue)
2018-07-15 17:51:17 +00:00
import Database.PostgreSQL.Value (class FromSQLValue, class ToSQLValue, fromSQLValue, instantFromString, instantToString, null, toSQLValue, unsafeIsBuffer) as Value
import Effect (Effect)
import Effect.Aff (Aff, bracket)
import Effect.Aff.Compat (EffectFnAff, fromEffectFnAff)
import Effect.Class (liftEffect)
import Effect.Exception (Error)
import Foreign (Foreign)
2016-12-22 18:12:38 +00:00
type Database = String
2016-12-22 18:12:38 +00:00
2018-11-19 06:25:59 +00:00
-- | PostgreSQL computations run in the `PG` monad. It's just `Aff` stacked with
-- | `ExceptT` to provide error handling.
-- |
-- | Errors originating from database queries or connection to the database are
-- | modeled with the `PGError` type. Use `runExceptT` from
-- | `Control.Monad.Except.Trans` to turn a `PG a` action into `Aff (Either
-- | PGError a)`.
type PG a = ExceptT PGError Aff a
2016-12-24 12:38:36 +00:00
-- | PostgreSQL connection pool configuration.
2016-12-22 18:12:38 +00:00
type PoolConfiguration =
{ database :: Database
, host :: Maybe String
, idleTimeoutMillis :: Maybe Int
, max :: Maybe Int
, password :: Maybe String
, port :: Maybe Int
, user :: Maybe String
}
defaultPoolConfiguration :: Database -> PoolConfiguration
defaultPoolConfiguration database =
{ database
, host: Nothing
, idleTimeoutMillis: Nothing
, max: Nothing
, password: Nothing
, port: Nothing
, user: Nothing
2016-12-22 18:12:38 +00:00
}
2016-12-24 12:38:36 +00:00
-- | PostgreSQL connection pool.
2017-04-20 08:05:17 +00:00
foreign import data Pool :: Type
2016-12-22 18:12:38 +00:00
2016-12-24 12:38:36 +00:00
-- | PostgreSQL connection.
2017-04-20 08:05:17 +00:00
foreign import data Connection :: Type
2016-12-22 18:12:38 +00:00
2016-12-24 12:38:36 +00:00
-- | PostgreSQL query with parameter (`$1`, `$2`, …) and return types.
2016-12-22 19:25:17 +00:00
newtype Query i o = Query String
derive instance newtypeQuery :: Newtype (Query i o) _
2016-12-24 12:38:36 +00:00
-- | Create a new connection pool.
2018-12-06 19:08:48 +00:00
newPool :: PoolConfiguration -> Effect Pool
newPool cfg =
2018-12-06 19:08:48 +00:00
ffiNewPool $ cfg'
where
cfg' =
{ user: toNullable cfg.user
, password: toNullable cfg.password
, host: toNullable cfg.host
, port: toNullable cfg.port
, database: cfg.database
, max: toNullable cfg.max
, idleTimeoutMillis: toNullable cfg.idleTimeoutMillis
}
-- | Configuration which we actually pass to FFI.
type PoolConfiguration' =
{ user :: Nullable String
, password :: Nullable String
, host :: Nullable String
, port :: Nullable Int
, database :: String
, max :: Nullable Int
, idleTimeoutMillis :: Nullable Int
}
foreign import ffiNewPool
:: PoolConfiguration'
2018-07-15 17:51:17 +00:00
-> Effect Pool
2016-12-22 18:12:38 +00:00
2017-12-05 21:12:01 +00:00
-- | Run an action with a connection. The connection is released to the pool
-- | when the action returns.
withConnection
2018-07-15 17:51:17 +00:00
:: a
2016-12-22 18:12:38 +00:00
. Pool
-> (Connection -> PG a)
-> PG a
withConnection p k =
except <=< lift $ bracket (connect p) cleanup run
where
cleanup (Left _) = pure unit
cleanup (Right { done }) = liftEffect done
run (Left err) = pure $ Left err
run (Right { connection }) = runExceptT $ k connection
2017-12-04 21:43:36 +00:00
connect
2018-07-15 17:51:17 +00:00
:: Pool
-> Aff (Either PGError ConnectResult)
connect =
fromEffectFnAff
<<< ffiConnect
{ nullableLeft: toNullable <<< map Left <<< convertError
, right: Right
}
type ConnectResult =
{ connection :: Connection
, done :: Effect Unit
}
2017-12-04 21:43:36 +00:00
foreign import ffiConnect
:: a
. { nullableLeft :: Error -> Nullable (Either PGError ConnectResult)
, right :: a -> Either PGError ConnectResult
}
-> Pool
-> EffectFnAff (Either PGError ConnectResult)
2017-12-04 21:43:36 +00:00
2016-12-24 12:38:36 +00:00
-- | Run an action within a transaction. The transaction is committed if the
2018-11-19 06:25:59 +00:00
-- | action returns cleanly, and rolled back if the action throws (either a
-- | `PGError` or a JavaScript exception in the Aff context). If you want to
2016-12-24 12:38:36 +00:00
-- | change the transaction mode, issue a separate `SET TRANSACTION` statement
-- | within the transaction.
2016-12-22 18:12:38 +00:00
withTransaction
2018-07-15 17:51:17 +00:00
:: a
2016-12-22 18:12:38 +00:00
. Connection
-> PG a
-> PG a
2016-12-22 18:12:38 +00:00
withTransaction conn action =
begin *> lift (try $ runExceptT action) >>= case _ of
Left jsErr -> do
rollback
lift $ throwError jsErr
Right (Left pgErr) -> do
rollback
throwError pgErr
Right (Right value) -> do
commit
pure value
where
begin = execute conn (Query "BEGIN TRANSACTION") Row0
commit = execute conn (Query "COMMIT TRANSACTION") Row0
rollback = execute conn (Query "ROLLBACK TRANSACTION") Row0
2016-12-22 18:12:38 +00:00
2016-12-24 12:38:36 +00:00
-- | Execute a PostgreSQL query and discard its results.
2016-12-22 18:12:38 +00:00
execute
2018-07-15 17:51:17 +00:00
:: i o
2016-12-22 18:12:38 +00:00
. (ToSQLRow i)
=> Connection
2016-12-22 19:25:17 +00:00
-> Query i o
2016-12-22 18:12:38 +00:00
-> i
-> PG Unit
2016-12-22 19:25:17 +00:00
execute conn (Query sql) values =
void $ unsafeQuery conn sql (toSQLRow values)
2016-12-22 18:12:38 +00:00
2016-12-24 12:38:36 +00:00
-- | Execute a PostgreSQL query and return its results.
2016-12-22 18:12:38 +00:00
query
2018-07-15 17:51:17 +00:00
:: i o
2017-04-20 08:05:17 +00:00
. ToSQLRow i
=> FromSQLRow o
2016-12-22 18:12:38 +00:00
=> Connection
2016-12-22 19:25:17 +00:00
-> Query i o
2016-12-22 18:12:38 +00:00
-> i
-> PG (Array o)
query conn (Query sql) values = do
_.rows <$> unsafeQuery conn sql (toSQLRow values)
>>= traverse (fromSQLRow >>> case _ of
Right row -> pure row
Left msg -> throwError $ ConversionError msg)
2016-12-22 18:12:38 +00:00
-- | Execute a PostgreSQL query and return the first field of the first row in
-- | the result.
2017-01-12 16:12:59 +00:00
scalar
2018-07-15 17:51:17 +00:00
:: i o
2017-04-20 08:05:17 +00:00
. ToSQLRow i
=> FromSQLValue o
2017-01-12 16:12:59 +00:00
=> Connection
-> Query i (Row1 o)
2017-01-12 16:12:59 +00:00
-> i
-> PG (Maybe o)
2017-01-12 16:12:59 +00:00
scalar conn sql values =
query conn sql values
<#> map (case _ of Row1 a -> a) <<< head
2017-01-12 16:12:59 +00:00
2018-10-09 06:30:12 +00:00
-- | Execute a PostgreSQL query and return its command tag value
-- | (how many rows were affected by the query). This may be useful
2018-11-19 06:25:59 +00:00
-- | for example with `DELETE` or `UPDATE` queries.
command
:: i
. ToSQLRow i
=> Connection
-> Query i Int
-> i
-> PG Int
command conn (Query sql) values =
_.rowCount <$> unsafeQuery conn sql (toSQLRow values)
type QueryResult =
{ rows :: Array (Array Foreign)
, rowCount :: Int
}
unsafeQuery
:: Connection
-> String
-> Array Foreign
-> PG QueryResult
unsafeQuery c s =
except <=< lift <<< fromEffectFnAff <<< ffiUnsafeQuery p c s
where
p =
{ nullableLeft: toNullable <<< map Left <<< convertError
, right: Right
}
foreign import ffiUnsafeQuery
:: { nullableLeft :: Error -> Nullable (Either PGError QueryResult)
, right :: QueryResult -> Either PGError QueryResult
}
-> Connection
-> String
-> Array Foreign
-> EffectFnAff (Either PGError QueryResult)
data PGError
= ConnectionError String
| ConversionError String
| InternalError PGErrorDetail
| OperationalError PGErrorDetail
| ProgrammingError PGErrorDetail
| IntegrityError PGErrorDetail
| DataError PGErrorDetail
| NotSupportedError PGErrorDetail
| QueryCanceledError PGErrorDetail
| TransactionRollbackError PGErrorDetail
derive instance eqPGError :: Eq PGError
derive instance genericPGError :: Generic PGError _
instance showPGError :: Show PGError where
show = genericShow
type PGErrorDetail =
{ severity :: String
, code :: String
, message :: String
, detail :: String
, hint :: String
, position :: String
, internalPosition :: String
, internalQuery :: String
, where_ :: String
, schema :: String
, table :: String
, column :: String
, dataType :: String
, constraint :: String
, file :: String
, line :: String
, routine :: String
}
foreign import ffiSQLState :: Error -> Nullable String
foreign import ffiErrorDetail :: Error -> PGErrorDetail
convertError :: Error -> Maybe PGError
convertError err =
case toMaybe $ ffiSQLState err of
Nothing -> Nothing
Just sqlState -> Just $ convert sqlState $ ffiErrorDetail err
where
convert :: String -> PGErrorDetail -> PGError
convert s =
if prefix "0A" s then NotSupportedError
else if prefix "20" s || prefix "21" s then ProgrammingError
else if prefix "22" s then DataError
else if prefix "23" s then IntegrityError
else if prefix "24" s || prefix "25" s then InternalError
else if prefix "26" s || prefix "27" s || prefix "28" s then OperationalError
else if prefix "2B" s || prefix "2D" s || prefix "2F" s then InternalError
else if prefix "34" s then OperationalError
else if prefix "38" s || prefix "39" s || prefix "3B" s then InternalError
else if prefix "3D" s || prefix "3F" s then ProgrammingError
else if prefix "40" s then TransactionRollbackError
else if prefix "42" s || prefix "44" s then ProgrammingError
else if s == "57014" then QueryCanceledError
else if prefix "5" s then OperationalError
else if prefix "F" s then InternalError
else if prefix "H" s then OperationalError
else if prefix "P" s then InternalError
else if prefix "X" s then InternalError
else const $ ConnectionError s
prefix :: String -> String -> Boolean
prefix p =
maybe false (_ == 0) <<< String.indexOf (Pattern p)
onIntegrityError :: forall a. PG a -> PG a -> PG a
onIntegrityError errorResult db =
catchError db handleError
where
handleError e =
case e of
IntegrityError _ -> errorResult
_ -> throwError e