feat: allow spreading one-to-many and many-to-many embedded resources

* Note: Aggregates are not implemented
This commit is contained in:
Laurence Isla
2025-03-24 14:45:56 +00:00
committed by GitHub
parent 09ba7c0d28
commit 0b618d0bef
14 changed files with 1001 additions and 253 deletions
+18 -9
View File
@@ -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) =
+16 -6
View File
@@ -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 =