fix: resource embedding opens empty transaction

This required untangling logic from App.hs.
Building/rejecting a plan no longer consumes a pool connection.

* Added io test for failed resource embedding not consuming connection
* optionalrollback to Query.hs and Response.hs
* delete Middleware module
* remove plan logic from App.hs
* remove RequestContext
* remove pkCols logic from App.hs
* remove proc logic from App.hs
* remove config logic from handleRequest
This commit is contained in:
steve-chavez
2022-10-07 18:52:57 -05:00
committed by Steve Chavez
parent d37e14c4db
commit e4b98d51be
9 changed files with 202 additions and 236 deletions
-1
View File
@@ -53,7 +53,6 @@ library
PostgREST.Error PostgREST.Error
PostgREST.GucHeader PostgREST.GucHeader
PostgREST.Logger PostgREST.Logger
PostgREST.Middleware
PostgREST.MediaType PostgREST.MediaType
PostgREST.Query PostgREST.Query
PostgREST.Query.QueryBuilder PostgREST.Query.QueryBuilder
+58 -132
View File
@@ -26,7 +26,6 @@ import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
setServerName) setServerName)
import System.Posix.Types (FileMode) import System.Posix.Types (FileMode)
import qualified Data.HashMap.Strict as HM
import qualified Hasql.Transaction.Sessions as SQL import qualified Hasql.Transaction.Sessions as SQL
import qualified Network.Wai as Wai import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Handler.Warp as Warp
@@ -37,47 +36,26 @@ import qualified PostgREST.Auth as Auth
import qualified PostgREST.Cors as Cors import qualified PostgREST.Cors as Cors
import qualified PostgREST.Error as Error import qualified PostgREST.Error as Error
import qualified PostgREST.Logger as Logger import qualified PostgREST.Logger as Logger
import qualified PostgREST.Middleware as Middleware
import qualified PostgREST.Plan as Plan import qualified PostgREST.Plan as Plan
import qualified PostgREST.Query as Query import qualified PostgREST.Query as Query
import qualified PostgREST.Request.ApiRequest as ApiRequest import qualified PostgREST.Request.ApiRequest as ApiRequest
import qualified PostgREST.Request.Types as ApiRequestTypes import qualified PostgREST.Request.Types as ApiRequestTypes
import qualified PostgREST.Response as Response import qualified PostgREST.Response as Response
import PostgREST.AppState (AppState) import PostgREST.AppState (AppState)
import PostgREST.Auth (AuthResult (..)) import PostgREST.Auth (AuthResult (..))
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..), LogLevel (..))
LogLevel (..)) import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.Config.PgVersion (PgVersion (..)) import PostgREST.DbStructure (DbStructure (..))
import PostgREST.DbStructure (DbStructure (..)) import PostgREST.Error (Error)
import PostgREST.DbStructure.Identifiers (FieldName, import PostgREST.Query (DbHandler)
QualifiedIdentifier (..), import PostgREST.Request.ApiRequest (Action (..), ApiRequest (..),
Schema) Mutation (..), Target (..))
import PostgREST.DbStructure.Proc (ProcDescription (..)) import PostgREST.Version (prettyVersion)
import PostgREST.DbStructure.Table (Table (..)) import PostgREST.Workers (connectionWorker, listener)
import PostgREST.Error (Error)
import PostgREST.Plan.MutatePlan (MutatePlan)
import PostgREST.Plan.ReadPlan (ReadPlanTree)
import PostgREST.Query (DbHandler)
import PostgREST.Request.ApiRequest (Action (..),
ApiRequest (..),
InvokeMethod (..),
Mutation (..), Target (..))
import PostgREST.Request.Preferences (PreferRepresentation (..))
import PostgREST.Version (prettyVersion)
import PostgREST.Workers (connectionWorker, listener)
import qualified PostgREST.DbStructure.Proc as Proc
import Protolude hiding (Handler) import Protolude hiding (Handler)
data RequestContext = RequestContext
{ ctxConfig :: AppConfig
, ctxDbStructure :: DbStructure
, ctxApiRequest :: ApiRequest
, ctxPgVersion :: PgVersion
}
type Handler = ExceptT Error type Handler = ExceptT Error
type SignalHandlerInstaller = AppState -> IO() type SignalHandlerInstaller = AppState -> IO()
@@ -163,7 +141,7 @@ postgrestResponse
-> AuthResult -> AuthResult
-> Wai.Request -> Wai.Request
-> Handler IO Wai.Response -> Handler IO Wai.Response
postgrestResponse appState conf@AppConfig{..} maybeDbStructure jsonDbS pgVer AuthResult{..} req = do postgrestResponse appState conf@AppConfig{..} maybeDbStructure jsonDbS pgVer authResult@AuthResult{..} req = do
dbStructure <- dbStructure <-
case maybeDbStructure of case maybeDbStructure of
Just dbStructure -> Just dbStructure ->
@@ -177,15 +155,8 @@ postgrestResponse appState conf@AppConfig{..} maybeDbStructure jsonDbS pgVer Aut
liftEither . mapLeft Error.ApiRequestError $ liftEither . mapLeft Error.ApiRequestError $
ApiRequest.userApiRequest conf dbStructure req body ApiRequest.userApiRequest conf dbStructure req body
let ctx apiReq = RequestContext conf dbStructure apiReq pgVer Response.optionalRollback conf apiRequest $
handleRequest authResult conf appState (Query.txMode apiRequest) (Just authRole /= configDbAnonRole) configDbPreparedStatements jsonDbS pgVer apiRequest dbStructure
if iAction apiRequest == ActionInfo then
pure $ Response.infoResponse (iTarget apiRequest) dbStructure
else
runDbHandler appState (Query.txMode apiRequest) (Just authRole /= configDbAnonRole) configDbPreparedStatements .
Middleware.optionalRollback conf apiRequest $ do
Query.setPgLocals conf authClaims authRole apiRequest jsonDbS pgVer
handleRequest (ctx apiRequest)
runDbHandler :: AppState.AppState -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b runDbHandler :: AppState.AppState -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
runDbHandler appState mode authenticated prepared handler = do runDbHandler appState mode authenticated prepared handler = do
@@ -199,97 +170,52 @@ runDbHandler appState mode authenticated prepared handler = do
liftEither resp liftEither resp
handleRequest :: RequestContext -> DbHandler Wai.Response handleRequest :: AuthResult -> AppConfig -> AppState.AppState -> SQL.Mode -> Bool -> Bool -> ByteString -> PgVersion -> ApiRequest -> DbStructure -> Handler IO Wai.Response
handleRequest context@(RequestContext _ _ ApiRequest{..} _) = handleRequest AuthResult{..} conf appState mode authenticated prepared jsonDbS pgVer apiReq@ApiRequest{..} dbStructure =
case (iAction, iTarget) of case (iAction, iTarget) of
(ActionRead headersOnly, TargetIdent identifier) -> (ActionRead headersOnly, TargetIdent identifier) -> do
handleRead headersOnly identifier context rPlan <- liftEither $ Plan.readPlan identifier conf dbStructure apiReq
(ActionMutate MutationCreate, TargetIdent identifier) -> resultSet <- runQuery $ Query.readQuery rPlan conf apiReq
handleCreate identifier context return $ Response.readResponse headersOnly identifier apiReq resultSet
(ActionMutate MutationUpdate, TargetIdent identifier) ->
handleUpdate identifier context (ActionMutate MutationCreate, TargetIdent identifier) -> do
(ActionMutate MutationSingleUpsert, TargetIdent identifier) -> mrPlan <- liftEither $ Plan.mutateReadPlan MutationCreate apiReq identifier conf dbStructure
handleSingleUpsert identifier context resultSet <- runQuery $ Query.createQuery mrPlan apiReq conf
(ActionMutate MutationDelete, TargetIdent identifier) -> return $ Response.createResponse identifier mrPlan apiReq resultSet
handleDelete identifier context
(ActionInvoke invMethod, TargetProc proc _) -> (ActionMutate MutationUpdate, TargetIdent identifier) -> do
handleInvoke invMethod proc context mrPlan <- liftEither $ Plan.mutateReadPlan MutationUpdate apiReq identifier conf dbStructure
(ActionInspect headersOnly, TargetDefaultSpec tSchema) -> resultSet <- runQuery $ Query.updateQuery mrPlan apiReq conf
handleOpenApi headersOnly tSchema context return $ Response.updateResponse apiReq resultSet
(ActionMutate MutationSingleUpsert, TargetIdent identifier) -> do
mrPlan <- liftEither $ Plan.mutateReadPlan MutationSingleUpsert apiReq identifier conf dbStructure
resultSet <- runQuery $ Query.singleUpsertQuery mrPlan apiReq conf
return $ Response.singleUpsertResponse apiReq resultSet
(ActionMutate MutationDelete, TargetIdent identifier) -> do
mrPlan <- liftEither $ Plan.mutateReadPlan MutationDelete apiReq identifier conf dbStructure
resultSet <- runQuery $ Query.deleteQuery mrPlan apiReq conf
return $ Response.deleteResponse apiReq resultSet
(ActionInvoke invMethod, TargetProc proc _) -> do
cPlan <- liftEither $ Plan.callReadPlan proc conf dbStructure apiReq
resultSet <- runQuery $ Query.invokeQuery proc cPlan apiReq conf
return $ Response.invokeResponse invMethod proc apiReq resultSet
(ActionInspect headersOnly, TargetDefaultSpec tSchema) -> do
oaiResult <- runQuery $ Query.openApiQuery dbStructure pgVer conf tSchema
return $ Response.openApiResponse headersOnly oaiResult conf dbStructure iSchema iNegotiatedByProfile
(ActionInfo, _) ->
return $ Response.infoResponse iTarget dbStructure
_ -> _ ->
-- This is unreachable as the ApiRequest.hs rejects it before -- This is unreachable as the ApiRequest.hs rejects it before
-- TODO Refactor the Action/Target types to remove this line -- TODO Refactor the Action/Target types to remove this line
throwError $ Error.ApiRequestError ApiRequestTypes.NotFound throwError $ Error.ApiRequestError ApiRequestTypes.NotFound
where
handleRead :: Bool -> QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response runQuery query =
handleRead headersOnly identifier context@RequestContext{..} = do runDbHandler appState mode authenticated prepared $ do
req <- liftEither $ readPlan identifier context Query.setPgLocals conf authClaims authRole apiReq jsonDbS pgVer
query
resultSet <- Query.readQuery req ctxConfig ctxApiRequest
pure $ Response.readResponse headersOnly identifier ctxApiRequest resultSet
handleCreate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
handleCreate identifier context@RequestContext{..} = do
let
ApiRequest{..} = ctxApiRequest
pkCols = if iPreferRepresentation /= None || isJust iPreferResolution
then maybe mempty tablePKCols $ HM.lookup identifier $ dbTables ctxDbStructure
else mempty
(mutateReq, readReq) <- liftEither $ mutatePlan MutationCreate identifier context pkCols
resultSet <- Query.createQuery mutateReq readReq pkCols ctxApiRequest ctxConfig
pure $ Response.createResponse identifier pkCols ctxApiRequest resultSet
handleUpdate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
handleUpdate identifier context@(RequestContext ctxConfig _ ctxApiRequest _) = do
(mutateReq, readReq) <- liftEither $ mutatePlan MutationUpdate identifier context mempty
resultSet <- Query.updateQuery mutateReq readReq ctxApiRequest ctxConfig
pure $ Response.updateResponse ctxApiRequest resultSet
handleSingleUpsert :: QualifiedIdentifier -> RequestContext-> DbHandler Wai.Response
handleSingleUpsert identifier context@(RequestContext ctxConfig ctxDbStructure ctxApiRequest _) = do
let pkCols = maybe mempty tablePKCols $ HM.lookup identifier $ dbTables ctxDbStructure
(mutateReq, readReq) <- liftEither $ mutatePlan MutationSingleUpsert identifier context pkCols
resultSet <- Query.singleUpsertQuery mutateReq readReq ctxApiRequest ctxConfig
pure $ Response.singleUpsertResponse ctxApiRequest resultSet
handleDelete :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
handleDelete identifier context@(RequestContext ctxConfig _ ctxApiRequest _) = do
(mutateReq, readReq) <- liftEither $ mutatePlan MutationDelete identifier context mempty
resultSet <- Query.deleteQuery mutateReq readReq ctxApiRequest ctxConfig
pure $ Response.deleteResponse ctxApiRequest resultSet
handleInvoke :: InvokeMethod -> ProcDescription -> RequestContext -> DbHandler Wai.Response
handleInvoke invMethod proc context@RequestContext{..} = do
let
identifier =
QualifiedIdentifier
(pdSchema proc)
(fromMaybe (pdName proc) $ Proc.procTableName proc)
readReq <- liftEither $ readPlan identifier context
let callReq = Plan.callPlan proc ctxApiRequest readReq
resultSet <- Query.invokeQuery proc callReq readReq ctxApiRequest ctxConfig
pure $ Response.invokeResponse invMethod proc ctxApiRequest resultSet
handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response
handleOpenApi headersOnly tSchema (RequestContext conf dbStructure apiRequest pgVer) = do
oaiResult <- Query.openApiQuery dbStructure pgVer conf tSchema
pure $ Response.openApiResponse headersOnly oaiResult conf dbStructure (iSchema apiRequest) (iNegotiatedByProfile apiRequest)
mutatePlan :: Mutation -> QualifiedIdentifier -> RequestContext -> [FieldName] -> Either Error (MutatePlan, ReadPlanTree)
mutatePlan mutation identifier@QualifiedIdentifier{..} context@RequestContext{..} pkCols = do
readReq <- readPlan identifier context
mutateReq <- Plan.mutatePlan mutation qiSchema qiName ctxApiRequest pkCols readReq
pure (mutateReq, readReq)
readPlan :: QualifiedIdentifier -> RequestContext -> Either Error ReadPlanTree
readPlan QualifiedIdentifier{..} (RequestContext AppConfig{..} dbStructure apiRequest _) =
Plan.readPlan qiSchema qiName configDbMaxRows
(dbRelationships dbStructure)
apiRequest
-50
View File
@@ -1,50 +0,0 @@
{-|
Module : PostgREST.Middleware
Description : Sets CORS policy. Also the PostgreSQL GUCs, role, search_path and pre-request function.
-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE RecordWildCards #-}
module PostgREST.Middleware
( optionalRollback
) where
import qualified Hasql.Transaction as SQL
import qualified Network.Wai as Wai
import PostgREST.Config (AppConfig (..))
import PostgREST.Error (Error, errorResponseFor)
import PostgREST.GucHeader (addHeadersIfNotIncluded)
import PostgREST.Request.ApiRequest (ApiRequest (..))
import PostgREST.Request.Preferences
import Protolude
-- | Set a transaction to eventually roll back if requested and set respective
-- headers on the response.
optionalRollback
:: AppConfig
-> ApiRequest
-> ExceptT Error SQL.Transaction Wai.Response
-> ExceptT Error SQL.Transaction Wai.Response
optionalRollback AppConfig{..} ApiRequest{..} transaction = do
resp <- catchError transaction $ return . errorResponseFor
when (shouldRollback || (configDbTxRollbackAll && not shouldCommit)) $ lift do
SQL.sql "SET CONSTRAINTS ALL IMMEDIATE"
SQL.condemn
return $ Wai.mapResponseHeaders preferenceApplied resp
where
shouldCommit =
configDbTxAllowOverride && iPreferTransaction == Just Commit
shouldRollback =
configDbTxAllowOverride && iPreferTransaction == Just Rollback
preferenceApplied
| shouldCommit =
addHeadersIfNotIncluded
[toAppliedHeader Commit]
| shouldRollback =
addHeadersIfNotIncluded
[toAppliedHeader Rollback]
| otherwise =
identity
+47 -17
View File
@@ -17,20 +17,25 @@ resource.
module PostgREST.Plan module PostgREST.Plan
( readPlan ( readPlan
, mutatePlan , mutateReadPlan
, callPlan , callReadPlan
, MutateReadPlan(..)
, CallReadPlan(..)
) where ) where
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.Set as S import qualified Data.Set as S
import qualified PostgREST.DbStructure.Proc as Proc
import Data.Either.Combinators (mapLeft) import Data.Either.Combinators (mapLeft)
import Data.List (delete) import Data.List (delete)
import Data.Tree (Tree (..)) import Data.Tree (Tree (..))
import PostgREST.Config (AppConfig (..))
import PostgREST.DbStructure (DbStructure (..))
import PostgREST.DbStructure.Identifiers (FieldName, import PostgREST.DbStructure.Identifiers (FieldName,
QualifiedIdentifier (..), QualifiedIdentifier (..),
Schema, TableName) Schema)
import PostgREST.DbStructure.Proc (ProcDescription (..), import PostgREST.DbStructure.Proc (ProcDescription (..),
ProcParam (..), ProcParam (..),
procReturnsScalar) procReturnsScalar)
@@ -38,6 +43,7 @@ import PostgREST.DbStructure.Relationship (Cardinality (..),
Junction (..), Junction (..),
Relationship (..), Relationship (..),
RelationshipsMap) RelationshipsMap)
import PostgREST.DbStructure.Table (tablePKCols)
import PostgREST.Error (Error (..)) import PostgREST.Error (Error (..))
import PostgREST.Query.SqlFragment (sourceCTEName) import PostgREST.Query.SqlFragment (sourceCTEName)
import PostgREST.RangeQuery (NonnegRange, allRange, import PostgREST.RangeQuery (NonnegRange, allRange,
@@ -50,7 +56,8 @@ import PostgREST.Request.ApiRequest (Action (..),
import PostgREST.Plan.CallPlan import PostgREST.Plan.CallPlan
import PostgREST.Plan.MutatePlan import PostgREST.Plan.MutatePlan
import PostgREST.Plan.ReadPlan as ReadPlan import PostgREST.Plan.ReadPlan as ReadPlan
import PostgREST.Request.Preferences import PostgREST.Request.Preferences
import PostgREST.Request.Types import PostgREST.Request.Types
@@ -58,14 +65,37 @@ import qualified PostgREST.Request.QueryParams as QueryParams
import Protolude hiding (from) import Protolude hiding (from)
data MutateReadPlan = MutateReadPlan {
mrReadPlan :: ReadPlanTree
, mrMutatePlan :: MutatePlan
}
data CallReadPlan = CallReadPlan {
crReadPlan :: ReadPlanTree
, crCallPlan :: CallPlan
}
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> DbStructure -> Either Error MutateReadPlan
mutateReadPlan mutation apiRequest identifier conf dbStructure = do
rPlan <- readPlan identifier conf dbStructure apiRequest
mPlan <- mutatePlan mutation identifier apiRequest dbStructure rPlan
return $ MutateReadPlan rPlan mPlan
callReadPlan :: ProcDescription -> AppConfig -> DbStructure -> ApiRequest -> Either Error CallReadPlan
callReadPlan proc conf dbStructure apiRequest = do
let identifier = QualifiedIdentifier (pdSchema proc) (fromMaybe (pdName proc) $ Proc.procTableName proc)
rPlan <- readPlan identifier conf dbStructure apiRequest
let cPlan = callPlan proc apiRequest rPlan
return $ CallReadPlan rPlan cPlan
-- | Builds the ReadPlan tree on a number of stages. -- | Builds the ReadPlan tree on a number of stages.
-- | Adds filters, order, limits on its respective nodes. -- | Adds filters, order, limits on its respective nodes.
-- | Adds joins conditions obtained from resource embedding. -- | Adds joins conditions obtained from resource embedding.
readPlan :: Schema -> TableName -> Maybe Integer -> RelationshipsMap -> ApiRequest -> Either Error ReadPlanTree readPlan :: QualifiedIdentifier -> AppConfig -> DbStructure -> ApiRequest -> Either Error ReadPlanTree
readPlan schema rootTableName maxRows allRels apiRequest = readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows} DbStructure{dbRelationships} apiRequest =
mapLeft ApiRequestError $ mapLeft ApiRequestError $
treeRestrictRange maxRows (iAction apiRequest) =<< treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
augmentRequestWithJoin schema allRels =<< augmentRequestWithJoin qiSchema dbRelationships =<<
addLogicTrees apiRequest =<< addLogicTrees apiRequest =<<
addRanges apiRequest =<< addRanges apiRequest =<<
addOrders apiRequest =<< addOrders apiRequest =<<
@@ -73,9 +103,9 @@ readPlan schema rootTableName maxRows allRels apiRequest =
where where
QueryParams.QueryParams{..} = iQueryParams apiRequest QueryParams.QueryParams{..} = iQueryParams apiRequest
(rootName, rootAlias) = case iAction apiRequest of (rootName, rootAlias) = case iAction apiRequest of
ActionRead _ -> (QualifiedIdentifier schema rootTableName, Nothing) ActionRead _ -> (qi, Nothing)
-- the CTE we use for non-read cases has a sourceCTEName(see Statements.hs) as the WITH name so we use the table name as an alias so findRel can find the right relationship -- the CTE we use for non-read cases has a sourceCTEName(see Statements.hs) as the WITH name so we use the table name as an alias so findRel can find the right relationship
_ -> (QualifiedIdentifier mempty $ decodeUtf8 sourceCTEName, Just rootTableName) _ -> (QualifiedIdentifier mempty $ decodeUtf8 sourceCTEName, Just qiName)
-- Build the initial tree with a Depth attribute so when a self join occurs we -- Build the initial tree with a Depth attribute so when a self join occurs we
-- can differentiate the parent and child tables by having an alias like -- can differentiate the parent and child tables by having an alias like
@@ -305,11 +335,11 @@ updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
findNode :: Maybe ReadPlanTree findNode :: Maybe ReadPlanTree
findNode = find (\(Node ReadPlan{nodeName, nodeAlias} _) -> nodeName == targetNodeName || nodeAlias == Just targetNodeName) forest findNode = find (\(Node ReadPlan{nodeName, nodeAlias} _) -> nodeName == targetNodeName || nodeAlias == Just targetNodeName) forest
mutatePlan :: Mutation -> Schema -> TableName -> ApiRequest -> [FieldName] -> ReadPlanTree -> Either Error MutatePlan mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> DbStructure -> ReadPlanTree -> Either Error MutatePlan
mutatePlan mutation schema tName ApiRequest{..} pkCols readReq = mapLeft ApiRequestError $ mutatePlan mutation qi ApiRequest{..} dbStructure readReq = mapLeft ApiRequestError $
case mutation of case mutation of
MutationCreate -> MutationCreate ->
Right $ Insert qi iColumns body ((,) <$> iPreferResolution <*> Just confCols) [] returnings Right $ Insert qi iColumns body ((,) <$> iPreferResolution <*> Just confCols) [] returnings pkCols
MutationUpdate -> Right $ Update qi iColumns body combinedLogic iTopLevelRange rootOrder returnings MutationUpdate -> Right $ Update qi iColumns body combinedLogic iTopLevelRange rootOrder returnings
MutationSingleUpsert -> MutationSingleUpsert ->
if null qsLogic && if null qsLogic &&
@@ -318,18 +348,18 @@ mutatePlan mutation schema tName ApiRequest{..} pkCols readReq = mapLeft ApiRequ
all (\case all (\case
Filter _ (OpExpr False (Op OpEqual _)) -> True Filter _ (OpExpr False (Op OpEqual _)) -> True
_ -> False) qsFiltersRoot _ -> False) qsFiltersRoot
then Right $ Insert qi iColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings then Right $ Insert qi iColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty
else else
Left InvalidFilters Left InvalidFilters
MutationDelete -> Right $ Delete qi combinedLogic iTopLevelRange rootOrder returnings MutationDelete -> Right $ Delete qi combinedLogic iTopLevelRange rootOrder returnings
where where
confCols = fromMaybe pkCols qsOnConflict confCols = fromMaybe pkCols qsOnConflict
QueryParams.QueryParams{..} = iQueryParams QueryParams.QueryParams{..} = iQueryParams
qi = QualifiedIdentifier schema tName
returnings = returnings =
if iPreferRepresentation == None if iPreferRepresentation == None
then [] then []
else returningCols readReq pkCols else returningCols readReq pkCols
pkCols = maybe mempty tablePKCols $ HM.lookup qi $ dbTables dbStructure
logic = map snd qsLogic logic = map snd qsLogic
rootOrder = maybe [] snd $ find (\(x, _) -> null x) qsOrder rootOrder = maybe [] snd $ find (\(x, _) -> null x) qsOrder
combinedLogic = foldr addFilterToLogicForest logic qsFiltersRoot combinedLogic = foldr addFilterToLogicForest logic qsFiltersRoot
+1
View File
@@ -22,6 +22,7 @@ data MutatePlan
, onConflict :: Maybe (PreferResolution, [FieldName]) , onConflict :: Maybe (PreferResolution, [FieldName])
, where_ :: [LogicTree] , where_ :: [LogicTree]
, returning :: [FieldName] , returning :: [FieldName]
, insPkCols :: [FieldName]
} }
| Update | Update
{ in_ :: QualifiedIdentifier { in_ :: QualifiedIdentifier
+48 -25
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
module PostgREST.Query module PostgREST.Query
( createQuery ( createQuery
@@ -38,8 +39,7 @@ import PostgREST.Config (AppConfig (..),
import PostgREST.Config.PgVersion (PgVersion (..), import PostgREST.Config.PgVersion (PgVersion (..),
pgVersion140) pgVersion140)
import PostgREST.DbStructure (DbStructure (..)) import PostgREST.DbStructure (DbStructure (..))
import PostgREST.DbStructure.Identifiers (FieldName, import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..),
QualifiedIdentifier (..),
Schema) Schema)
import PostgREST.DbStructure.Proc (ProcDescription (..), import PostgREST.DbStructure.Proc (ProcDescription (..),
ProcVolatility (..), ProcVolatility (..),
@@ -47,8 +47,9 @@ import PostgREST.DbStructure.Proc (ProcDescription (..),
import PostgREST.DbStructure.Table (TablesMap) import PostgREST.DbStructure.Table (TablesMap)
import PostgREST.Error (Error) import PostgREST.Error (Error)
import PostgREST.MediaType (MediaType (..)) import PostgREST.MediaType (MediaType (..))
import PostgREST.Plan.CallPlan (CallPlan) import PostgREST.Plan (CallReadPlan (..),
import PostgREST.Plan.MutatePlan (MutatePlan) MutateReadPlan (..))
import PostgREST.Plan.MutatePlan (MutatePlan (..))
import PostgREST.Plan.ReadPlan (ReadPlanTree) import PostgREST.Plan.ReadPlan (ReadPlanTree)
import PostgREST.Query.SqlFragment (fromQi, intercalateSnippet, import PostgREST.Query.SqlFragment (fromQi, intercalateSnippet,
pgFmtIdentList, pgFmtIdentList,
@@ -61,6 +62,7 @@ import PostgREST.Request.ApiRequest (Action (..),
Target (..)) Target (..))
import PostgREST.Request.Preferences (PreferCount (..), import PostgREST.Request.Preferences (PreferCount (..),
PreferParameters (..), PreferParameters (..),
PreferTransaction (..),
shouldCount) shouldCount)
import Protolude hiding (Handler) import Protolude hiding (Handler)
@@ -85,6 +87,7 @@ readQuery req conf@AppConfig{..} apiReq@ApiRequest{..} = do
iBinaryField iBinaryField
configDbPreparedStatements configDbPreparedStatements
failNotSingular iAcceptMediaType resultSet failNotSingular iAcceptMediaType resultSet
optionalRollback conf apiReq
resultSetWTotal conf apiReq resultSet countQuery resultSetWTotal conf apiReq resultSet countQuery
resultSetWTotal :: AppConfig -> ApiRequest -> ResultSet -> SQL.Snippet -> DbHandler ResultSet resultSetWTotal :: AppConfig -> ApiRequest -> ResultSet -> SQL.Snippet -> DbHandler ResultSet
@@ -109,23 +112,26 @@ resultSetWTotal AppConfig{..} ApiRequest{..} rs@RSStandard{rsTableTotal=tableTot
lift . SQL.statement mempty . Statements.preparePlanRows countQuery $ lift . SQL.statement mempty . Statements.preparePlanRows countQuery $
configDbPreparedStatements configDbPreparedStatements
createQuery :: MutatePlan -> ReadPlanTree -> [FieldName] -> ApiRequest -> AppConfig -> DbHandler ResultSet createQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
createQuery mutateReq readReq pkCols apiReq@ApiRequest{..} conf = do createQuery mrPlan apiReq@ApiRequest{..} conf = do
resultSet <- writeQuery mutateReq readReq True pkCols apiReq conf resultSet <- writeQuery mrPlan apiReq conf
failNotSingular iAcceptMediaType resultSet failNotSingular iAcceptMediaType resultSet
optionalRollback conf apiReq
pure resultSet pure resultSet
updateQuery :: MutatePlan -> ReadPlanTree -> ApiRequest -> AppConfig -> DbHandler ResultSet updateQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
updateQuery mutateReq readReq apiReq@ApiRequest{..} conf = do updateQuery mrPlan apiReq@ApiRequest{..} conf = do
resultSet <- writeQuery mutateReq readReq False mempty apiReq conf resultSet <- writeQuery mrPlan apiReq conf
failNotSingular iAcceptMediaType resultSet failNotSingular iAcceptMediaType resultSet
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
optionalRollback conf apiReq
pure resultSet pure resultSet
singleUpsertQuery :: MutatePlan -> ReadPlanTree -> ApiRequest -> AppConfig -> DbHandler ResultSet singleUpsertQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
singleUpsertQuery mutateReq readReq apiReq conf = do singleUpsertQuery mrPlan apiReq conf = do
resultSet <- writeQuery mutateReq readReq False mempty apiReq conf resultSet <- writeQuery mrPlan apiReq conf
failPut resultSet failPut resultSet
optionalRollback conf apiReq
pure resultSet pure resultSet
-- Makes sure the querystring pk matches the payload pk -- Makes sure the querystring pk matches the payload pk
@@ -140,29 +146,31 @@ failPut RSStandard{rsQueryTotal=queryTotal} =
lift SQL.condemn lift SQL.condemn
throwError Error.PutMatchingPkError throwError Error.PutMatchingPkError
deleteQuery :: MutatePlan -> ReadPlanTree -> ApiRequest -> AppConfig -> DbHandler ResultSet deleteQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
deleteQuery mutateReq readReq apiReq@ApiRequest{..} conf = do deleteQuery mrPlan apiReq@ApiRequest{..} conf = do
resultSet <- writeQuery mutateReq readReq False mempty apiReq conf resultSet <- writeQuery mrPlan apiReq conf
failNotSingular iAcceptMediaType resultSet failNotSingular iAcceptMediaType resultSet
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
optionalRollback conf apiReq
pure resultSet pure resultSet
invokeQuery :: ProcDescription -> CallPlan -> ReadPlanTree -> ApiRequest -> AppConfig -> DbHandler ResultSet invokeQuery :: ProcDescription -> CallReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
invokeQuery proc callReq readReq ApiRequest{..} AppConfig{..} = do invokeQuery proc CallReadPlan{crReadPlan, crCallPlan} apiReq@ApiRequest{..} conf@AppConfig{..} = do
resultSet <- resultSet <-
lift . SQL.statement mempty $ lift . SQL.statement mempty $
Statements.prepareCall Statements.prepareCall
(Proc.procReturnsScalar proc) (Proc.procReturnsScalar proc)
(Proc.procReturnsSingle proc) (Proc.procReturnsSingle proc)
(QueryBuilder.callPlanToQuery callReq) (QueryBuilder.callPlanToQuery crCallPlan)
(QueryBuilder.readPlanToQuery readReq) (QueryBuilder.readPlanToQuery crReadPlan)
(QueryBuilder.readPlanToCountQuery readReq) (QueryBuilder.readPlanToCountQuery crReadPlan)
(shouldCount iPreferCount) (shouldCount iPreferCount)
iAcceptMediaType iAcceptMediaType
(iPreferParameters == Just MultipleObjects) (iPreferParameters == Just MultipleObjects)
iBinaryField iBinaryField
configDbPreparedStatements configDbPreparedStatements
optionalRollback conf apiReq
failNotSingular iAcceptMediaType resultSet failNotSingular iAcceptMediaType resultSet
pure resultSet pure resultSet
@@ -200,12 +208,15 @@ txMode ApiRequest{..} =
_ -> _ ->
SQL.Write SQL.Write
writeQuery :: MutatePlan -> ReadPlanTree -> Bool -> [Text] -> ApiRequest -> AppConfig -> DbHandler ResultSet writeQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
writeQuery mutateReq readReq isInsert pkCols apiReq conf = do writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan} apiReq conf =
let
(isInsert, pkCols) = case mrMutatePlan of {Insert{insPkCols} -> (True, insPkCols); _ -> (False, mempty);}
in
lift . SQL.statement mempty $ lift . SQL.statement mempty $
Statements.prepareWrite Statements.prepareWrite
(QueryBuilder.readPlanToQuery readReq) (QueryBuilder.readPlanToQuery mrReadPlan)
(QueryBuilder.mutatePlanToQuery mutateReq) (QueryBuilder.mutatePlanToQuery mrMutatePlan)
isInsert isInsert
(iAcceptMediaType apiReq) (iAcceptMediaType apiReq)
(iPreferRepresentation apiReq) (iPreferRepresentation apiReq)
@@ -230,6 +241,18 @@ failsChangesOffLimits (Just maxChanges) RSStandard{rsQueryTotal=queryTotal} =
lift SQL.condemn lift SQL.condemn
throwError $ Error.OffLimitsChangesError queryTotal maxChanges throwError $ Error.OffLimitsChangesError queryTotal maxChanges
-- | Set a transaction to roll back if requested
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
optionalRollback AppConfig{..} ApiRequest{..} = do
lift $ when (shouldRollback || (configDbTxRollbackAll && not shouldCommit)) $ do
SQL.sql "SET CONSTRAINTS ALL IMMEDIATE"
SQL.condemn
where
shouldCommit =
configDbTxAllowOverride && iPreferTransaction == Just Commit
shouldRollback =
configDbTxAllowOverride && iPreferTransaction == Just Rollback
-- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function -- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function
setPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> Text -> setPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> Text ->
ApiRequest -> ByteString -> PgVersion -> DbHandler () ApiRequest -> ByteString -> PgVersion -> DbHandler ()
+1 -1
View File
@@ -84,7 +84,7 @@ getSelectsJoins rr@(Node ReadPlan{nodeName=name, nodeRel=Just rel, nodeAlias=ali
(sel:selects, joi:joins) (sel:selects, joi:joins)
mutatePlanToQuery :: MutatePlan -> SQL.Snippet mutatePlanToQuery :: MutatePlan -> SQL.Snippet
mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings) = mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _) =
"WITH " <> normalizedBody body <> " " <> "WITH " <> normalizedBody body <> " " <>
"INSERT INTO " <> SQL.sql (fromQi mainQi) <> SQL.sql (if S.null iCols then " " else "(" <> cols <> ") ") <> "INSERT INTO " <> SQL.sql (fromQi mainQi) <> SQL.sql (if S.null iCols then " " else "(" <> cols <> ") ") <>
"SELECT " <> SQL.sql cols <> " " <> "SELECT " <> SQL.sql cols <> " " <>
+37 -10
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
module PostgREST.Response module PostgREST.Response
( createResponse ( createResponse
@@ -10,6 +11,7 @@ module PostgREST.Response
, updateResponse , updateResponse
, addRetryHint , addRetryHint
, isServiceUnavailable , isServiceUnavailable
, optionalRollback
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
@@ -30,8 +32,7 @@ import qualified PostgREST.Response.OpenAPI as OpenAPI
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.DbStructure (DbStructure (..)) import PostgREST.DbStructure (DbStructure (..))
import PostgREST.DbStructure.Identifiers (FieldName, import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..),
QualifiedIdentifier (..),
Schema) Schema)
import PostgREST.DbStructure.Proc (ProcDescription (..), import PostgREST.DbStructure.Proc (ProcDescription (..),
ProcVolatility (..), ProcVolatility (..),
@@ -41,11 +42,14 @@ import PostgREST.GucHeader (GucHeader,
addHeadersIfNotIncluded, addHeadersIfNotIncluded,
unwrapGucHeader) unwrapGucHeader)
import PostgREST.MediaType (MediaType (..)) import PostgREST.MediaType (MediaType (..))
import PostgREST.Plan (MutateReadPlan (..))
import PostgREST.Plan.MutatePlan (MutatePlan (..))
import PostgREST.Query.Statements (ResultSet (..)) import PostgREST.Query.Statements (ResultSet (..))
import PostgREST.Request.ApiRequest (ApiRequest (..), import PostgREST.Request.ApiRequest (ApiRequest (..),
InvokeMethod (..), InvokeMethod (..),
Target (..)) Target (..))
import PostgREST.Request.Preferences (PreferRepresentation (..), import PostgREST.Request.Preferences (PreferRepresentation (..),
PreferTransaction (..),
shouldCount, shouldCount,
toAppliedHeader) toAppliedHeader)
import PostgREST.Request.QueryParams (QueryParams (..)) import PostgREST.Request.QueryParams (QueryParams (..))
@@ -81,10 +85,11 @@ readResponse headersOnly identifier ctxApiRequest@ApiRequest{..} resultSet = cas
RSPlan plan -> RSPlan plan ->
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
createResponse :: QualifiedIdentifier -> [FieldName] -> ApiRequest -> ResultSet -> Wai.Response createResponse :: QualifiedIdentifier -> MutateReadPlan -> ApiRequest -> ResultSet -> Wai.Response
createResponse QualifiedIdentifier{..} pkCols ctxApiRequest@ApiRequest{..} resultSet = case resultSet of createResponse QualifiedIdentifier{..} MutateReadPlan{mrMutatePlan} ctxApiRequest@ApiRequest{..} resultSet = case resultSet of
RSStandard{..} -> do RSStandard{..} -> do
let let
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
response = gucResponse rsGucStatus rsGucHeaders response = gucResponse rsGucStatus rsGucHeaders
headers = headers =
catMaybes catMaybes
@@ -127,11 +132,12 @@ updateResponse ctxApiRequest@ApiRequest{..} resultSet = case resultSet of
contentRangeHeader = contentRangeHeader =
RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $ RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
if shouldCount iPreferCount then Just rsQueryTotal else Nothing if shouldCount iPreferCount then Just rsQueryTotal else Nothing
headers = [contentRangeHeader]
if fullRepr then if fullRepr then
response status (contentTypeHeaders ctxApiRequest ++ [contentRangeHeader]) (LBS.fromStrict rsBody) response status (headers ++ contentTypeHeaders ctxApiRequest) (LBS.fromStrict rsBody)
else else
response status [contentRangeHeader] mempty response status headers mempty
RSPlan plan -> RSPlan plan ->
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
@@ -158,13 +164,14 @@ deleteResponse ctxApiRequest@ApiRequest{..} resultSet = case resultSet of
contentRangeHeader = contentRangeHeader =
RangeQuery.contentRangeH 1 0 $ RangeQuery.contentRangeH 1 0 $
if shouldCount iPreferCount then Just rsQueryTotal else Nothing if shouldCount iPreferCount then Just rsQueryTotal else Nothing
headers = [contentRangeHeader]
if iPreferRepresentation == Full then if iPreferRepresentation == Full then
response HTTP.status200 response HTTP.status200
(contentTypeHeaders ctxApiRequest ++ [contentRangeHeader]) (headers ++ contentTypeHeaders ctxApiRequest)
(LBS.fromStrict rsBody) (LBS.fromStrict rsBody)
else else
response HTTP.status204 [contentRangeHeader] mempty response HTTP.status204 headers mempty
RSPlan plan -> RSPlan plan ->
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
@@ -203,12 +210,13 @@ invokeResponse invMethod proc ctxApiRequest@ApiRequest{..} resultSet = case resu
then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange
$ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal) $ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
else LBS.fromStrict rsBody else LBS.fromStrict rsBody
headers = [contentRange]
if Proc.procReturnsVoid proc then if Proc.procReturnsVoid proc then
response HTTP.status204 [contentRange] mempty response HTTP.status204 headers mempty
else else
response status response status
(contentTypeHeaders ctxApiRequest ++ [contentRange]) (headers ++ contentTypeHeaders ctxApiRequest)
(if invMethod == InvHead then mempty else rsOrErrBody) (if invMethod == InvHead then mempty else rsOrErrBody)
RSPlan plan -> RSPlan plan ->
@@ -260,3 +268,22 @@ addRetryHint delay response = do
isServiceUnavailable :: Wai.Response -> Bool isServiceUnavailable :: Wai.Response -> Bool
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503 isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
optionalRollback :: AppConfig -> ApiRequest -> ExceptT Error.Error IO Wai.Response -> ExceptT Error.Error IO Wai.Response
optionalRollback AppConfig{..} ApiRequest{..} resp = do
newRes <- catchError resp $ return . Error.errorResponseFor
return $ Wai.mapResponseHeaders preferenceApplied newRes
where
shouldCommit =
configDbTxAllowOverride && iPreferTransaction == Just Commit
shouldRollback =
configDbTxAllowOverride && iPreferTransaction == Just Rollback
preferenceApplied
| shouldCommit =
addHeadersIfNotIncluded
[toAppliedHeader Commit]
| shouldRollback =
addHeadersIfNotIncluded
[toAppliedHeader Rollback]
| otherwise =
identity
+10
View File
@@ -808,6 +808,16 @@ def test_no_pool_connection_required_on_bad_jwt_claim(defaultenv):
assert response.status_code == 401 assert response.status_code == 401
def test_no_pool_connection_required_on_bad_embedding(defaultenv):
"no pool connection should be consumed for failing to embed"
with run(env=defaultenv, no_pool_connection_available=True) as postgrest:
# OPTIONS on a table shouldn't require opening a connection
response = postgrest.session.get("/projects?select=*,unexistent(*)")
assert response.status_code == 400
# TODO: This test fails now because of https://github.com/PostgREST/postgrest/pull/2122 # TODO: This test fails now because of https://github.com/PostgREST/postgrest/pull/2122
# The stack size of 1K(-with-rtsopts=-K1K) is not enough and this fails with "stack overflow" # The stack size of 1K(-with-rtsopts=-K1K) is not enough and this fails with "stack overflow"
# A stack size of 200K seems to be enough for succeess # A stack size of 200K seems to be enough for succeess