feat: allow spreading one-to-many and many-to-many embedded resources
* Note: Aggregates are not implemented
This commit is contained in:
+8
-10
@@ -75,10 +75,10 @@ data ApiRequestError
|
||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
||||
| NotEmbedded Text
|
||||
| NotImplemented Text
|
||||
| PutLimitNotAllowedError
|
||||
| QueryParamError QPError
|
||||
| RelatedOrderNotToOne Text Text
|
||||
| SpreadNotToOne Text Text
|
||||
| UnacceptableFilter Text
|
||||
| UnacceptableSchema [Text]
|
||||
| UnsupportedMethod ByteString
|
||||
@@ -123,10 +123,10 @@ instance PgrstError ApiRequestError where
|
||||
status NoRelBetween{} = HTTP.status400
|
||||
status NoRpc{} = HTTP.status404
|
||||
status NotEmbedded{} = HTTP.status400
|
||||
status NotImplemented{} = HTTP.status400
|
||||
status PutLimitNotAllowedError = HTTP.status400
|
||||
status QueryParamError{} = HTTP.status400
|
||||
status RelatedOrderNotToOne{} = HTTP.status400
|
||||
status SpreadNotToOne{} = HTTP.status400
|
||||
status UnacceptableFilter{} = HTTP.status400
|
||||
status UnacceptableSchema{} = HTTP.status406
|
||||
status UnsupportedMethod{} = HTTP.status405
|
||||
@@ -212,12 +212,6 @@ instance JSON.ToJSON ApiRequestError where
|
||||
(Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship")
|
||||
Nothing
|
||||
|
||||
toJSON (SpreadNotToOne origin target) = toJsonPgrstError
|
||||
ApiRequestErrorCode19
|
||||
("A spread operation on '" <> target <> "' is not possible")
|
||||
(Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship")
|
||||
Nothing
|
||||
|
||||
toJSON (UnacceptableFilter target) = toJsonPgrstError
|
||||
ApiRequestErrorCode20
|
||||
("Bad operator on the '" <> target <> "' embedded resource")
|
||||
@@ -307,6 +301,9 @@ instance JSON.ToJSON ApiRequestError where
|
||||
Nothing
|
||||
(JSON.String <$> tableNotFoundHint schemaName relName tbls)
|
||||
|
||||
toJSON (NotImplemented details) = toJsonPgrstError
|
||||
ApiRequestErrorCode27 "Feature not implemented" (Just $ JSON.String details) Nothing
|
||||
|
||||
-- |
|
||||
-- If no relationship is found then:
|
||||
--
|
||||
@@ -712,7 +709,7 @@ data ErrorCode
|
||||
| ApiRequestErrorCode16
|
||||
| ApiRequestErrorCode17
|
||||
| ApiRequestErrorCode18
|
||||
| ApiRequestErrorCode19
|
||||
-- | ApiRequestErrorCode19 -- no longer used (used to be mapped to SpreadNotToOne)
|
||||
| ApiRequestErrorCode20
|
||||
| ApiRequestErrorCode21
|
||||
| ApiRequestErrorCode22
|
||||
@@ -720,6 +717,7 @@ data ErrorCode
|
||||
| ApiRequestErrorCode24
|
||||
| ApiRequestErrorCode25
|
||||
| ApiRequestErrorCode26
|
||||
| ApiRequestErrorCode27
|
||||
-- Schema Cache errors
|
||||
| SchemaCacheErrorCode00
|
||||
| SchemaCacheErrorCode01
|
||||
@@ -763,7 +761,6 @@ buildErrorCode code = case code of
|
||||
ApiRequestErrorCode16 -> "PGRST116"
|
||||
ApiRequestErrorCode17 -> "PGRST117"
|
||||
ApiRequestErrorCode18 -> "PGRST118"
|
||||
ApiRequestErrorCode19 -> "PGRST119"
|
||||
ApiRequestErrorCode20 -> "PGRST120"
|
||||
ApiRequestErrorCode21 -> "PGRST121"
|
||||
ApiRequestErrorCode22 -> "PGRST122"
|
||||
@@ -771,6 +768,7 @@ buildErrorCode code = case code of
|
||||
ApiRequestErrorCode24 -> "PGRST124"
|
||||
ApiRequestErrorCode25 -> "PGRST125"
|
||||
ApiRequestErrorCode26 -> "PGRST126"
|
||||
ApiRequestErrorCode27 -> "PGRST127"
|
||||
|
||||
SchemaCacheErrorCode00 -> "PGRST200"
|
||||
SchemaCacheErrorCode01 -> "PGRST201"
|
||||
|
||||
+75
-44
@@ -29,6 +29,7 @@ import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.HashMap.Strict.InsOrd as HMI
|
||||
import qualified Data.List as L
|
||||
import qualified Data.Set as S
|
||||
import qualified Data.Text as T
|
||||
import qualified PostgREST.SchemaCache.Routine as Routine
|
||||
|
||||
import Data.Either.Combinators (mapLeft, mapRight)
|
||||
@@ -338,11 +339,11 @@ readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregate
|
||||
in
|
||||
mapLeft ApiRequestError $
|
||||
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
|
||||
addToManyOrderSelects =<<
|
||||
hoistSpreadAggFunctions =<<
|
||||
validateAggFunctions configDbAggregates =<<
|
||||
addRelSelects =<<
|
||||
addNullEmbedFilters =<<
|
||||
validateSpreadEmbeds =<<
|
||||
addRelatedOrders =<<
|
||||
addAliases =<<
|
||||
expandStars ctx =<<
|
||||
@@ -358,7 +359,7 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
|
||||
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} []
|
||||
where
|
||||
rootDepth = 0
|
||||
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing False [] rootDepth
|
||||
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing Nothing [] rootDepth
|
||||
treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree
|
||||
treeEntry depth (Node si fldForest) (Node q rForest) =
|
||||
let nxtDepth = succ depth in
|
||||
@@ -371,33 +372,41 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
|
||||
SpreadRelation{..} ->
|
||||
Node q $
|
||||
foldr (treeEntry nxtDepth)
|
||||
(Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth, relIsSpread=True} [])
|
||||
(Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth, relSpread=Just ToOneSpread} [])
|
||||
fldForest:rForest
|
||||
SelectField{..} ->
|
||||
Node q{select=CoercibleSelectField (resolveOutputField ctx{qi=from q} selField) selAggregateFunction selAggregateCast selCast selAlias:select q} rForest
|
||||
|
||||
-- If an alias is explicitly specified, it is always respected. However, an alias may be
|
||||
-- determined automatically in the case of a select term with a JSON path, or in the case
|
||||
-- of domain representations.
|
||||
-- determined automatically in these cases:
|
||||
-- * A select term with a JSON path
|
||||
-- * Domain representations
|
||||
-- * Aggregates in spread relationships
|
||||
addAliases :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addAliases = Right . fmap addAliasToPlan
|
||||
where
|
||||
addAliasToPlan rp@ReadPlan{select=sel} = rp{select=map aliasSelectField sel}
|
||||
addAliasToPlan rp@ReadPlan{select=sel, relSpread=spr} = rp{select=map (aliasSelectField $ isJust spr) sel}
|
||||
|
||||
aliasSelectField :: CoercibleSelectField -> CoercibleSelectField
|
||||
aliasSelectField field@CoercibleSelectField{csField=fieldDetails, csAggFunction=aggFun, csAlias=alias}
|
||||
aliasSelectField :: Bool -> CoercibleSelectField -> CoercibleSelectField
|
||||
aliasSelectField isSpread field@CoercibleSelectField{csField=fieldDetails, csAggFunction=aggFun, csAlias=alias}
|
||||
| isJust alias = field
|
||||
| isJust aggFun = fieldAliasForSpreadAgg field
|
||||
| isJust aggFun = fieldAliasForSpreadAgg isSpread field
|
||||
| isJsonKeyPath fieldDetails, Just key <- lastJsonKey fieldDetails = field { csAlias = Just key }
|
||||
| isTransformPath fieldDetails = field { csAlias = Just (cfName fieldDetails) }
|
||||
| otherwise = field
|
||||
|
||||
-- A request like: `/top_table?select=...middle_table(...nested_table(count()))` will `SELECT` the full row instead of `*`,
|
||||
-- because doing a `COUNT(*)` in `top_table` would not return the desired results.
|
||||
-- So we use the "count" alias if none is present since the field name won't be selected.
|
||||
fieldAliasForSpreadAgg field
|
||||
| cfFullRow (csField field) = field { csAlias = Just "count" }
|
||||
| otherwise = field
|
||||
-- Spread relationships with non-aliased aggregates can cause problems when selecting the fields in the top level resource.
|
||||
-- The top level won't know the name of the field in this case:
|
||||
-- A nested to-one spread like `/top_table?select=...middle_table(...nested_table(count()))`
|
||||
-- will do a `SELECT nested_table` instead of `SELECT *`, because doing a `COUNT(*)` in `top_table`
|
||||
-- would not return the desired results.
|
||||
--
|
||||
-- That's why we need to use the aggregate name as an alias (e.g. COUNT(...) AS "count").
|
||||
-- Since PostgreSQL labels the columns with the aggregate name, it shouldn't be a problem to
|
||||
-- apply the aliases to all the aggregates regardless if the previous conditions are met.
|
||||
fieldAliasForSpreadAgg True field@CoercibleSelectField{csAggFunction=Just agg} =
|
||||
field { csAlias = Just (T.toLower $ show agg) }
|
||||
fieldAliasForSpreadAgg _ field = field
|
||||
|
||||
isJsonKeyPath CoercibleField{cfJsonPath=(_: _)} = True
|
||||
isJsonKeyPath _ = False
|
||||
@@ -423,13 +432,14 @@ knownColumnsInContext ResolverContext{..} =
|
||||
-- | Expand "select *" into explicit field names of the table in the following situations:
|
||||
-- * When there are data representations present.
|
||||
-- * When there is an aggregate function in a given ReadPlan or its parent.
|
||||
-- * When the ReadPlan is a to-many spread relationship
|
||||
expandStars :: ResolverContext -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
|
||||
where
|
||||
expandStarsForReadPlan :: Bool -> ReadPlanTree -> ReadPlanTree
|
||||
expandStarsForReadPlan hasAgg (Node rp@ReadPlan{select, from=fromQI, fromAlias=alias} children) =
|
||||
expandStarsForReadPlan hasAgg (Node rp@ReadPlan{select, from=fromQI, fromAlias=alias, relSpread=spread} children) =
|
||||
let
|
||||
newHasAgg = hasAgg || any (isJust . csAggFunction) select
|
||||
newHasAgg = hasAgg || any (isJust . csAggFunction) select || case spread of Just ToManySpread{} -> True; _ -> False
|
||||
newCtx = adjustContext ctx fromQI alias
|
||||
newRPlan = expandStarsForTable newCtx newHasAgg rp
|
||||
in Node newRPlan (map (expandStarsForReadPlan newHasAgg) children)
|
||||
@@ -442,18 +452,18 @@ expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
|
||||
adjustContext context fromQI _ = context{qi=fromQI}
|
||||
|
||||
expandStarsForTable :: ResolverContext -> Bool -> ReadPlan -> ReadPlan
|
||||
expandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@ReadPlan{select=selectFields, relIsSpread=isSpread}
|
||||
expandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@ReadPlan{select=selectFields, relSpread=spread}
|
||||
-- We expand if either of the below are true:
|
||||
-- * We have a '*' select AND there is an aggregate function in this ReadPlan's sub-tree.
|
||||
-- * We have a '*' select AND the target table has at least one data representation.
|
||||
-- We ignore '*' selects that have an aggregate function attached, unless it's a `COUNT(*)` for a Spread Embed,
|
||||
-- we tag it as "full row" in that case.
|
||||
| hasStarSelect && (hasAgg || hasDataRepresentation) = rp{select = concatMap (expandStarSelectField isSpread knownColumns) selectFields}
|
||||
| hasStarSelect && (hasAgg || hasDataRepresentation) = rp{select = concatMap (expandStarSelectField (isJust spread) knownColumns) selectFields}
|
||||
| otherwise = rp
|
||||
where
|
||||
hasStarSelect = "*" `elem` map (cfName . csField) filteredSelectFields
|
||||
filteredSelectFields = filter (shouldExpandOrTag . csAggFunction) selectFields
|
||||
shouldExpandOrTag aggFunc = isNothing aggFunc || (isSpread && aggFunc == Just Count)
|
||||
shouldExpandOrTag aggFunc = isNothing aggFunc || (isJust spread && aggFunc == Just Count)
|
||||
hasDataRepresentation = any hasOutputRep knownColumns
|
||||
knownColumns = knownColumnsInContext ctx
|
||||
|
||||
@@ -478,20 +488,21 @@ treeRestrictRange maxRows _ request = pure $ nodeRestrictRange maxRows <$> reque
|
||||
-- add relationships to the nodes of the tree by traversing the forest while keeping track of the parentNode(https://stackoverflow.com/questions/22721064/get-the-parent-of-a-node-in-data-tree-haskell#comment34627048_22721064)
|
||||
-- also adds aliasing
|
||||
addRels :: Schema -> Action -> RelationshipsMap -> Maybe ReadPlanTree -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,relAlias,depth} forest) =
|
||||
addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,relAlias,relSpread,depth} forest) =
|
||||
case parentNode of
|
||||
Just (Node ReadPlan{from=parentNodeQi, fromAlias=parentAlias} _) ->
|
||||
let
|
||||
newReadPlan = (\r ->
|
||||
let newAlias = Just (qiName (relForeignTable r) <> "_" <> show depth)
|
||||
aggAlias = qiName (relTable r) <> "_" <> fromMaybe relName relAlias <> "_" <> show depth in
|
||||
aggAlias = qiName (relTable r) <> "_" <> fromMaybe relName relAlias <> "_" <> show depth
|
||||
updSpread = if isJust relSpread && not (relIsToOne r) then Just $ ToManySpread [] [] else relSpread in
|
||||
case r of
|
||||
Relationship{relCardinality=M2M _} -> -- m2m does internal implicit joins that don't need aliasing
|
||||
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, relJoinConds=getJoinConditions Nothing parentAlias r}
|
||||
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, relJoinConds=getJoinConditions Nothing parentAlias r, relSpread=updSpread}
|
||||
ComputedRelationship{} ->
|
||||
rPlan{from=relForeignTable r, relToParent=Just r{relTableAlias=maybe (relTable r) (QualifiedIdentifier mempty) parentAlias}, relAggAlias=aggAlias, fromAlias=newAlias}
|
||||
rPlan{from=relForeignTable r, relToParent=Just r{relTableAlias=maybe (relTable r) (QualifiedIdentifier mempty) parentAlias}, relAggAlias=aggAlias, fromAlias=newAlias, relSpread=updSpread}
|
||||
_ ->
|
||||
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, fromAlias=newAlias, relJoinConds=getJoinConditions newAlias parentAlias r}
|
||||
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, fromAlias=newAlias, relJoinConds=getJoinConditions newAlias parentAlias r, relSpread=updSpread}
|
||||
) <$> rel
|
||||
origin = if depth == 1 -- Only on depth 1 we check if the root(depth 0) has an alias so the sourceCTEName alias can be found as a relationship
|
||||
then fromMaybe (qiName parentNodeQi) parentAlias
|
||||
@@ -632,9 +643,9 @@ addRelSelects node@(Node rp forest)
|
||||
in Right $ Node rp { relSelect = newRelSelects } newForest
|
||||
|
||||
generateRelSelectField :: ReadPlanTree -> Maybe RelSelectField
|
||||
generateRelSelectField (Node rp@ReadPlan{relToParent=Just _, relAggAlias, relIsSpread = True} _) =
|
||||
generateRelSelectField (Node rp@ReadPlan{relToParent=Just _, relAggAlias, relSpread = Just _} _) =
|
||||
Just $ Spread { rsSpreadSel = generateSpreadSelectFields rp, rsAggAlias = relAggAlias }
|
||||
generateRelSelectField (Node ReadPlan{relToParent=Just rel, select, relName, relAlias, relAggAlias, relIsSpread = False} forest) =
|
||||
generateRelSelectField (Node ReadPlan{relToParent=Just rel, select, relName, relAlias, relAggAlias, relSpread = Nothing} forest) =
|
||||
Just $ JsonEmbed { rsEmbedMode, rsSelName, rsAggAlias = relAggAlias, rsEmptyEmbed }
|
||||
where
|
||||
rsSelName = fromMaybe relName relAlias
|
||||
@@ -663,7 +674,7 @@ generateSpreadSelectFields ReadPlan{select, relSelect} =
|
||||
relSelectToSpread (Spread{rsSpreadSel}) =
|
||||
rsSpreadSel
|
||||
|
||||
-- When aggregates are present in a ReadPlan that will be spread, we "hoist"
|
||||
-- When aggregates are present in a ReadPlan with a to-one spread, we "hoist"
|
||||
-- to the highest level possible so that their semantics make sense. For instance,
|
||||
-- imagine the user performs the following request:
|
||||
-- `GET /projects?select=client_id,...project_invoices(invoice_total.sum())`
|
||||
@@ -684,28 +695,31 @@ generateSpreadSelectFields ReadPlan{select, relSelect} =
|
||||
--
|
||||
-- The second tuple contains the aggregate function to be applied, the cast, and
|
||||
-- the alias, if it was supplied by the user or otherwise determined.
|
||||
--
|
||||
-- No hoisting is done for to-many spreads
|
||||
type HoistedAgg = ((Alias, FieldName), (AggregateFunction, Maybe Cast, Maybe Alias))
|
||||
|
||||
hoistSpreadAggFunctions :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
hoistSpreadAggFunctions tree = Right $ fst $ applySpreadAggHoistingToNode tree
|
||||
|
||||
applySpreadAggHoistingToNode :: ReadPlanTree -> (ReadPlanTree, [HoistedAgg])
|
||||
applySpreadAggHoistingToNode (Node rp@ReadPlan{relAggAlias, relToParent, relIsSpread} children) =
|
||||
applySpreadAggHoistingToNode (Node rp@ReadPlan{relAggAlias, relToParent, relSpread} children) =
|
||||
let (newChildren, childAggLists) = unzip $ map applySpreadAggHoistingToNode children
|
||||
allChildAggLists = concat childAggLists
|
||||
(newSelects, aggList) = if depth rp == 0 || (isJust relToParent && not relIsSpread)
|
||||
isToOneSpread = relSpread == Just ToOneSpread
|
||||
(newSelects, aggList) = if depth rp == 0 || (isJust relToParent && not isToOneSpread)
|
||||
then (select rp, [])
|
||||
else hoistFromSelectFields relAggAlias (select rp)
|
||||
|
||||
-- If the current `ReadPlan` is a spread rel and it has aggregates hoisted from
|
||||
-- If the current `ReadPlan` is a to-one spread rel and it has aggregates hoisted from
|
||||
-- child relationships, then it must hoist those aggregates to its parent rel.
|
||||
-- So we update them with the current `relAggAlias`.
|
||||
hoistAgg ((_, fieldName), hoistFunc) = ((relAggAlias, fieldName), hoistFunc)
|
||||
hoistedAggList = if relIsSpread
|
||||
hoistedAggList = if isToOneSpread
|
||||
then aggList ++ map hoistAgg allChildAggLists
|
||||
else aggList
|
||||
|
||||
newRelSelects = if null children || relIsSpread
|
||||
newRelSelects = if null children || isToOneSpread
|
||||
then relSelect rp
|
||||
else map (hoistIntoRelSelectFields allChildAggLists) $ relSelect rp
|
||||
in (Node rp { select = newSelects, relSelect = newRelSelects } newChildren, hoistedAggList)
|
||||
@@ -743,6 +757,32 @@ hoistIntoRelSelectFields aggList r@(Spread {rsSpreadSel = spreadSelects, rsAggAl
|
||||
Nothing -> s
|
||||
hoistIntoRelSelectFields _ r = r
|
||||
|
||||
-- | Handle ordering in a To-Many Spread Relationship
|
||||
-- * It removes the ordering done in the ReadPlan and moves it to the SpreadType.
|
||||
-- We also select the ordering columns and alias them to avoid collisions. This is because it would be impossible
|
||||
-- to order once it's aggregated if it's not selected in the inner query beforehand.
|
||||
addToManyOrderSelects :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addToManyOrderSelects (Node rp@ReadPlan{order, select, relAggAlias, relSelect, relSpread = Just ToManySpread {}} forest)
|
||||
| anyAggSel || anyAggRelSel = Left $ NotImplemented "Aggregates are not implemented for one-to-many or many-to-many spreads."
|
||||
| otherwise = Node rp { order = [], relSpread = newRelSpread } <$> addToManyOrderSelects `traverse` forest
|
||||
where
|
||||
newRelSpread = Just ToManySpread { stExtraSelect = addSprExtraSelects, stOrder = addSprOrder}
|
||||
anyAggSel = any (isJust . csAggFunction) select
|
||||
anyAggRelSel = any (\case Spread sels _ -> any (isJust . ssSelAggFunction) sels; _ -> False) relSelect
|
||||
(addSprExtraSelects, addSprOrder) = unzip $ zipWith ordToExtraSelsAndSprOrds [1..] order
|
||||
ordToExtraSelsAndSprOrds i = \case
|
||||
CoercibleOrderTerm fld dir ordr -> (
|
||||
(Nothing, CoercibleSelectField fld Nothing Nothing Nothing (Just $ selOrdAlias (cfName fld) i)),
|
||||
CoercibleOrderTerm (unknownField (selOrdAlias (cfName fld) i) []) dir ordr
|
||||
)
|
||||
CoercibleOrderRelationTerm rel (fld,jp) dir ordr -> (
|
||||
(Just rel, CoercibleSelectField (unknownField fld jp) Nothing Nothing Nothing (Just $ selOrdAlias fld i)),
|
||||
CoercibleOrderTerm (unknownField (selOrdAlias fld i) []) dir ordr
|
||||
)
|
||||
selOrdAlias :: Alias -> Integer -> Alias
|
||||
selOrdAlias name i = relAggAlias <> "_" <> name <> "_" <> show i -- add index to avoid collisions in aliases
|
||||
addToManyOrderSelects (Node rp forest) = Node rp <$> addToManyOrderSelects `traverse` forest
|
||||
|
||||
validateAggFunctions :: Bool -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
|
||||
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left AggregatesNotAllowed
|
||||
@@ -825,7 +865,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
-- relName = "projects",
|
||||
-- relToParent = Nothing,
|
||||
-- relJoinConds = [],
|
||||
-- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relIsSpread = False, depth = 1,
|
||||
-- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relSpread = Nothing, depth = 1,
|
||||
-- relSelect = []
|
||||
-- },
|
||||
-- subForest = []
|
||||
@@ -851,7 +891,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
-- )
|
||||
-- ],
|
||||
-- order = [], range_ = fullRange, relName = "clients", relToParent = Nothing, relJoinConds = [], relAlias = Nothing, relAggAlias = "", relHint = Nothing,
|
||||
-- relJoinType = Nothing, relIsSpread = False, depth = 0,
|
||||
-- relJoinType = Nothing, relSpread = Nothing, depth = 0,
|
||||
-- relSelect = []
|
||||
-- },
|
||||
-- subForest = subForst
|
||||
@@ -916,15 +956,6 @@ resolveLogicTree ctx (Expr b op lts) = CoercibleExpr b op (map (resolveLogicTree
|
||||
resolveFilter :: ResolverContext -> Filter -> CoercibleFilter
|
||||
resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld opExpr, opExpr=opExpr}
|
||||
|
||||
-- Validates that spread embeds are only done on to-one relationships
|
||||
validateSpreadEmbeds :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
validateSpreadEmbeds (Node rp@ReadPlan{relToParent=Nothing} forest) = Node rp <$> validateSpreadEmbeds `traverse` forest
|
||||
validateSpreadEmbeds (Node rp@ReadPlan{relIsSpread,relToParent=Just rel,relName} forest) = do
|
||||
validRP <- if relIsSpread && not (relIsToOne rel)
|
||||
then Left $ SpreadNotToOne (qiName $ relTable rel) relName -- TODO using relTable is not entirely right because ReadPlan might have an alias, need to store the parent alias on ReadPlan
|
||||
else Right rp
|
||||
Node validRP <$> validateSpreadEmbeds `traverse` forest
|
||||
|
||||
-- Find a Node of the Tree and apply a function to it
|
||||
updateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
updateNode f ([], a) rr = f a <$> rr
|
||||
|
||||
@@ -2,6 +2,7 @@ module PostgREST.Plan.ReadPlan
|
||||
( ReadPlanTree
|
||||
, ReadPlan(..)
|
||||
, JoinCondition(..)
|
||||
, SpreadType(..)
|
||||
) where
|
||||
|
||||
import Data.Tree (Tree (..))
|
||||
@@ -11,7 +12,8 @@ import PostgREST.ApiRequest.Types (Alias, Depth, Hint,
|
||||
import PostgREST.Plan.Types (CoercibleLogicTree,
|
||||
CoercibleOrderTerm,
|
||||
CoercibleSelectField (..),
|
||||
RelSelectField (..))
|
||||
RelSelectField (..),
|
||||
SpreadType (..))
|
||||
import PostgREST.RangeQuery (NonnegRange)
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier)
|
||||
@@ -42,7 +44,7 @@ data ReadPlan = ReadPlan
|
||||
, relAggAlias :: Alias
|
||||
, relHint :: Maybe Hint
|
||||
, relJoinType :: Maybe JoinType
|
||||
, relIsSpread :: Bool
|
||||
, relSpread :: Maybe SpreadType
|
||||
, relSelect :: [RelSelectField]
|
||||
, depth :: Depth
|
||||
-- ^ used for aliasing
|
||||
|
||||
@@ -10,6 +10,7 @@ module PostgREST.Plan.Types
|
||||
, RelSelectField(..)
|
||||
, RelJsonEmbedMode(..)
|
||||
, SpreadSelectField(..)
|
||||
, SpreadType(..)
|
||||
) where
|
||||
|
||||
import PostgREST.ApiRequest.Types (AggregateFunction, Alias, Cast,
|
||||
@@ -111,3 +112,11 @@ data SpreadSelectField =
|
||||
, ssSelAlias :: Maybe Alias
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SpreadType
|
||||
= ToOneSpread
|
||||
| ToManySpread
|
||||
{ stExtraSelect :: [(Maybe FieldName, CoercibleSelectField)]
|
||||
, stOrder :: [CoercibleOrderTerm]
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -44,9 +44,9 @@ import PostgREST.Query.SqlFragment
|
||||
import Protolude
|
||||
|
||||
readPlanToQuery :: ReadPlanTree -> SQL.Snippet
|
||||
readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds, relSelect} forest) =
|
||||
readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds, relSelect, relSpread} forest) =
|
||||
"SELECT " <>
|
||||
intercalateSnippet ", " ((pgFmtSelectItem qi <$> (if null select && null forest then defSelect else select)) ++ joinsSelects) <>
|
||||
intercalateSnippet ", " (selects ++ sprExtraSelects ++ joinsSelects) <>
|
||||
fromFrag <>
|
||||
intercalateSnippet " " joins <>
|
||||
(if null logicForest && null relJoinConds
|
||||
@@ -61,7 +61,11 @@ readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicFor
|
||||
-- gets all the columns in case of an empty select, ignoring/obtaining these columns is done at the aggregation stage
|
||||
defSelect = [CoercibleSelectField (unknownField "*" []) Nothing Nothing Nothing Nothing]
|
||||
joins = getJoins node
|
||||
selects = pgFmtSelectItem qi <$> (if null select && null forest then defSelect else select)
|
||||
joinsSelects = getJoinSelects node
|
||||
sprExtraSelects = case relSpread of
|
||||
Just (ToManySpread sels _) -> (\s -> pgFmtSelectItem (maybe qi (QualifiedIdentifier "") $ fst s) $ snd s) <$> sels
|
||||
_ -> mempty
|
||||
|
||||
getJoinSelects :: ReadPlanTree -> [SQL.Snippet]
|
||||
getJoinSelects (Node ReadPlan{relSelect} _) =
|
||||
@@ -91,23 +95,28 @@ getJoins (Node ReadPlan{relSelect} forest) =
|
||||
) relSelect
|
||||
|
||||
getJoin :: RelSelectField -> ReadPlanTree -> SQL.Snippet
|
||||
getJoin fld node@(Node ReadPlan{relJoinType} _) =
|
||||
getJoin fld node@(Node ReadPlan{relJoinType, relSpread} _) =
|
||||
let
|
||||
correlatedSubquery sub al cond =
|
||||
" " <> (if relJoinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> al <> " ON " <> cond
|
||||
subquery = readPlanToQuery node
|
||||
aggAlias = pgFmtIdent $ rsAggAlias fld
|
||||
selectSubqAgg = "SELECT json_agg(" <> aggAlias <> ")::jsonb AS " <> aggAlias
|
||||
fromSubqAgg = " FROM (" <> subquery <> " ) AS " <> aggAlias
|
||||
joinCondition = if relJoinType == Just JTInner then aggAlias <> " IS NOT NULL" else "TRUE"
|
||||
in
|
||||
case fld of
|
||||
JsonEmbed{rsEmbedMode = JsonObject} ->
|
||||
correlatedSubquery subquery aggAlias "TRUE"
|
||||
Spread{} ->
|
||||
correlatedSubquery subquery aggAlias "TRUE"
|
||||
Spread{rsSpreadSel, rsAggAlias} ->
|
||||
case relSpread of
|
||||
Just (ToManySpread _ sprOrder) ->
|
||||
let selSpread = selectSubqAgg <> (if null rsSpreadSel then mempty else ", ") <> intercalateSnippet ", " (pgFmtSpreadJoinSelectItem rsAggAlias sprOrder <$> rsSpreadSel)
|
||||
in correlatedSubquery (selSpread <> fromSubqAgg) aggAlias joinCondition
|
||||
_ ->
|
||||
correlatedSubquery subquery aggAlias "TRUE"
|
||||
JsonEmbed{rsEmbedMode = JsonArray} ->
|
||||
let
|
||||
subq = "SELECT json_agg(" <> aggAlias <> ")::jsonb AS " <> aggAlias <> " FROM (" <> subquery <> " ) AS " <> aggAlias
|
||||
condition = if relJoinType == Just JTInner then aggAlias <> " IS NOT NULL" else "TRUE"
|
||||
in correlatedSubquery subq aggAlias condition
|
||||
correlatedSubquery (selectSubqAgg <> fromSubqAgg) aggAlias joinCondition
|
||||
|
||||
mutatePlanToQuery :: MutatePlan -> SQL.Snippet
|
||||
mutatePlanToQuery (Insert mainQi iCols body onConflict putConditions returnings _ applyDefaults) =
|
||||
|
||||
@@ -22,6 +22,7 @@ module PostgREST.Query.SqlFragment
|
||||
, pgFmtOrderTerm
|
||||
, pgFmtSelectItem
|
||||
, pgFmtSpreadSelectItem
|
||||
, pgFmtSpreadJoinSelectItem
|
||||
, fromJsonBodyF
|
||||
, responseHeadersF
|
||||
, responseStatusF
|
||||
@@ -257,7 +258,7 @@ pgFmtField table cf = case cfToTsVector cf of
|
||||
_ -> fmtFld
|
||||
where
|
||||
fmtFld = case cf of
|
||||
CoercibleField{cfFullRow=True} -> fromQi table
|
||||
CoercibleField{cfFullRow=True} -> pgFmtIdent (qiName table)
|
||||
CoercibleField{cfName=fn, cfJsonPath=[]} -> pgFmtColumn table fn
|
||||
CoercibleField{cfName=fn, cfToJson=doToJson, cfJsonPath=jp} | doToJson -> "to_jsonb(" <> pgFmtColumn table fn <> ")" <> pgFmtJsonPath jp
|
||||
| otherwise -> pgFmtColumn table fn <> pgFmtJsonPath jp
|
||||
@@ -278,11 +279,7 @@ pgFmtSelectItem table CoercibleSelectField{csField=fld, csAggFunction=agg, csAgg
|
||||
|
||||
pgFmtSpreadSelectItem :: Alias -> SpreadSelectField -> SQL.Snippet
|
||||
pgFmtSpreadSelectItem aggAlias SpreadSelectField{ssSelName, ssSelAggFunction, ssSelAggCast, ssSelAlias} =
|
||||
pgFmtApplyAggregate ssSelAggFunction ssSelAggCast fullSelName <> pgFmtAs ssSelAlias
|
||||
where
|
||||
fullSelName = case ssSelName of
|
||||
"*" -> pgFmtIdent aggAlias <> ".*"
|
||||
_ -> pgFmtIdent aggAlias <> "." <> pgFmtIdent ssSelName
|
||||
pgFmtApplyAggregate ssSelAggFunction ssSelAggCast (pgFmtFullSelName aggAlias ssSelName) <> pgFmtAs ssSelAlias
|
||||
|
||||
pgFmtApplyAggregate :: Maybe AggregateFunction -> Maybe Cast -> SQL.Snippet -> SQL.Snippet
|
||||
pgFmtApplyAggregate Nothing _ snippet = snippet
|
||||
@@ -294,6 +291,14 @@ pgFmtApplyAggregate (Just agg) aggCast snippet =
|
||||
convertAggFunction = SQL.sql . BS.map toUpper . BS.pack . show
|
||||
aggregatedSnippet = convertAggFunction agg <> "(" <> snippet <> ")"
|
||||
|
||||
pgFmtSpreadJoinSelectItem :: Alias -> [CoercibleOrderTerm] -> SpreadSelectField -> SQL.Snippet
|
||||
pgFmtSpreadJoinSelectItem aggAlias order SpreadSelectField{ssSelName, ssSelAlias} =
|
||||
"COALESCE(json_agg(" <> fmtField <> " " <> fmtOrder <> "),'[]')::jsonb" <> " AS " <> fmtAlias
|
||||
where
|
||||
fmtField = pgFmtFullSelName aggAlias ssSelName
|
||||
fmtOrder = orderF (QualifiedIdentifier "" aggAlias) order
|
||||
fmtAlias = pgFmtIdent (fromMaybe ssSelName ssSelAlias)
|
||||
|
||||
pgFmtApplyCast :: Maybe Cast -> SQL.Snippet -> SQL.Snippet
|
||||
pgFmtApplyCast Nothing snippet = snippet
|
||||
-- Ideally we'd quote the cast with "pgFmtIdent cast". However, that would invalidate common casts such as "int", "bigint", etc.
|
||||
@@ -301,6 +306,11 @@ pgFmtApplyCast Nothing snippet = snippet
|
||||
-- Not quoting should be fine, we validate the input on Parsers.
|
||||
pgFmtApplyCast (Just cast) snippet = "CAST( " <> snippet <> " AS " <> SQL.sql (encodeUtf8 cast) <> " )"
|
||||
|
||||
pgFmtFullSelName :: Alias -> FieldName -> SQL.Snippet
|
||||
pgFmtFullSelName aggAlias fieldName = case fieldName of
|
||||
"*" -> pgFmtIdent aggAlias <> ".*"
|
||||
_ -> pgFmtIdent aggAlias <> "." <> pgFmtIdent fieldName
|
||||
|
||||
-- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
|
||||
fromJsonBodyF :: Maybe LBS.ByteString -> [CoercibleField] -> Bool -> Bool -> Bool -> SQL.Snippet
|
||||
fromJsonBodyF body fields includeSelect includeLimitOne includeDefaults =
|
||||
|
||||
Reference in New Issue
Block a user