{-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- | Functions for running Postgres queries. module Postgres ( -- Connection Connection.Connection, Connection.connection, -- Settings Settings.Settings, Settings.decoder, -- Querying Query.Query, Query.Error (..), Query.sql, doQuery, -- Handling transactions transaction, inTestTransaction, -- Reexposing useful postgresql-typed types PGTypes.PGColumn (pgDecode), PGTypes.PGParameter (pgEncode), ) where import qualified Control.Exception.Safe as Exception import qualified Data.Pool import Database.PostgreSQL.Typed (PGConnection) import qualified Database.PostgreSQL.Typed.Array as PGArray import Database.PostgreSQL.Typed.Protocol ( PGError, pgBegin, pgCommit, pgErrorCode, pgRollback, pgRollbackAll, ) import qualified Database.PostgreSQL.Typed.Types as PGTypes import GHC.Stack (HasCallStack, withFrozenCallStack) import qualified List import qualified Log import qualified Log.SqlQuery as SqlQuery import qualified Platform import Postgres.Connection (Connection) import qualified Postgres.Connection as Connection import qualified Postgres.Query as Query import qualified Postgres.Settings as Settings import qualified Postgres.Time as Time import qualified Task import qualified Tuple import qualified Prelude -- | -- Perform a database transaction. transaction :: Connection -> (Connection -> Task e a) -> Task e a transaction conn func = let start :: PGConnection -> Task x PGConnection start c = doIO conn <| do pgBegin c Prelude.pure c -- end :: Platform.Succeeded -> PGConnection -> Task x () end succeeded c = doIO conn <| case succeeded of Platform.Succeeded -> pgCommit c Platform.Failed -> pgRollback c Platform.FailedWith _ -> pgRollback c -- setSingle :: PGConnection -> Connection setSingle c = -- All queries in a transactions must run on the same thread. conn {Connection.singleOrPool = Connection.Single c} in withConnection conn <| \c -> Platform.bracketWithError (start c) end (setSingle >> func) -- | Run code in a transaction, then roll that transaction back. -- Useful in tests that shouldn't leave anything behind in the DB. inTestTransaction :: Connection -> (Connection -> Task x a) -> Task x a inTestTransaction conn func = let start :: PGConnection -> Task x PGConnection start c = do rollbackAllSafe conn c doIO conn <| pgBegin c Prelude.pure c -- end :: Platform.Succeeded -> PGConnection -> Task x () end _ c = rollbackAllSafe conn c -- setSingle :: PGConnection -> Connection setSingle c = -- All queries in a transactions must run on the same thread. conn {Connection.singleOrPool = Connection.Single c} in -- withConnection conn <| \c -> Platform.bracketWithError (start c) end (setSingle >> func) rollbackAllSafe :: Connection -> PGConnection -> Task x () rollbackAllSafe conn c = doIO conn <| do -- Because calling `rollbackAllTransactions` when no transactions are -- running will result in a warning message in the log (even if tests -- pass), let's start by beginning a transaction, so that we alwas have -- at least one to kill. pgBegin c pgRollbackAll c -- | Run a query against MySql. This will return a list of rows, where the @row@ -- type is a tuple containing the queried columns. -- -- > doQuery -- > connection -- > [sql| SELECT name, breed FROM doggos |] -- > (\result -> -- > case result of -- > Ok rows -> Task.succeed rows -- > Err err -> Task.fail err -- > ) doQuery :: HasCallStack => Connection -> Query.Query row -> (Result Query.Error [row] -> Task e a) -> Task e a doQuery conn query handleResponse = runQuery conn query -- Handle the response before wrapping the operation in a context. This way, -- if the response handling logic creates errors, those errors can inherit -- context values like the query string. |> ( \task -> withFrozenCallStack Platform.tracingSpan "Postgresql Query" <| do res <- Platform.finally task ( do Platform.setTracingSpanDetails queryInfo Platform.setTracingSpanSummary ( (SqlQuery.sqlOperation queryInfo |> Maybe.withDefault "?") ++ " " ++ (SqlQuery.queriedRelation queryInfo |> Maybe.withDefault "?") ) ) -- If we end up here it means the query succeeded. Overwrite the tracing -- details to contain the amount of selected rows. This information can be -- useful when debugging slow queries. Platform.setTracingSpanDetails queryInfo {SqlQuery.rowsReturned = Just (List.length res)} Prelude.pure res ) |> map Ok |> Task.onError (Task.succeed << Err) |> andThen handleResponse where queryInfo = Query.details query (Connection.connDetails conn) fromPGError :: Connection -> PGError -> Query.Error fromPGError c pgError = -- There's a lot of errors Postgres might throw. For a couple we have custom -- `Error` constructors defined, because we've seen a couple of them and would -- like to handle them in special ways or define custom error messages for -- them. If a Postgres error starts showing up in our log, please feel free -- to add a special case for it to this list! case pgErrorCode pgError of "23505" -> Exception.displayException pgError |> Text.fromList |> Query.UniqueViolation "57014" -> Query.Timeout (Time.milliseconds (Connection.timeout c)) _ -> Exception.displayException pgError |> Text.fromList -- We add the full error in the context array rather than the -- message string, to help errors being grouped correctly in a -- bug tracker. Errors might contain unique bits of data like -- generated id's or timestamps which when included in the main -- error message would result in each error being grouped by -- itself. |> (\err -> Query.Other "Postgres query failed with unexpected error" [Log.context "error" err]) -- -- CONNECTION HELPERS -- runQuery :: Connection -> Query.Query row -> Task Query.Error [row] runQuery conn query = withConnection conn <| \c -> Query.runQuery query c |> Exception.try |> map ( \res -> case res of Prelude.Right x -> Ok x Prelude.Left err -> Err (fromPGError conn err) ) |> Platform.doAnything (Connection.doAnything conn) |> withTimeout conn withTimeout :: Connection -> Task Query.Error a -> Task Query.Error a withTimeout conn task = if Time.microseconds (Connection.timeout conn) > 0 then Task.timeout (Time.milliseconds (Connection.timeout conn)) (Query.Timeout (Time.milliseconds (Connection.timeout conn))) task else task -- | by default, queries pull a connection from the connection pool. -- For SQL transactions, we want all queries within the transaction to run -- on the same connection. withConnection lets transaction bundle -- queries on the same connection. withConnection :: Connection -> (PGConnection -> Task e a) -> Task e a withConnection conn func = let acquire :: Data.Pool.Pool conn -> Task x (conn, Data.Pool.LocalPool conn) acquire pool = Log.withContext "acquiring Postgres connection from pool" [] <| doIO conn <| Data.Pool.takeResource pool -- release :: Data.Pool.Pool conn -> Platform.Succeeded -> (conn, Data.Pool.LocalPool conn) -> Task y () release pool succeeded (c, localPool) = doIO conn <| case succeeded of Platform.Succeeded -> Data.Pool.putResource localPool c Platform.Failed -> Data.Pool.destroyResource pool localPool c Platform.FailedWith _ -> Data.Pool.destroyResource pool localPool c in -- case Connection.singleOrPool conn of (Connection.Single c) -> func c -- (Connection.Pool pool) -> Platform.bracketWithError (acquire pool) (release pool) (Tuple.first >> func) doIO :: Connection -> Prelude.IO a -> Task x a doIO conn io = Platform.doAnything (Connection.doAnything conn) (io |> map Ok) -- useful typeclass instances instance PGTypes.PGType "jsonb" => PGTypes.PGType "jsonb[]" where type PGVal "jsonb[]" = PGArray.PGArray (PGTypes.PGVal "jsonb") instance PGTypes.PGType "jsonb" => PGArray.PGArrayType "jsonb[]" where type PGElemType "jsonb[]" = "jsonb"