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
+3
View File
@@ -17,6 +17,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2255, Apply `to_tsvector()` explicitly to the full-text search filtered column (excluding `tsvector` types) - @laurenceisla
- #1578, Log the main SQL query to stderr at the current `log-level` when `log-query=main-query` - @laurenceisla
- #3903, Log connection pool borrows on `log-level=debug` - @taimoorzaeem
- #3041, Allow spreading one-to-many and many-to-many embedded resources - @laurenceisla
+ The selected columns in the embedded resources are aggregated into arrays
+ Aggregates are not supported
### Fixed
+7 -3
View File
@@ -225,10 +225,10 @@ If we also want to get the total ``amount`` grouped by the ``order_date`` of the
Note that the aggregate is done within the embedded resource ``orders``.
It is not affected by any of the columns from the top-level relationship ``customers``.
Using Aggregates in Spreads
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Aggregates in To-One Spreads
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
All the aggregates inside a :ref:`spread embedded resource <spread_embed>` will be hoisted to the top-level relationship.
All the aggregates inside a :ref:`one-to-one or many-to-one spread embedded resource <spread_to_one_embed>` will be hoisted to the top-level relationship.
In other words, it will behave as if the aggregate was done in the top-level relationship itself. For example:
.. code-block:: bash
@@ -251,3 +251,7 @@ This will take the ``max`` and ``min`` subscription date of every customer and g
"min": "2016-02-11"
}
]
.. note::
Aggregates inside to-many spreads are not supported
+117 -3
View File
@@ -1150,7 +1150,20 @@ For example, to arrange the films in descending order using the director's last
Spread embedded resource
========================
On many-to-one and one-to-one relationships, you can "spread" the embedded resource. That is, remove the surrounding JSON object for the embedded resource columns.
The ``...`` operator lets you "spread" an embedded resource.
That is, it removes the surrounding JSON object for the embedded resource columns.
.. note::
The spread operator ``...`` is borrowed from the Javascript `spread syntax <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax>`_.
.. _spread_to_one_embed:
Spread To-One relationships
---------------------------
This applies to :ref:`one-to-one <one-to-one>` and :ref:`many-to-one <many-to-one>` relationships.
Take the following example:
.. code-block:: bash
@@ -1196,6 +1209,107 @@ You can use this to get the columns of a join table in a many-to-many relationsh
}
]
.. note::
.. _spread_to_many_embed:
The spread operator ``...`` is borrowed from the Javascript `spread syntax <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax>`_.
Spread To-Many relationships
----------------------------
The spread columns in :ref:`one-to-many <one-to-many>` or :ref:`many-to-many <many-to-many>` relationships will show the data in arrays.
.. code-block:: bash
# curl -g "http://localhost:3000/directors?select=first_name,...films(film_titles:title,film_years:year)&first_name=like.Quentin*"
curl --get "http://localhost:3000/directors" \
-d "select=first_name,...films(film_titles:title,film_years:year)" \
-d "first_name=like.Quentin*"
.. code-block:: json
[
{
"first_name": "Quentin",
"film_titles": [
"Pulp Fiction",
"Reservoir Dogs"
],
"film_years": [
1994,
1992
]
}
]
Note that there is no ``films`` array of objects.
By default, the order of the values inside the resulting array is unspecified but `it is safe to assume <https://www.postgresql.org/message-id/15950.1491843689%40sss.pgh.pa.us>`_ that all the columns return the values in the same unspecified order.
From the previous result, we can say that "Pulp Fiction" premiered in 1994 and "Reservoir Dogs" in 1992.
You can still order all the resulting arrays explicitly. For example, to order by the release year:
.. code-block:: bash
# curl -g "http://localhost:3000/directors?select=first_name,...films(film_titles:title,film_years:year)&first_name=like.Quentin*&films.order=year"
curl --get "http://localhost:3000/directors" \
-d "select=first_name,...films(film_titles:title,film_years:year)" \
-d "first_name=like.Quentin*" \
-d "films.order=year"
.. code-block:: json
[
{
"first_name": "Quentin",
"film_titles": [
"Reservoir Dogs",
"Pulp Fiction"
],
"film_years": [
1992,
1994
]
}
]
Nesting Spreads
~~~~~~~~~~~~~~~
For example, let's nest ``...technical_specs`` (one-to-one) and ``...roles`` (one-to-many) inside ``...films``:
.. code-block:: bash
# curl -g "http://localhost:3000/directors?select=first_name,...films(film_titles:title,film_years:year,...technical_specs(film_runtimes:runtime),...roles(film_characters:character))&first_name=like.Quentin*&films.order=year&films.roles.order=character"
curl --get "http://localhost:3000/directors" \
-d "select=first_name,...films(film_titles:title,film_years:year,...technical_specs(film_runtimes:runtime),...roles(film_characters:character))" \
-d "first_name=like.Quentin*" \
-d "films.order=year" \
-d "films.roles.order=character"
.. code-block:: json
[
{
"first_name": "Quentin",
"film_titles": [
"Reservoir Dogs",
"Pulp Fiction"
],
"film_years": [
1992,
1994
],
"film_runtimes": [
"01:39:00",
"02:29:00"
]
"film_characters": [
[ "Mr. Pink", "Mr. White" ],
[ "Mia Wallace", "Vincent Vega" ]
]
}
]
All the elements inside ``films`` are selected in the same order, including both nested resources.
For example, we can say that "Reservoir Dogs" premiered in 1992, its runtime is 1:39:00 and it has the following characters: ``[ "Mr. Pink", "Mr. White" ]``.
Note that the data inside to-many nested resources can also be ordered (``roles`` by the ``character`` name in our example).
+4 -4
View File
@@ -233,10 +233,6 @@ Related to the HTTP request elements.
| | | there is no many-to-one or one-to-one relationship between |
| PGRST118 | | them. |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst119: | 400 | Could not use the spread operator on the related table |
| | | because there is no many-to-one or one-to-one relationship |
| PGRST119 | | between them. |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst120: | 400 | An embedded resource can only be filtered using the |
| | | ``is.null`` or ``not.is.null`` :ref:`operators <operators>`.|
| PGRST120 | | |
@@ -265,6 +261,10 @@ Related to the HTTP request elements.
| | | accessed. See :ref:`openapi-mode`. |
| PGRST126 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst127: | 400 | The feature specified in the ``details`` field is not |
| | | implemented. |
| PGRST127 | | |
+---------------+-------------+-------------------------------------------------------------+
.. _pgrst2**:
+8 -10
View File
@@ -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
View File
@@ -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
+4 -2
View File
@@ -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
+9
View File
@@ -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)
+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 =
+175 -149
View File
@@ -141,155 +141,170 @@ allowed =
{ matchHeaders = [matchContentTypeJson] }
context "performing aggregations on spreaded fields from an embedded resource" $ do
it "supports the use of aggregates on spreaded fields" $ do
get "/budget_expenses?select=total_expenses:expense_amount.sum(),...budget_categories(budget_owner,total_budget:budget_amount.sum())&order=budget_categories(budget_owner)" `shouldRespondWith`
[json|[
{"total_expenses": 600.52,"budget_owner": "Brian Smith", "total_budget": 2000.42},
{"total_expenses": 100.22, "budget_owner": "Jane Clarkson","total_budget": 7000.41},
{"total_expenses": 900.27, "budget_owner": "Sally Hughes", "total_budget": 500.23}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports the use of aggregates on spreaded fields when only aggregates are supplied" $ do
get "/budget_expenses?select=...budget_categories(total_budget:budget_amount.sum())" `shouldRespondWith`
[json|[{"total_budget": 9501.06}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports aggregates from a spread relationships grouped by spreaded fields from other relationships" $ do
get "/processes?select=...process_costs(cost.sum()),...process_categories(name)" `shouldRespondWith`
[json|[
{"sum": 400.00, "name": "Batch"},
{"sum": 320.00, "name": "Mass"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/processes?select=...process_costs(cost_sum:cost.sum()),...process_categories(category:name)" `shouldRespondWith`
[json|[
{"cost_sum": 400.00, "category": "Batch"},
{"cost_sum": 320.00, "category": "Mass"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports aggregates on spreaded fields from nested relationships" $ do
get "/process_supervisor?select=...processes(factory_id,...process_costs(cost.sum()))" `shouldRespondWith`
[json|[
{"factory_id": 3, "sum": 120.00},
{"factory_id": 2, "sum": 500.00},
{"factory_id": 1, "sum": 350.00}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=...processes(factory_id,...process_costs(cost_sum:cost.sum()))" `shouldRespondWith`
[json|[
{"factory_id": 3, "cost_sum": 120.00},
{"factory_id": 2, "cost_sum": 500.00},
{"factory_id": 1, "cost_sum": 350.00}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports aggregates on spreaded fields from nested relationships, grouped by a regular nested relationship" $ do
get "/process_supervisor?select=...processes(factories(name),...process_costs(cost.sum()))" `shouldRespondWith`
[json|[
{"factories": {"name": "Factory A"}, "sum": 350.00},
{"factories": {"name": "Factory B"}, "sum": 500.00},
{"factories": {"name": "Factory C"}, "sum": 120.00}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=...processes(factory:factories(name),...process_costs(cost_sum:cost.sum()))" `shouldRespondWith`
[json|[
{"factory": {"name": "Factory A"}, "cost_sum": 350.00},
{"factory": {"name": "Factory B"}, "cost_sum": 500.00},
{"factory": {"name": "Factory C"}, "cost_sum": 120.00}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports aggregates on spreaded fields from nested relationships, grouped by spreaded fields from other nested relationships" $ do
get "/process_supervisor?select=supervisor_id,...processes(...process_costs(cost.sum()),...process_categories(name))&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor_id": 1, "sum": 220.00, "name": "Batch"},
{"supervisor_id": 2, "sum": 70.00, "name": "Batch"},
{"supervisor_id": 2, "sum": 200.00, "name": "Mass"},
{"supervisor_id": 3, "sum": 180.00, "name": "Batch"},
{"supervisor_id": 3, "sum": 120.00, "name": "Mass"},
{"supervisor_id": 4, "sum": 180.00, "name": "Batch"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=supervisor_id,...processes(...process_costs(cost_sum:cost.sum()),...process_categories(category:name))&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor_id": 1, "cost_sum": 220.00, "category": "Batch"},
{"supervisor_id": 2, "cost_sum": 70.00, "category": "Batch"},
{"supervisor_id": 2, "cost_sum": 200.00, "category": "Mass"},
{"supervisor_id": 3, "cost_sum": 180.00, "category": "Batch"},
{"supervisor_id": 3, "cost_sum": 120.00, "category": "Mass"},
{"supervisor_id": 4, "cost_sum": 180.00, "category": "Batch"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports aggregates on spreaded fields from nested relationships, grouped by spreaded fields from other nested relationships, using a nested relationship as top parent" $ do
get "/supervisors?select=name,process_supervisor(...processes(...process_costs(cost.sum()),...process_categories(name)))" `shouldRespondWith`
[json|[
{"name": "Mary", "process_supervisor": [{"name": "Batch", "sum": 220.00}]},
{"name": "John", "process_supervisor": [{"name": "Batch", "sum": 70.00}, {"name": "Mass", "sum": 200.00}]},
{"name": "Peter", "process_supervisor": [{"name": "Batch", "sum": 180.00}, {"name": "Mass", "sum": 120.00}]},
{"name": "Sarah", "process_supervisor": [{"name": "Batch", "sum": 180.00}]}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/supervisors?select=name,process_supervisor(...processes(...process_costs(cost_sum:cost.sum()),...process_categories(category:name)))" `shouldRespondWith`
[json|[
{"name": "Mary", "process_supervisor": [{"category": "Batch", "cost_sum": 220.00}]},
{"name": "John", "process_supervisor": [{"category": "Batch", "cost_sum": 70.00}, {"category": "Mass", "cost_sum": 200.00}]},
{"name": "Peter", "process_supervisor": [{"category": "Batch", "cost_sum": 180.00}, {"category": "Mass", "cost_sum": 120.00}]},
{"name": "Sarah", "process_supervisor": [{"category": "Batch", "cost_sum": 180.00}]}]|]
{ matchHeaders = [matchContentTypeJson] }
context "to-one spread relationships" $ do
it "supports the use of aggregates on spreaded fields" $ do
get "/budget_expenses?select=total_expenses:expense_amount.sum(),...budget_categories(budget_owner,total_budget:budget_amount.sum())&order=budget_categories(budget_owner)" `shouldRespondWith`
[json|[
{"total_expenses": 600.52,"budget_owner": "Brian Smith", "total_budget": 2000.42},
{"total_expenses": 100.22, "budget_owner": "Jane Clarkson","total_budget": 7000.41},
{"total_expenses": 900.27, "budget_owner": "Sally Hughes", "total_budget": 500.23}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports the use of aggregates on spreaded fields when only aggregates are supplied" $ do
get "/budget_expenses?select=...budget_categories(total_budget:budget_amount.sum())" `shouldRespondWith`
[json|[{"total_budget": 9501.06}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports aggregates from a spread relationships grouped by spreaded fields from other relationships" $ do
get "/processes?select=...process_costs(cost.sum()),...process_categories(name)" `shouldRespondWith`
[json|[
{"sum": 400.00, "name": "Batch"},
{"sum": 350.00, "name": "Mass"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/processes?select=...process_costs(cost_sum:cost.sum()),...process_categories(category:name)" `shouldRespondWith`
[json|[
{"cost_sum": 400.00, "category": "Batch"},
{"cost_sum": 350.00, "category": "Mass"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports aggregates on spreaded fields from nested relationships" $ do
get "/process_supervisor?select=...processes(factory_id,...process_costs(cost.sum()))" `shouldRespondWith`
[json|[
{"factory_id": 3, "sum": 110.00},
{"factory_id": 2, "sum": 500.00},
{"factory_id": 1, "sum": 350.00}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=...processes(factory_id,...process_costs(cost_sum:cost.sum()))" `shouldRespondWith`
[json|[
{"factory_id": 3, "cost_sum": 110.00},
{"factory_id": 2, "cost_sum": 500.00},
{"factory_id": 1, "cost_sum": 350.00}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports aggregates on spreaded fields from nested relationships, grouped by a regular nested relationship" $ do
get "/process_supervisor?select=...processes(factories(name),...process_costs(cost.sum()))" `shouldRespondWith`
[json|[
{"factories": {"name": "Factory A"}, "sum": 350.00},
{"factories": {"name": "Factory B"}, "sum": 500.00},
{"factories": {"name": "Factory C"}, "sum": 110.00}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=...processes(factory:factories(name),...process_costs(cost_sum:cost.sum()))" `shouldRespondWith`
[json|[
{"factory": {"name": "Factory A"}, "cost_sum": 350.00},
{"factory": {"name": "Factory B"}, "cost_sum": 500.00},
{"factory": {"name": "Factory C"}, "cost_sum": 110.00}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports aggregates on spreaded fields from nested relationships, grouped by spreaded fields from other nested relationships" $ do
get "/process_supervisor?select=supervisor_id,...processes(...process_costs(cost.sum()),...process_categories(name))&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor_id": 1, "sum": 220.00, "name": "Batch"},
{"supervisor_id": 2, "sum": 70.00, "name": "Batch"},
{"supervisor_id": 2, "sum": 200.00, "name": "Mass"},
{"supervisor_id": 3, "sum": 180.00, "name": "Batch"},
{"supervisor_id": 3, "sum": 110.00, "name": "Mass"},
{"supervisor_id": 4, "sum": 180.00, "name": "Batch"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=supervisor_id,...processes(...process_costs(cost_sum:cost.sum()),...process_categories(category:name))&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor_id": 1, "cost_sum": 220.00, "category": "Batch"},
{"supervisor_id": 2, "cost_sum": 70.00, "category": "Batch"},
{"supervisor_id": 2, "cost_sum": 200.00, "category": "Mass"},
{"supervisor_id": 3, "cost_sum": 180.00, "category": "Batch"},
{"supervisor_id": 3, "cost_sum": 110.00, "category": "Mass"},
{"supervisor_id": 4, "cost_sum": 180.00, "category": "Batch"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "supports aggregates on spreaded fields from nested relationships, grouped by spreaded fields from other nested relationships, using a nested relationship as top parent" $ do
get "/supervisors?select=name,process_supervisor(...processes(...process_costs(cost.sum()),...process_categories(name)))" `shouldRespondWith`
[json|[
{"name": "Mary", "process_supervisor": [{"name": "Batch", "sum": 220.00}]},
{"name": "John", "process_supervisor": [{"name": "Batch", "sum": 70.00}, {"name": "Mass", "sum": 200.00}]},
{"name": "Peter", "process_supervisor": [{"name": "Batch", "sum": 180.00}, {"name": "Mass", "sum": 110.00}]},
{"name": "Sarah", "process_supervisor": [{"name": "Batch", "sum": 180.00}]},
{"name": "Jane", "process_supervisor": []}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/supervisors?select=name,process_supervisor(...processes(...process_costs(cost_sum:cost.sum()),...process_categories(category:name)))" `shouldRespondWith`
[json|[
{"name": "Mary", "process_supervisor": [{"category": "Batch", "cost_sum": 220.00}]},
{"name": "John", "process_supervisor": [{"category": "Batch", "cost_sum": 70.00}, {"category": "Mass", "cost_sum": 200.00}]},
{"name": "Peter", "process_supervisor": [{"category": "Batch", "cost_sum": 180.00}, {"category": "Mass", "cost_sum": 110.00}]},
{"name": "Sarah", "process_supervisor": [{"category": "Batch", "cost_sum": 180.00}]},
{"name": "Jane", "process_supervisor": []}]|]
{ matchHeaders = [matchContentTypeJson] }
context "supports count() aggregate without specifying a field" $ do
it "works by itself in the embedded resource" $ do
get "/process_supervisor?select=supervisor_id,...processes(count())&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor_id": 1, "count": 2},
{"supervisor_id": 2, "count": 2},
{"supervisor_id": 3, "count": 3},
{"supervisor_id": 4, "count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=supervisor_id,...processes(processes_count:count())&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor_id": 1, "processes_count": 2},
{"supervisor_id": 2, "processes_count": 2},
{"supervisor_id": 3, "processes_count": 3},
{"supervisor_id": 4, "processes_count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works alongside other columns in the embedded resource" $ do
get "/process_supervisor?select=...supervisors(id,count())&order=supervisors(id)" `shouldRespondWith`
[json|[
{"id": 1, "count": 2},
{"id": 2, "count": 2},
{"id": 3, "count": 3},
{"id": 4, "count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=...supervisors(supervisor:id,supervisor_count:count())&order=supervisors(supervisor)" `shouldRespondWith`
[json|[
{"supervisor": 1, "supervisor_count": 2},
{"supervisor": 2, "supervisor_count": 2},
{"supervisor": 3, "supervisor_count": 3},
{"supervisor": 4, "supervisor_count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works on nested resources" $ do
get "/process_supervisor?select=supervisor_id,...processes(...process_costs(count()))&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor_id": 1, "count": 2},
{"supervisor_id": 2, "count": 2},
{"supervisor_id": 3, "count": 2},
{"supervisor_id": 4, "count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=supervisor:supervisor_id,...processes(...process_costs(process_costs_count:count()))&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor": 1, "process_costs_count": 2},
{"supervisor": 2, "process_costs_count": 2},
{"supervisor": 3, "process_costs_count": 2},
{"supervisor": 4, "process_costs_count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works on nested resources grouped by spreaded fields" $ do
get "/process_supervisor?select=...processes(factory_id,...process_costs(count()))&order=processes(factory_id)" `shouldRespondWith`
[json|[
{"factory_id": 1, "count": 2},
{"factory_id": 2, "count": 4},
{"factory_id": 3, "count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=...processes(factory:factory_id,...process_costs(process_costs_count:count()))&order=processes(factory)" `shouldRespondWith`
[json|[
{"factory": 1, "process_costs_count": 2},
{"factory": 2, "process_costs_count": 4},
{"factory": 3, "process_costs_count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works on different levels of the nested resources at the same time" $
get "/process_supervisor?select=...processes(factory:factory_id,processes_count:count(),...process_costs(process_costs_count:count()))&order=processes(factory)" `shouldRespondWith`
[json|[
{"factory": 1, "processes_count": 2, "process_costs_count": 2},
{"factory": 2, "processes_count": 4, "process_costs_count": 4},
{"factory": 3, "processes_count": 2, "process_costs_count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
context "supports count() aggregate without specifying a field" $ do
it "works by itself in the embedded resource" $ do
get "/process_supervisor?select=supervisor_id,...processes(count())&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor_id": 1, "count": 2},
{"supervisor_id": 2, "count": 2},
{"supervisor_id": 3, "count": 3},
{"supervisor_id": 4, "count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=supervisor_id,...processes(processes_count:count())&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor_id": 1, "processes_count": 2},
{"supervisor_id": 2, "processes_count": 2},
{"supervisor_id": 3, "processes_count": 3},
{"supervisor_id": 4, "processes_count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works alongside other columns in the embedded resource" $ do
get "/process_supervisor?select=...supervisors(id,count())&order=supervisors(id)" `shouldRespondWith`
[json|[
{"id": 1, "count": 2},
{"id": 2, "count": 2},
{"id": 3, "count": 3},
{"id": 4, "count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=...supervisors(supervisor:id,supervisor_count:count())&order=supervisors(supervisor)" `shouldRespondWith`
[json|[
{"supervisor": 1, "supervisor_count": 2},
{"supervisor": 2, "supervisor_count": 2},
{"supervisor": 3, "supervisor_count": 3},
{"supervisor": 4, "supervisor_count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works on nested resources" $ do
get "/process_supervisor?select=supervisor_id,...processes(...process_costs(count()))&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor_id": 1, "count": 2},
{"supervisor_id": 2, "count": 2},
{"supervisor_id": 3, "count": 3},
{"supervisor_id": 4, "count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=supervisor:supervisor_id,...processes(...process_costs(process_costs_count:count()))&order=supervisor_id" `shouldRespondWith`
[json|[
{"supervisor": 1, "process_costs_count": 2},
{"supervisor": 2, "process_costs_count": 2},
{"supervisor": 3, "process_costs_count": 3},
{"supervisor": 4, "process_costs_count": 1}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works on nested resources grouped by spreaded fields" $ do
get "/process_supervisor?select=...processes(factory_id,...process_costs(count()))&order=processes(factory_id)" `shouldRespondWith`
[json|[
{"factory_id": 1, "count": 2},
{"factory_id": 2, "count": 4},
{"factory_id": 3, "count": 2}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/process_supervisor?select=...processes(factory:factory_id,...process_costs(process_costs_count:count()))&order=processes(factory)" `shouldRespondWith`
[json|[
{"factory": 1, "process_costs_count": 2},
{"factory": 2, "process_costs_count": 4},
{"factory": 3, "process_costs_count": 2}]|]
{ matchHeaders = [matchContentTypeJson] }
it "works on different levels of the nested resources at the same time" $
get "/process_supervisor?select=...processes(factory:factory_id,processes_count:count(),...process_costs(process_costs_count:count()))&order=processes(factory)" `shouldRespondWith`
[json|[
{"factory": 1, "processes_count": 2, "process_costs_count": 2},
{"factory": 2, "processes_count": 4, "process_costs_count": 4},
{"factory": 3, "processes_count": 2, "process_costs_count": 2}]|]
{ matchHeaders = [matchContentTypeJson] }
context "to-many spread relationships" $ do
it "does not support the use of aggregates" $ do
get "/factories?select=name,...factory_buildings(type,size.sum())" `shouldRespondWith`
[json|{
"code": "PGRST127",
"message":"Feature not implemented",
"details":"Aggregates are not implemented for one-to-many or many-to-many spreads.",
"hint":null
}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson] }
disallowed :: SpecWith ((), Application)
disallowed =
@@ -316,7 +331,7 @@ disallowed =
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson] }
it "prevents the use of aggregates on spread embeds" $
it "prevents the use of aggregates on to-one spread embeds" $
get "/project_invoices?select=...projects(id.count())" `shouldRespondWith`
[json|{
"hint":null,
@@ -326,3 +341,14 @@ disallowed =
}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson] }
it "prevents the use of aggregates on to-many spread embeds" $
get "/factories?select=...processes(id.count())" `shouldRespondWith`
[json|{
"hint":null,
"details":null,
"code":"PGRST123",
"message":"Use of aggregate functions is not allowed"
}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson] }
+510 -22
View File
@@ -63,28 +63,6 @@ spec =
, matchHeaders = [matchContentTypeJson]
}
it "fails when is not a to-one relationship" $ do
get "/clients?select=*,...projects(*)" `shouldRespondWith`
[json|{
"code":"PGRST119",
"details":"'clients' and 'projects' do not form a many-to-one or one-to-one relationship",
"hint":null,
"message":"A spread operation on 'projects' is not possible"
}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
get "/designers?select=*,...computed_videogames(*)" `shouldRespondWith`
[json|{
"code":"PGRST119",
"details":"'designers' and 'computed_videogames' do not form a many-to-one or one-to-one relationship",
"hint":null,
"message":"A spread operation on 'computed_videogames' is not possible"
}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
}
it "can include or exclude attributes of the junction on a m2m" $ do
get "/users?select=*,tasks:users_tasks(*,...tasks(*))&limit=1" `shouldRespondWith`
[json|[{
@@ -112,3 +90,513 @@ spec =
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
context "one-to-many relationships" $ do
it "should spread a column as a json array" $ do
get "/factories?select=factory:name,...processes(name)&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","name":["Process A1", "Process A2"]},
{"factory":"Factory B","name":["Process B1", "Process B2"]},
{"factory":"Factory C","name":["Process C1", "Process C2", "Process XX", "Process YY"]},
{"factory":"Factory D","name":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
get "/factories?select=factory:name,...processes(processes:name)&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","processes":["Process A1", "Process A2"]},
{"factory":"Factory B","processes":["Process B1", "Process B2"]},
{"factory":"Factory C","processes":["Process C1", "Process C2", "Process XX", "Process YY"]},
{"factory":"Factory D","processes":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should spread many columns as json arrays" $ do
get "/factories?select=factory:name,...processes(name,category_id)&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","name":["Process A1", "Process A2"],"category_id":[1, 2]},
{"factory":"Factory B","name":["Process B1", "Process B2"],"category_id":[1, 1]},
{"factory":"Factory C","name":["Process C1", "Process C2", "Process XX", "Process YY"],"category_id":[2, 2, 2, 2]},
{"factory":"Factory D","name":[],"category_id":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
get "/factories?select=factory:name,...processes(processes:name,categories:category_id)&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","processes":["Process A1", "Process A2"],"categories":[1, 2]},
{"factory":"Factory B","processes":["Process B1", "Process B2"],"categories":[1, 1]},
{"factory":"Factory C","processes":["Process C1", "Process C2", "Process XX", "Process YY"],"categories":[2, 2, 2, 2]},
{"factory":"Factory D","processes":[],"categories":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should return an empty array when no elements are found" $
get "/factories?select=factory:name,...processes(processes:name)&processes=is.null" `shouldRespondWith`
[json|[
{"factory":"Factory D","processes":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should return a single null element array, not an empty one, when the row exists but the value happens to be null" $
get "/managers?select=name,...organizations(organizations:name,referees:referee)&id=eq.1" `shouldRespondWith`
[json|[
{"name":"Referee Manager","organizations":["Referee Org"],"referees":[null]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should work when selecting all columns" $
get "/factories?select=factory:name,...processes(*)&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","id":[1, 2],"name":["Process A1", "Process A2"],"factory_id":[1, 1],"category_id":[1, 2]},
{"factory":"Factory B","id":[3, 4],"name":["Process B1", "Process B2"],"factory_id":[2, 2],"category_id":[1, 1]},
{"factory":"Factory C","id":[5, 6, 7, 8],"name":["Process C1", "Process C2", "Process XX", "Process YY"],"factory_id":[3, 3, 3, 3],"category_id":[2, 2, 2, 2]},
{"factory":"Factory D","id":[],"name":[],"factory_id":[],"category_id":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should select spread columns from a nested one-to-one relationship" $
get "/factories?select=factory:name,...processes(process:name,...process_costs(process_costs:cost))&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","process":["Process A1", "Process A2"],"process_costs":[150.00, 200.00]},
{"factory":"Factory B","process":["Process B1", "Process B2"],"process_costs":[180.00, 70.00]},
{"factory":"Factory C","process":["Process C1", "Process C2", "Process YY", "Process XX"],"process_costs":[40.00, 70.00, 40.00, null]},
{"factory":"Factory D","process":[],"process_costs":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should select spread columns from a nested many-to-one relationship" $
get "/factories?select=factory:name,...processes(process:name,...process_categories(categories:name))&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","process":["Process A1", "Process A2"],"categories":["Batch", "Mass"]},
{"factory":"Factory B","process":["Process B2", "Process B1"],"categories":["Batch", "Batch"]},
{"factory":"Factory C","process":["Process YY", "Process XX", "Process C2", "Process C1"],"categories":["Mass", "Mass", "Mass", "Mass"]},
{"factory":"Factory D","process":[],"categories":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should select spread columns from a nested one-to-many relationship" $
get "/factories?select=factory:name,...processes(process:name,...process_supervisor(supervisor_ids:supervisor_id))&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","process":["Process A1", "Process A2"],"supervisor_ids":[[1], [2]]},
{"factory":"Factory B","process":["Process B1", "Process B2"],"supervisor_ids":[[3, 4], [1, 2]]},
{"factory":"Factory C","process":["Process C1", "Process C2", "Process XX", "Process YY"],"supervisor_ids":[[3], [3], [], []]},
{"factory":"Factory D","process":[],"supervisor_ids":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should select spread columns from a nested many-to-many relationship" $ do
get "/factories?select=factory:name,...processes(process:name,...supervisors(supervisors:name))&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","process":["Process A1", "Process A2"],"supervisors":[["Mary"], ["John"]]},
{"factory":"Factory B","process":["Process B1", "Process B2"],"supervisors":[["Peter", "Sarah"], ["Mary", "John"]]},
{"factory":"Factory C","process":["Process C1", "Process C2", "Process XX", "Process YY"],"supervisors":[["Peter"], ["Peter"], [], []]},
{"factory":"Factory D","process":[],"supervisors":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show a nested non-spread one-to-one relationship as an array of objects" $ do
get "/factories?select=factory:name,...processes(process:name,process_costs(cost))&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","process":["Process A1", "Process A2"],"process_costs":[{"cost": 150.00}, {"cost": 200.00}]},
{"factory":"Factory B","process":["Process B1", "Process B2"],"process_costs":[{"cost": 180.00}, {"cost": 70.00}]},
{"factory":"Factory C","process":["Process C1", "Process C2", "Process YY", "Process XX"],"process_costs":[{"cost": 40.00}, {"cost": 70.00}, {"cost": 40.00}, null]},
{"factory":"Factory D","process":[],"process_costs":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show a nested non-spread many-to-one relationship as an array of objects" $
get "/factories?select=factory:name,...processes(process:name,process_categories(name))&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","process":["Process A1", "Process A2"],"process_categories":[{"name": "Batch"}, {"name": "Mass"}]},
{"factory":"Factory B","process":["Process B2", "Process B1"],"process_categories":[{"name": "Batch"}, {"name": "Batch"}]},
{"factory":"Factory C","process":["Process YY", "Process XX", "Process C2", "Process C1"],"process_categories":[{"name": "Mass"}, {"name": "Mass"}, {"name": "Mass"}, {"name": "Mass"}]},
{"factory":"Factory D","process":[],"process_categories":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show a nested non-spread one-to-many relationship as an array of arrays" $
get "/factories?select=factory:name,...processes(process:name,process_supervisor(supervisor_id))&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","process":["Process A1", "Process A2"],"process_supervisor":[[{"supervisor_id": 1}], [{"supervisor_id": 2}]]},
{"factory":"Factory B","process":["Process B1", "Process B2"],"process_supervisor":[[{"supervisor_id": 3}, {"supervisor_id": 4}], [{"supervisor_id": 1}, {"supervisor_id": 2}]]},
{"factory":"Factory C","process":["Process C1", "Process C2", "Process XX", "Process YY"],"process_supervisor":[[{"supervisor_id": 3}], [{"supervisor_id": 3}], [], []]},
{"factory":"Factory D","process":[],"process_supervisor":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show a nested non-spread many-to-many relationship as an array of arrays" $
get "/factories?select=factory:name,...processes(process:name,supervisors(name))&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","process":["Process A1", "Process A2"],"supervisors":[[{"name": "Mary"}], [{"name": "John"}]]},
{"factory":"Factory B","process":["Process B1", "Process B2"],"supervisors":[[{"name": "Peter"}, {"name": "Sarah"}], [{"name": "Mary"}, {"name": "John"}]]},
{"factory":"Factory C","process":["Process C1", "Process C2", "Process XX", "Process YY"],"supervisors":[[{"name": "Peter"}], [{"name": "Peter"}], [], []]},
{"factory":"Factory D","process":[],"supervisors":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should work when selecting all columns in a nested to-one resource" $
get "/factories?select=factory:name,...processes(*,...process_costs(*))&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","id":[1, 2],"name":["Process A1", "Process A2"],"factory_id":[1, 1],"category_id":[1, 2],"process_id":[1, 2],"cost":[150.00, 200.00]},
{"factory":"Factory B","id":[3, 4],"name":["Process B1", "Process B2"],"factory_id":[2, 2],"category_id":[1, 1],"process_id":[3, 4],"cost":[180.00, 70.00]},
{"factory":"Factory C","id":[5, 6, 8, 7],"name":["Process C1", "Process C2", "Process YY", "Process XX"],"factory_id":[3, 3, 3, 3],"category_id":[2, 2, 2, 2],"process_id":[5, 6, 8, null],"cost":[40.00, 70.00, 40.00, null]},
{"factory":"Factory D","id":[],"name":[],"factory_id":[],"category_id":[],"process_id":[],"cost":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "works when column filters are specified" $
get "/factories?select=factory:name,...processes(*)&processes.name=not.like.*1&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","id":[2],"name":["Process A2"],"factory_id":[1],"category_id":[2]},
{"factory":"Factory B","id":[4],"name":["Process B2"],"factory_id":[2],"category_id":[1]},
{"factory":"Factory C","id":[6, 7, 8],"name":["Process C2", "Process XX", "Process YY"],"factory_id":[3, 3, 3],"category_id":[2, 2, 2]},
{"factory":"Factory D","id":[],"name":[],"factory_id":[],"category_id":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "works with inner joins or not.is.null filters" $ do
get "/factories?select=factory:name,...processes!inner(name)&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","name":["Process A1", "Process A2"]},
{"factory":"Factory B","name":["Process B1", "Process B2"]},
{"factory":"Factory C","name":["Process C1", "Process C2", "Process XX", "Process YY"]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
get "/factories?select=factory:name,...processes(name)&processes=not.is.null&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","name":["Process A1", "Process A2"]},
{"factory":"Factory B","name":["Process B1", "Process B2"]},
{"factory":"Factory C","name":["Process C1", "Process C2", "Process XX", "Process YY"]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "orders all the resulting arrays according to the spread relationship ordering columns" $ do
get "/factories?select=factory:name,...processes(*)&processes.order=category_id.asc,name.desc&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","id":[1, 2],"name":["Process A1", "Process A2"],"factory_id":[1, 1],"category_id":[1, 2]},
{"factory":"Factory B","id":[4, 3],"name":["Process B2", "Process B1"],"factory_id":[2, 2],"category_id":[1, 1]},
{"factory":"Factory C","id":[8, 7, 6, 5],"name":["Process YY", "Process XX", "Process C2", "Process C1"],"factory_id":[3, 3, 3, 3],"category_id":[2, 2, 2, 2]},
{"factory":"Factory D","id":[],"name":[],"factory_id":[],"category_id":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
get "/factories?select=factory:name,...factory_buildings(*)&factory_buildings.order=inspections->pending.asc.nullsfirst,inspections->ins.desc&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","id":[2, 1],"code":["A002", "A001"],"size":[200, 150],"type":["A", "A"],"factory_id":[1, 1],"inspections":[{"ins": "2025A", "pending": true}, {"ins": "2024C", "pending": true}]},
{"factory":"Factory B","id":[4, 3],"code":["B002", "B001"],"size":[120, 50],"type":["C", "B"],"factory_id":[2, 2],"inspections":[{"ins": "2023A"}, {"ins": "2025A", "pending": true}]},
{"factory":"Factory C","id":[5],"code":["C001"],"size":[240],"type":["B"],"factory_id":[3],"inspections":[{"ins": "2022B"}]},
{"factory":"Factory D","id":[6],"code":["D001"],"size":[310],"type":["A"],"factory_id":[4],"inspections":[{"ins": "2024C", "pending": true}]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "orders all the resulting arrays according to the spread relationship ordering columns even if they aren't selected" $
get "/factories?select=factory:name,...processes(name)&processes.order=category_id.asc,name.desc&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","name":["Process A1", "Process A2"]},
{"factory":"Factory B","name":["Process B2", "Process B1"]},
{"factory":"Factory C","name":["Process YY", "Process XX", "Process C2", "Process C1"]},
{"factory":"Factory D","name":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "orders all the resulting arrays according to the related ordering columns in the spread relationship" $
get "/factories?select=factory:name,...processes(name,...process_costs(cost))&processes.order=process_costs(cost)&order=name" `shouldRespondWith`
[json|[
{"factory":"Factory A","name":["Process A1", "Process A2"],"cost":[150.00, 200.00]},
{"factory":"Factory B","name":["Process B2", "Process B1"],"cost":[70.00, 180.00]},
{"factory":"Factory C","name":["Process C1", "Process YY", "Process C2", "Process XX"],"cost":[40.00, 40.00, 70.00, null]},
{"factory":"Factory D","name":[],"cost":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
context "many-to-many relationships" $ do
it "should spread a column as a json array" $ do
get "/operators?select=operator:name,...processes(name)&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","name":["Process C2", "Process XX"]},
{"operator":"Anne","name":["Process A1", "Process A2", "Process B2"]},
{"operator":"Jeff","name":["Process A2", "Process B1", "Process B2", "Process C2"]},
{"operator":"Liz","name":[]},
{"operator":"Louis","name":["Process A1", "Process A2"]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
get "/operators?select=operator:name,...processes(processes:name)&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","processes":["Process C2", "Process XX"]},
{"operator":"Anne","processes":["Process A1", "Process A2", "Process B2"]},
{"operator":"Jeff","processes":["Process A2", "Process B1", "Process B2", "Process C2"]},
{"operator":"Liz","processes":[]},
{"operator":"Louis","processes":["Process A1", "Process A2"]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should spread many columns as json arrays" $ do
get "/operators?select=operator:name,...processes(name,category_id)&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","name":["Process C2", "Process XX"],"category_id":[2, 2]},
{"operator":"Anne","name":["Process A1", "Process A2", "Process B2"],"category_id":[1, 2, 1]},
{"operator":"Jeff","name":["Process A2", "Process B1", "Process B2", "Process C2"],"category_id":[2, 1, 1, 2]},
{"operator":"Liz","name":[],"category_id":[]},
{"operator":"Louis","name":["Process A1", "Process A2"],"category_id":[1, 2]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
get "/operators?select=operator:name,...processes(processes:name,categories:category_id)&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","processes":["Process C2", "Process XX"],"categories":[2, 2]},
{"operator":"Anne","processes":["Process A1", "Process A2", "Process B2"],"categories":[1, 2, 1]},
{"operator":"Jeff","processes":["Process A2", "Process B1", "Process B2", "Process C2"],"categories":[2, 1, 1, 2]},
{"operator":"Liz","processes":[],"categories":[]},
{"operator":"Louis","processes":["Process A1", "Process A2"],"categories":[1, 2]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should return an empty array when no elements are found" $
get "/operators?select=operator:name,...processes(processes:name)&processes=is.null" `shouldRespondWith`
[json|[
{"operator":"Liz","processes":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should return a single null element array, not an empty one, when the row exists but the value happens to be null" $
get "/operators?select=name,...processes(process:name,...process_costs(cost)))&id=eq.5&processes.id=eq.7" `shouldRespondWith`
[json|[
{"name":"Alfred","process":["Process XX"],"cost":[null]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should work when selecting all columns" $
get "/operators?select=operator:name,...processes(*)&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","id":[6, 7],"name":["Process C2", "Process XX"],"factory_id":[3, 3],"category_id":[2, 2]},
{"operator":"Anne","id":[1, 2, 4],"name":["Process A1", "Process A2", "Process B2"],"factory_id":[1, 1, 2],"category_id":[1, 2, 1]},
{"operator":"Jeff","id":[2, 3, 4, 6],"name":["Process A2", "Process B1", "Process B2", "Process C2"],"factory_id":[1, 2, 2, 3],"category_id":[2, 1, 1, 2]},
{"operator":"Liz","id":[],"name":[],"factory_id":[],"category_id":[]},
{"operator":"Louis","id":[1, 2],"name":["Process A1", "Process A2"],"factory_id":[1, 1],"category_id":[1, 2]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show spread columns from a nested one-to-one relationship" $
get "/operators?select=operator:name,...processes(process:name,...process_costs(process_costs:cost))&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","process":["Process C2", "Process XX"],"process_costs":[70.00, null]},
{"operator":"Anne","process":["Process A1", "Process A2", "Process B2"],"process_costs":[150.00, 200.00, 70.00]},
{"operator":"Jeff","process":["Process A2", "Process B1", "Process B2", "Process C2"],"process_costs":[200.00, 180.00, 70.00, 70.00]},
{"operator":"Liz","process":[],"process_costs":[]},
{"operator":"Louis","process":["Process A1", "Process A2"],"process_costs":[150.00, 200.00]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show spread columns from a nested many-to-one relationship" $
get "/operators?select=operator:name,...processes(process:name,...process_categories(categories:name))&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","process":["Process C2", "Process XX"],"categories":["Mass", "Mass"]},
{"operator":"Anne","process":["Process A1", "Process A2", "Process B2"],"categories":["Batch", "Mass", "Batch"]},
{"operator":"Jeff","process":["Process A2", "Process B1", "Process B2", "Process C2"],"categories":["Mass", "Batch", "Batch", "Mass"]},
{"operator":"Liz","process":[],"categories":[]},
{"operator":"Louis","process":["Process A1", "Process A2"],"categories":["Batch", "Mass"]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show spread columns from a nested one-to-many relationship" $
get "/operators?select=operator:name,...processes(process:name,...process_supervisor(supervisor_ids:supervisor_id))&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","process":["Process C2", "Process XX"],"supervisor_ids":[[3], []]},
{"operator":"Anne","process":["Process A1", "Process A2", "Process B2"],"supervisor_ids":[[1], [2], [1, 2]]},
{"operator":"Jeff","process":["Process A2", "Process B1", "Process B2", "Process C2"],"supervisor_ids":[[2], [3, 4], [1, 2], [3]]},
{"operator":"Liz","process":[],"supervisor_ids":[]},
{"operator":"Louis","process":["Process A1", "Process A2"],"supervisor_ids":[[1], [2]]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show spread columns from a nested many-to-many relationship" $ do
get "/operators?select=operator:name,...processes(process:name,...supervisors(supervisors:name))&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","process":["Process C2", "Process XX"],"supervisors":[["Peter"], []]},
{"operator":"Anne","process":["Process A1", "Process A2", "Process B2"],"supervisors":[["Mary"], ["John"], ["Mary", "John"]]},
{"operator":"Jeff","process":["Process A2", "Process B1", "Process B2", "Process C2"],"supervisors":[["John"], ["Peter", "Sarah"], ["Mary", "John"], ["Peter"]]},
{"operator":"Liz","process":[],"supervisors":[]},
{"operator":"Louis","process":["Process A1", "Process A2"],"supervisors":[["Mary"], ["John"]]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show a nested non-spread one-to-one relationship as an array of objects" $ do
get "/operators?select=operator:name,...processes(process:name,process_costs(cost))&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","process":["Process C2", "Process XX"],"process_costs":[{"cost": 70.00}, null]},
{"operator":"Anne","process":["Process A1", "Process A2", "Process B2"],"process_costs":[{"cost": 150.00}, {"cost": 200.00}, {"cost": 70.00}]},
{"operator":"Jeff","process":["Process A2", "Process B1", "Process B2", "Process C2"],"process_costs":[{"cost": 200.00}, {"cost": 180.00}, {"cost": 70.00}, {"cost": 70.00}]},
{"operator":"Liz","process":[],"process_costs":[]},
{"operator":"Louis","process":["Process A1", "Process A2"],"process_costs":[{"cost": 150.00}, {"cost": 200.00}]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show a nested non-spread many-to-one relationship as an array of objects" $
get "/operators?select=operator:name,...processes(process:name,process_categories(name))&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","process":["Process C2", "Process XX"],"process_categories":[{"name": "Mass"}, {"name": "Mass"}]},
{"operator":"Anne","process":["Process A1", "Process A2", "Process B2"],"process_categories":[{"name": "Batch"}, {"name": "Mass"}, {"name": "Batch"}]},
{"operator":"Jeff","process":["Process A2", "Process B1", "Process B2", "Process C2"],"process_categories":[{"name": "Mass"}, {"name": "Batch"}, {"name": "Batch"}, {"name": "Mass"}]},
{"operator":"Liz","process":[],"process_categories":[]},
{"operator":"Louis","process":["Process A1", "Process A2"],"process_categories":[{"name": "Batch"}, {"name": "Mass"}]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show a nested non-spread one-to-many relationship as an array of arrays" $
get "/operators?select=operator:name,...processes(process:name,process_supervisor(supervisor_id))&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","process":["Process C2", "Process XX"],"process_supervisor":[[{"supervisor_id": 3}], []]},
{"operator":"Anne","process":["Process A1", "Process A2", "Process B2"],"process_supervisor":[[{"supervisor_id": 1}], [{"supervisor_id": 2}], [{"supervisor_id": 1}, {"supervisor_id": 2}]]},
{"operator":"Jeff","process":["Process A2", "Process B1", "Process B2", "Process C2"],"process_supervisor":[[{"supervisor_id": 2}], [{"supervisor_id": 3}, {"supervisor_id": 4}], [{"supervisor_id": 1}, {"supervisor_id": 2}], [{"supervisor_id": 3}]]},
{"operator":"Liz","process":[],"process_supervisor":[]},
{"operator":"Louis","process":["Process A1", "Process A2"],"process_supervisor":[[{"supervisor_id": 1}], [{"supervisor_id": 2}]]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should show a nested non-spread many-to-many relationship as an array of arrays" $
get "/operators?select=operator:name,...processes(process:name,supervisors(name))&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","process":["Process C2", "Process XX"],"supervisors":[[{"name": "Peter"}], []]},
{"operator":"Anne","process":["Process A1", "Process A2", "Process B2"],"supervisors":[[{"name": "Mary"}], [{"name": "John"}], [{"name": "Mary"}, {"name": "John"}]]},
{"operator":"Jeff","process":["Process A2", "Process B1", "Process B2", "Process C2"],"supervisors":[[{"name": "John"}], [{"name": "Peter"}, {"name": "Sarah"}], [{"name": "Mary"}, {"name": "John"}], [{"name": "Peter"}]]},
{"operator":"Liz","process":[],"supervisors":[]},
{"operator":"Louis","process":["Process A1", "Process A2"],"supervisors":[[{"name": "Mary"}], [{"name": "John"}]]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "should work when selecting all columns in a nested to-one resource" $
get "/operators?select=operator:name,...processes(*,...process_costs(*))&order=name" `shouldRespondWith`
[json|[
{"operator":"Alfred","id":[6, 7],"name":["Process C2", "Process XX"],"factory_id":[3, 3],"category_id":[2, 2],"process_id":[6, null],"cost":[70.00, null]},
{"operator":"Anne","id":[1, 2, 4],"name":["Process A1", "Process A2", "Process B2"],"factory_id":[1, 1, 2],"category_id":[1, 2, 1],"process_id":[1, 2, 4],"cost":[150.00, 200.00, 70.00]},
{"operator":"Jeff","id":[2, 3, 4, 6],"name":["Process A2", "Process B1", "Process B2", "Process C2"],"factory_id":[1, 2, 2, 3],"category_id":[2, 1, 1, 2],"process_id":[2, 3, 4, 6],"cost":[200.00, 180.00, 70.00, 70.00]},
{"operator":"Liz","id":[],"name":[],"factory_id":[],"category_id":[],"process_id":[],"cost":[]},
{"operator":"Louis","id":[1, 2],"name":["Process A1", "Process A2"],"factory_id":[1, 1],"category_id":[1, 2],"process_id":[1, 2],"cost":[150.00, 200.00]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "works when column filters are specified" $
get "/supervisors?select=supervisor:name,...processes(*)&processes.name=not.like.*1&order=name" `shouldRespondWith`
[json|[
{"supervisor":"Jane","id":[],"name":[],"factory_id":[],"category_id":[]},
{"supervisor":"John","id":[2, 4],"name":["Process A2", "Process B2"],"factory_id":[1, 2],"category_id":[2, 1]},
{"supervisor":"Mary","id":[4],"name":["Process B2"],"factory_id":[2],"category_id":[1]},
{"supervisor":"Peter","id":[6],"name":["Process C2"],"factory_id":[3],"category_id":[2]},
{"supervisor":"Sarah","id":[],"name":[],"factory_id":[],"category_id":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "works with inner joins or not.is.null filters" $ do
get "/supervisors?select=supervisor:name,...processes!inner(name)&order=name" `shouldRespondWith`
[json|[
{"supervisor":"John","name":["Process A2", "Process B2"]},
{"supervisor":"Mary","name":["Process A1", "Process B2"]},
{"supervisor":"Peter","name":["Process B1", "Process C1", "Process C2"]},
{"supervisor":"Sarah","name":["Process B1"]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
get "/supervisors?select=supervisor:name,...processes(name)&processes=not.is.null&order=name" `shouldRespondWith`
[json|[
{"supervisor":"John","name":["Process A2", "Process B2"]},
{"supervisor":"Mary","name":["Process A1", "Process B2"]},
{"supervisor":"Peter","name":["Process B1", "Process C1", "Process C2"]},
{"supervisor":"Sarah","name":["Process B1"]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "orders all the resulting arrays according to the spread relationship ordering columns" $ do
get "/supervisors?select=supervisor:name,...processes(*)&processes.order=category_id.asc,name.desc&order=name" `shouldRespondWith`
[json|[
{"supervisor":"Jane","id":[],"name":[],"factory_id":[],"category_id":[]},
{"supervisor":"John","id":[4, 2],"name":["Process B2", "Process A2"],"factory_id":[2, 1],"category_id":[1, 2]},
{"supervisor":"Mary","id":[4, 1],"name":["Process B2", "Process A1"],"factory_id":[2, 1],"category_id":[1, 1]},
{"supervisor":"Peter","id":[3, 6, 5],"name":["Process B1", "Process C2", "Process C1"],"factory_id":[2, 3, 3],"category_id":[1, 2, 2]},
{"supervisor":"Sarah","id":[3],"name":["Process B1"],"factory_id":[2],"category_id":[1]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
get "/processes?select=process:name,...operators(*)&operators.order=status->afk.asc.nullsfirst,status->id.desc&order=name" `shouldRespondWith`
[json|[
{"process":"Process A1","id":[2, 1],"name":["Louis", "Anne"],"status":[{"id": "012345"}, {"id": "543210", "afk": true}]},
{"process":"Process A2","id":[2, 3, 1],"name":["Louis", "Jeff", "Anne"],"status":[{"id": "012345"}, {"id": "666666", "afk": true}, {"id": "543210", "afk": true}]},
{"process":"Process B1","id":[3],"name":["Jeff"],"status":[{"id": "666666", "afk": true}]},
{"process":"Process B2","id":[3, 1],"name":["Jeff", "Anne"],"status":[{"id": "666666", "afk": true}, {"id": "543210", "afk": true}]},
{"process":"Process C1","id":[],"name":[],"status":[]},
{"process":"Process C2","id":[5, 3],"name":["Alfred", "Jeff"],"status":[{"id": "000000"}, {"id": "666666", "afk": true}]},
{"process":"Process XX","id":[5],"name":["Alfred"],"status":[{"id": "000000"}]},
{"process":"Process YY","id":[],"name":[],"status":[]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "orders all the resulting arrays according to the spread relationship ordering columns even if they aren't selected" $
get "/supervisors?select=supervisor:name,...processes(name)&processes.order=category_id.asc,name.desc&order=name" `shouldRespondWith`
[json|[
{"supervisor":"Jane","name":[]},
{"supervisor":"John","name":["Process B2", "Process A2"]},
{"supervisor":"Mary","name":["Process B2", "Process A1"]},
{"supervisor":"Peter","name":["Process B1", "Process C2", "Process C1"]},
{"supervisor":"Sarah","name":["Process B1"]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "orders all the resulting arrays according to the related ordering columns in the spread relationship" $
get "/supervisors?select=supervisor:name,...processes(name,...process_costs(cost))&processes.order=process_costs(cost)&order=name" `shouldRespondWith`
[json|[
{"supervisor":"Jane","name":[],"cost":[]},
{"supervisor":"John","name":["Process B2", "Process A2"],"cost":[70.00, 200.00]},
{"supervisor":"Mary","name":["Process B2", "Process A1"],"cost":[70.00, 150.00]},
{"supervisor":"Peter","name":["Process C1", "Process C2", "Process B1"],"cost":[40.00, 70.00, 180.00]},
{"supervisor":"Sarah","name":["Process B1"],"cost":[180.00]}
]|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
+34 -1
View File
@@ -883,19 +883,24 @@ INSERT INTO processes VALUES (3, 'Process B1', 2, 1);
INSERT INTO processes VALUES (4, 'Process B2', 2, 1);
INSERT INTO processes VALUES (5, 'Process C1', 3, 2);
INSERT INTO processes VALUES (6, 'Process C2', 3, 2);
INSERT INTO processes VALUES (7, 'Process XX', 3, 2);
INSERT INTO processes VALUES (8, 'Process YY', 3, 2);
TRUNCATE TABLE process_costs CASCADE;
INSERT INTO process_costs VALUES (1, 150.00);
INSERT INTO process_costs VALUES (2, 200.00);
INSERT INTO process_costs VALUES (3, 180.00);
INSERT INTO process_costs VALUES (4, 70.00);
INSERT INTO process_costs VALUES (5, 120.00);
INSERT INTO process_costs VALUES (5, 40.00);
INSERT INTO process_costs VALUES (6, 70.00);
INSERT INTO process_costs VALUES (8, 40.00);
TRUNCATE TABLE supervisors CASCADE;
INSERT INTO supervisors VALUES (1, 'Mary');
INSERT INTO supervisors VALUES (2, 'John');
INSERT INTO supervisors VALUES (3, 'Peter');
INSERT INTO supervisors VALUES (4, 'Sarah');
INSERT INTO supervisors VALUES (5, 'Jane');
TRUNCATE TABLE process_supervisor CASCADE;
INSERT INTO process_supervisor VALUES (1, 1);
@@ -907,6 +912,34 @@ INSERT INTO process_supervisor VALUES (4, 2);
INSERT INTO process_supervisor VALUES (5, 3);
INSERT INTO process_supervisor VALUES (6, 3);
TRUNCATE TABLE operators CASCADE;
INSERT INTO operators VALUES (1, 'Anne', '{"id": "543210", "afk": true}');
INSERT INTO operators VALUES (2, 'Louis', '{"id": "012345"}');
INSERT INTO operators VALUES (3, 'Jeff', '{"id": "666666", "afk": true}');
INSERT INTO operators VALUES (4, 'Liz', '{"id": "999999"}');
INSERT INTO operators VALUES (5, 'Alfred', '{"id": "000000"}');
TRUNCATE TABLE process_operator CASCADE;
INSERT INTO process_operator VALUES (1,1);
INSERT INTO process_operator VALUES (1,2);
INSERT INTO process_operator VALUES (2,1);
INSERT INTO process_operator VALUES (2,2);
INSERT INTO process_operator VALUES (2,3);
INSERT INTO process_operator VALUES (3,3);
INSERT INTO process_operator VALUES (4,1);
INSERT INTO process_operator VALUES (4,3);
INSERT INTO process_operator VALUES (6,3);
INSERT INTO process_operator VALUES (6,5);
INSERT INTO process_operator VALUES (7,5);
TRUNCATE TABLE factory_buildings CASCADE;
INSERT INTO factory_buildings VALUES (1, 'A001', 150, 'A', 1, '{"ins": "2024C", "pending": true}');
INSERT INTO factory_buildings VALUES (2, 'A002', 200, 'A', 1, '{"ins": "2025A", "pending": true}');
INSERT INTO factory_buildings VALUES (3, 'B001', 50, 'B', 2, '{"ins": "2025A", "pending": true}');
INSERT INTO factory_buildings VALUES (4, 'B002', 120, 'C', 2, '{"ins": "2023A"}');
INSERT INTO factory_buildings VALUES (5, 'C001', 240, 'B', 3, '{"ins": "2022B"}' );
INSERT INTO factory_buildings VALUES (6, 'D001', 310, 'A', 4, '{"ins": "2024C", "pending": true}');
TRUNCATE TABLE surr_serial_upsert CASCADE;
INSERT INTO surr_serial_upsert(name, extra) VALUES ('value', 'existing value');
+21
View File
@@ -3779,3 +3779,24 @@ CREATE TABLE test.albums (
ON UPDATE CASCADE
ON DELETE CASCADE
);
create table operators (
id int primary key,
name text,
status jsonb
);
create table process_operator (
process_id int references processes(id),
operator_id int references operators(id),
primary key (process_id, operator_id)
);
create table factory_buildings (
id int primary key,
code char(4),
size numeric,
"type" char(1),
factory_id int references factories(id),
inspections jsonb
);