diff --git a/docs/odata-data-aggregation-ext/odata-data-aggregation-ext.html b/docs/odata-data-aggregation-ext/odata-data-aggregation-ext.html index 533c6c1d..43accdd6 100644 --- a/docs/odata-data-aggregation-ext/odata-data-aggregation-ext.html +++ b/docs/odata-data-aggregation-ext/odata-data-aggregation-ext.html @@ -1527,7 +1527,7 @@
The definitions of italicized terms made in this section are used throughout this text, always with a hyperlink to this section.
All input sets and output sets in one transformation sequence are collections of the input type, that is the entity type or complex type of the first input set, or in other words, of the resource to which the transformation sequence is applied. The input type is determined by the entity model element identified within the metadata document by the context URL of that resource OData-Protocol, section 10. Individual instances in an input or output set can have a subtype of the input type. (See example 75.) The transformation sequence given as the $apply
system query option is applied to the resource addressed by the resource path. The transformations defined below can have nested transformation sequences as parameters, these are then applied to resources that can differ from the current input set.
All input sets and output sets in one transformation sequence are collections of the input type, that is the entity type or complex type of the first input set, or in other words, of the resource to which the transformation sequence is applied. The input type is determined by the entity model element identified within the metadata document by the context URL of that resource OData-Protocol, section 10. Individual instances in an input or output set can have a subtype of the input type. (See example 74.) The transformation sequence given as the $apply
system query option is applied to the resource addressed by the resource path. The transformations defined below can have nested transformation sequences as parameters, these are then applied to resources that can differ from the current input set.
The structure of an instance that occurs in an input or output set is defined by the names of the structural and navigation properties that the instance contains. Instances of an input type can have different structures, subject to the following rules:
An output set thus consists of instances with different structures. This is the same situation as with a collection of an open type OData-CSDL, sections 6.3 and 9.3 and it is handled in the same way.
-If the first input set is a collection of entities from a given entity set, then so are all input sets and output sets in the transformation sequence. The {select-list}
in the context URL OData-Protocol, section 10 MUST describe only properties that are present or annotated as absent (for example, if Core.Permissions
is None
OData-Protocol, section 11.2.2) in all instances of the collection, after applying any $select
and $expand
system query options. The {select-list}
SHOULD describe as many such properties as possible, even if the request involves a concatenation that leads to a non-homogeneous structure. If the server cannot determine any such properties, the {select-list}
MUST consist of just the instance annotation AnyStructure
defined in the Core
vocabulary OData-VocCore. (See example 76.)
If the first input set is a collection of entities from a given entity set, then so are all input sets and output sets in the transformation sequence. The {select-list}
in the context URL OData-Protocol, section 10 MUST describe only properties that are present or annotated as absent (for example, if Core.Permissions
is None
OData-Protocol, section 11.2.2) in all instances of the collection, after applying any $select
and $expand
system query options. The {select-list}
SHOULD describe as many such properties as possible, even if the request involves a concatenation that leads to a non-homogeneous structure. If the server cannot determine any such properties, the {select-list}
MUST consist of just the instance annotation AnyStructure
defined in the Core
vocabulary OData-VocCore. (See example 75.)
Input sets and output sets are not sets of instances in the mathematical sense but collections, because the same instance can occur multiple times in them. In other words: A collection contains values (which can be instances of structured types or primitive values), possibly with repetitions. The occurrences of the values in the collection form a set in the mathematical sense. The cardinality of a collection is the total number of occurrences in it. When this text describes a transformation algorithmically and stipulates that certain steps are carried out for each occurrence in a collection, this means that the steps are carried out multiple times for the same value if it occurs multiple times in the collection.
A collection addressed by the resource path is returned by the service either as an ordered collection OData-Protocol, section 11.4.10 or as an unordered collection. The same applies to collections that are nested in or related to the addressed resource as well as to collections that are the result of evaluating an expression starting with $root
, which occur, for example, as the first parameter of a hierarchical transformation.
Collections are the same if there is a one-to-one correspondence \(f\) between them such that
@@ -1618,7 +1618,7 @@Otherwise, let \(q\) be the portion of \(p\) up to and including the last navigation property, if any, and any type-cast segment that immediately follows, and let \(r\) be the remainder, if any, of \(p\) that contains no navigation properties, such that \(p\) equals the concatenated path \(q⁄r\). The aggregate transformation considers each entity reached via the path \(q\) exactly once. To this end, using the \(\Gamma\) notation:
Then, if \(r\) is empty, let \(A=E\), otherwise let \(A=\Gamma(E,r)\), this consists of instances of structured types or primitive values, possibly with repetitions.
@@ -1822,7 +1822,7 @@groupby
The groupby
transformation takes one or two parameters where the second is a list of set transformations, separated by forward slashes to express that they are consecutively applied. If the second parameter is not specified, it defaults to a single transformation whose output set consists of a single instance of the input type without properties and without entity id.
In its simplest form the first parameter of groupby
specifies the grouping properties, a comma-separated parenthesized list \(G\) of one or more data aggregation paths with single-valued segments. The same path SHOULD NOT appear more than once; redundant property paths MAY be considered valid, but MUST NOT alter the meaning of the request. Navigation properties and stream properties specified in grouping properties are expanded by default (see example 73).
In its simplest form the first parameter of groupby
specifies the grouping properties, a comma-separated parenthesized list \(G\) of one or more data aggregation paths with single-valued segments. The same path SHOULD NOT appear more than once; redundant property paths MAY be considered valid, but MUST NOT alter the meaning of the request. Navigation properties and stream properties specified in grouping properties are expanded by default (see example 72).
The algorithmic description of this transformation makes use of the following definitions: Let \(u[q]\) denote the value of a structural or navigation property \(q\) in an instance \(u\). A path \(p_1\) is called a prefix of a path \(p\) if there is a non-empty path \(p_2\) such that \(p\) equals the concatenated path \(p_1/p_2\). Let \(e\) denote the empty path.
The output set of the groupby
transformation is constructed in five steps.
rolluprecursive
, and in hierarchy functions. The same entity can serve as nodes in different recursive hierarchies, given different qualifiers.
A root node is a node without parent nodes. A recursive hierarchy can have one or more root nodes. A node is a child node of its parent nodes, a node without child nodes is a leaf node. Two nodes with a common parent node are sibling nodes and so are two root nodes.
The descendants with maximum distance \(d≥1\) of a node are its child nodes and, if \(d>1\), the descendants of these child nodes with maximum distance \(d-1\). The descendants are the descendants with maximum distance \(d=∞\). A node together with its descendants forms a sub-hierarchy of the hierarchy.
-The ancestors with maximum distance \(d≥1\) of a node are its parent nodes and, if \(d>1\), the ancestors of these parent nodes with maximum distance \(d-1\). The ancestors are the ancestors with maximum distance \(d=∞\).
-The term UpPath
can be used in hierarchical result sets to associate with each instance one of its ancestors, one ancestor of that ancestor and so on. The term Cycle
is used to tag instances in hierarchical result sets that are their own ancestor and therefore part of a cycle. These instance annotations are introduced in section 6.2.2.
The ancestors with maximum distance \(d≥1\) of a node are its parent nodes and, if \(d>1\), the ancestors of these parent nodes with maximum distance \(d-1\). The ancestors are the ancestors with maximum distance \(d=∞\). The ParentNavigationProperty
MUST be such that no node is an ancestor of itself.
The term UpPath
can be used in hierarchical result sets to associate with each instance one of its ancestors, one ancestor of that ancestor and so on. This instance annotation is introduced in section 6.2.2.
For testing the position of a given entity in a recursive hierarchy, the Aggregation vocabulary OData-VocAggr defines unbound functions. These have
Here paths are considered equal if their non-type-cast segments refer to the same model elements when evaluated relative to the input set (see example 69).
+Here paths are considered equal if their non-type-cast segments refer to the same model elements when evaluated relative to the input set (see example 68).
The function \(a(u,t,x)\) takes an instance, a path and another instance as arguments and is defined recursively as follows:
(See example 113.)
+(See example 112.)
traverse
The algorithm is first given for the standard case where RecursiveHierarchy/ParentNavigationProperty
is single-valued and the optional parameter \(S\) is not specified. In this standard case, start nodes are root nodes and \(σ(x)\) is computed exactly once for every node \(x\), as part of the recursive formula for \(R(x)\) given below. The general case follows later.
Let \(r_1,…,r_n\) be a sequence of the start nodes in \(H'\) preserving the order of \(H'\) stable-sorted by \(o\). Then the transformation \({\tt traverse}(H,Q,p,h,o)\) is defined as equivalent to \[{\tt concat}(R(r_1),…,R(r_n)).\]
@@ -2901,7 +2901,7 @@traverse
In the general case, the recursive algorithm can reach a node \(x\) multiple times, via different parents or ancestors, or because \(x\) is a start node and a descendant of another start node. Then the algorithm computes \(R(x)\) and hence \(σ(x)\) multiple times. In order to distinguish these computation results, information about the ancestors up to the start node is injected into each \(σ(x)\) by annotating \(x\) differently before each \(σ(x)\) is computed. On the other hand, certain nodes can be unreachable from any start node, these are called orphans of the traversal (see example 118).
+In the general case, the recursive algorithm can reach a node \(x\) multiple times, via different parents or ancestors, or because \(x\) is a start node and a descendant of another start node. Then the algorithm computes \(R(x)\) and hence \(σ(x)\) multiple times. In order to distinguish these computation results, information about the ancestors up to the start node is injected into each \(σ(x)\) by annotating \(x\) differently before each \(σ(x)\) is computed. On the other hand, certain nodes can be unreachable from any start node, these are called orphans of the traversal (see example 117).
More precisely, in the general case every node \(y\) is annotated with the term UpPath
from the Aggregation
vocabulary OData-VocAggr. The annotation has \(Q\) as qualifier and the annotation value is a collection of string values of node identifiers. The first member of that collection is the node identifier of the parent node \(x\) such that \(R(y)\) appears on the right-hand side of the recursive formula for \(R(x)\). The following members are the members of the Aggregation.UpPath
collection of \(x\). Every instance in the output set of traverse
is related to one node with Aggregation.UpPath
annotation. Start nodes appear annotated with an empty collection.
⚠ Example 64: A sales organization Atlantis with two parents US and EMEA would occur twice in the result of a traverse
transformation:
Given a node \(x\) annotated with \(x/@\hbox{\tt Aggregation.UpPath}\#Q=[x_1,…,x_d]\), where \(d≥0\), and given a child \(y\) of \(x\), let \(ρ(y,x)\) be the node \(y\) with the annotation \[ρ(y,x)/@\hbox{\tt Aggregation.UpPath}\#Q=[{\tt cast}(x[q],\hbox{\tt Edm.String}),x_1,…,x_d].\]
-If the string value of the node identifier of \(y\) is among the values on the right-hand side of the previous equation, a cycle has been detected and \(ρ(y,x)\) is additionally annotated with \[ρ(y,x)/@\hbox{\tt Aggregation.Cycle}\#Q={\tt true}.\] The algorithm does then not process the children of this node again.
-⚠ Example 65: If the child of Atlantis is also a parent of Atlantis:
-GET /service/SalesOrganizations?$apply=
- /traverse($root/SalesOrganizations,MultiParentHierarchy,ID,preorder)
-results in
-{
-"@context": "$metadata#SalesOrganizations",
- "value": [
- ...
- { "ID": "Atlantis", "Name": "Atlantis",
- "@Aggregation.UpPath#MultiParentHierarchy":
- [ "US", "Sales" ] },
- { "ID": "AtlantisChild", "Name": "Child of Atlantis",
- "@Aggregation.UpPath#MultiParentHierarchy":
- [ "Atlantis", "US", "Sales" ] },
- { "ID": "Atlantis", "Name": "Atlantis",
- "@Aggregation.Cycle#MultiParentHierarchy": true,
- "@Aggregation.UpPath#MultiParentHierarchy":
- [ "AtlantisChild", "Atlantis", "US", "Sales" ] },
- ...
- { "ID": "Atlantis", "Name": "Atlantis",
- "@Aggregation.UpPath#MultiParentHierarchy":
- [ "EMEA", "Sales" ] },
- { "ID": "AtlantisChild", "Name": "Child of Atlantis",
- "@Aggregation.UpPath#MultiParentHierarchy":
- [ "Atlantis", "EMEA", "Sales" ] },
- { "ID": "Atlantis", "Name": "Atlantis",
- "@Aggregation.Cycle#MultiParentHierarchy": true,
- "@Aggregation.UpPath#MultiParentHierarchy":
- [ "AtlantisChild", "Atlantis", "EMEA", "Sales" ] },
- ...
- ]
- }
Like structural and navigation properties, these instance annotations are considered part of the node \(x\) and are copied over to \(σ(x)\). For them to be included in the transformation \(\Pi_G(σ(x))\), an additional step is inserted between steps 2 and 3 of the function \(a_G(u,s,p)\) as defined in the simple grouping section:
Aggregation.UpPath
or Aggregation.Cycle
and qualifier \(Q\), copy these annotations from \(s\) to \(u\).Aggregation.UpPath
and qualifier \(Q\), copy this annotation from \(s\) to \(u\).Recall that instance annotations never appear in data aggregation paths or aggregatable expressions. They are not considered when determining whether instances of structured types are the same, they do not cause conflicting representations and are absent from merged representations.
-Let \(r_1,…,r_n\) be the start nodes in \(H'\) as above, then the transformation \({\tt traverse}(H,Q,p,h,S,o)\) is defined as equivalent to \[{\tt concat}(R(ρ_0(r_1)),…,R(ρ_0(r_n))\] where the function \(R(x)\) takes as argument a node with optional Aggregation.UpPath
and Aggregation.Cycle
annotations. With \(F(x)\) as above, if \(x\) is annotated with Aggregation.Cycle
as true, then \[R(x)=F(x)/\Pi_G(σ(x)).\]
Otherwise, with \(c_1,…,c_m\) as above, if \(h={\tt preorder}\), then \[R(x)={\tt concat}(F(x)/\Pi_G(σ(x)),R(ρ(c_1,x)),…,R(ρ(c_m,x))),\] and if \(h={\tt postorder}\), then \[R(x)={\tt concat}(R(ρ(c_1,x)),…,R(ρ(c_m,x)),F(x)/\Pi_G(σ(x))).\]
+Let \(r_1,…,r_n\) be the start nodes in \(H'\) as above, then the transformation \({\tt traverse}(H,Q,p,h,S,o)\) is defined as equivalent to \[{\tt concat}(R(ρ_0(r_1)),…,R(ρ_0(r_n))\] where the function \(R(x)\) takes as argument a node with optional Aggregation.UpPath
annotation. With \(F(x)\) and \(c_1,…,c_m\) as above, if \(h={\tt preorder}\), then \[R(x)={\tt concat}(F(x)/\Pi_G(σ(x)),R(ρ(c_1,x)),…,R(ρ(c_m,x))),\] and if \(h={\tt postorder}\), then \[R(x)={\tt concat}(R(ρ(c_1,x)),…,R(ρ(c_m,x)),F(x)/\Pi_G(σ(x))).\]
In the general case, servers MUST include the Aggregation.UpPath
annotations in the result of $apply
but MAY omit them if RecursiveHierarchy/ParentNavigationProperty
is single-valued and all start nodes are root nodes.
If RecursiveHierarchy/ParentNavigationProperty
is collection-valued but the parent collection never contains more than one parent and the optional parameter \(S\) is not specified, then the result is effectively like in the standard case, except for the presence of the Aggregation.UpPath
annotations.
rolluprecursive
Let \(T\) be a transformation sequence, \(P_1\) stand in for zero or more property paths and \(P_2\) for zero or more rollup
or rolluprecursive
operators or property paths. The transformation \({\tt groupby}((P_1,{\tt rolluprecursive}(H,Q,p,S),P_2),T)\) is computed by the following algorithm, which invokes itself recursively if the number of rolluprecursive
operators in the first argument of the groupby
transformation, which is called \(M\), is greater than one. Let \(N\) be the recursion depth of the algorithm, starting with 1.
The rolluprecursive
algorithm:
A property \(χ_N\) appears in the algorithm, but is not present in the output set. It is explained later (see example 67). \(Z_N\) is a transformation whose output set is its input set with property \(χ_N\) removed.
-Let \(x_1,…,x_n\) be the nodes in \(H'\), possibly with repetitions. If the optional transformation sequence \(S\) ends with a traverse
transformation, as in example 119, the sequence \(x_1,…,x_n\) MUST have the preorder or postorder established by that traversal, otherwise its order is arbitrary. Then the transformation \({\tt groupby}((P_1,{\tt rolluprecursive}(H,Q,p,S),P_2),T)\) is defined as equivalent to \[{\tt concat}(R(x_1),…,R(x_n))\] with no order defined on the output set unless \(S\) ends with a traverse
transformation.
A property \(χ_N\) appears in the algorithm, but is not present in the output set. It is explained later (see example 66). \(Z_N\) is a transformation whose output set is its input set with property \(χ_N\) removed.
+Let \(x_1,…,x_n\) be the nodes in \(H'\), possibly with repetitions. If the optional transformation sequence \(S\) ends with a traverse
transformation, as in example 118, the sequence \(x_1,…,x_n\) MUST have the preorder or postorder established by that traversal, otherwise its order is arbitrary. Then the transformation \({\tt groupby}((P_1,{\tt rolluprecursive}(H,Q,p,S),P_2),T)\) is defined as equivalent to \[{\tt concat}(R(x_1),…,R(x_n))\] with no order defined on the output set unless \(S\) ends with a traverse
transformation.
\(R(x)\) is a transformation that processes the entire sub-hierarchy rooted at \(x\), which is the output set of \(F(x)\). The output set of \(R(x)\) is a collection of aggregated instances for all rollup results.
If at least one of \(P_1\) or \(P_2\) is non-empty, then \[R(x)=F(x)/{\tt compute}(x{\tt\ as\ }χ_N)/{\tt groupby}((P_1,P_2),T/Z_N/\Pi_G(σ(x)))\] with no order defined on the output set.
The property \(χ_N=x\) is present during the evaluation of \(T\), but not afterwards. If \(P_2\) contains a rolluprecursive
operator, the evaluation of the formula involves a recursive invocation (with \(N\) increased by 1) of the rolluprecursive
algorithm.
Otherwise if \(P_1\) and \(P_2\) are empty, then \[R(x)=F(x)/{\tt compute}(x{\tt\ as\ }χ_N)/T/Z_N/\Pi_G(σ(x))\] with no order defined on the output set.
\(F(x)\) is defined as follows: If \(p\) contains only single-valued segments, then \[\matrix{ F(x)={\tt filter}(\hbox{\tt Aggregation.isdescendant}(\hfill\\ \quad {\tt HierarchyNodes}=H,\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \quad {\tt Node}=p,\;{\tt Ancestor}=x[q],\;{\tt IncludeSelf}={\tt true})).\hfill }\]
-Otherwise \(p=p_1/…/p_k/r\) with \(k≥1\) and \[\matrix{ F(x)={\tt filter}(\hfill\\ \hskip1pc p_1/{\tt any}(y_1:\hfill\\ \hskip2pc y_1/p_2/{\tt any}(y_2:\hfill\\ \hskip3pc ⋱\hfill\\ \hskip4pc y_{k-1}/p_k/{\tt any}(y_k:\hfill\\ \hskip5pc \hbox{\tt Aggregation.isdescendant}(\hfill\\ \hskip6pc {\tt HierarchyNodes}=H,\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \hskip6pc {\tt Node}=y_k/r,\;{\tt Ancestor}=x[q],\;{\tt IncludeSelf}={\tt true}\hfill\\ \hskip5pc )\hfill\\ \hskip4pc )\hfill\\ \hskip3pc ⋰\hfill\\ \hskip2pc )\hfill\\ \hskip1pc )\hfill\\ )\hfill }\] where \(y_1,…,y_k\) denote lambdaVariableExpr
s and \({}/r\) may be absent. (See example 114 for a case with \(k=1\).)
Otherwise \(p=p_1/…/p_k/r\) with \(k≥1\) and \[\matrix{ F(x)={\tt filter}(\hfill\\ \hskip1pc p_1/{\tt any}(y_1:\hfill\\ \hskip2pc y_1/p_2/{\tt any}(y_2:\hfill\\ \hskip3pc ⋱\hfill\\ \hskip4pc y_{k-1}/p_k/{\tt any}(y_k:\hfill\\ \hskip5pc \hbox{\tt Aggregation.isdescendant}(\hfill\\ \hskip6pc {\tt HierarchyNodes}=H,\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \hskip6pc {\tt Node}=y_k/r,\;{\tt Ancestor}=x[q],\;{\tt IncludeSelf}={\tt true}\hfill\\ \hskip5pc )\hfill\\ \hskip4pc )\hfill\\ \hskip3pc ⋰\hfill\\ \hskip2pc )\hfill\\ \hskip1pc )\hfill\\ )\hfill }\] where \(y_1,…,y_k\) denote lambdaVariableExpr
s and \({}/r\) may be absent. (See example 113 for a case with \(k=1\).)
Informatively speaking, the effect of the algorithm can be summarized as follows: If \(M≥1\) and \(\hat F_N(x)\) denotes the collection of all instances that are related to a node \(x\) as determined by \(F(x)\) in the recursive hierarchy of the \(N\)-th rolluprecursive
operator, then \(T\) is applied to each of the intersections of \(\hat F_1(χ_1),…,\hat F_M(χ_M)\), as \(χ_N\) runs over all nodes of the \(N\)-th recursive hierarchy for \(1≤N≤M\). Into the instances of the resulting output sets the \(\Pi_G\) transformations inject information about the nodes \(χ_1,…,χ_M\).
Example 66: Total number of sub-organizations for all organizations in the hierarchy defined in Hierarchy Examples with \(p=q={\tt ID}\) (case 1 of the definition of \(σ(x)\)). In this case \(\Pi_G(σ(x))\) writes back the entire node into the output set of \(T\), aggregates must have an alias to avoid overwriting by a property of the node with the same name.
+Example 65: Total number of sub-organizations for all organizations in the hierarchy defined in Hierarchy Examples with \(p=q={\tt ID}\) (case 1 of the definition of \(σ(x)\)). In this case \(\Pi_G(σ(x))\) writes back the entire node into the output set of \(T\), aggregates must have an alias to avoid overwriting by a property of the node with the same name.
GET /service/SalesOrganizations?$apply=
groupby((rolluprecursive(
$root/SalesOrganizations,SalesOrgHierarchy,ID)),
@@ -2999,28 +2963,28 @@
results in
-{
-"@context":
- "$metadata#SalesOrganizations(ID,Name,SubOrgCnt,Superordinate(ID))",
- "value": [
- { "ID": "US West", "Name": "US West",
- "SubOrgCount": 0, "Superordinate": { "ID": "US" } },
- { "ID": "US East", "Name": "US East",
- "SubOrgCount": 0, "Superordinate": { "ID": "US" } },
- { "ID": "US", "Name": "US",
- "SubOrgCount": 2, "Superordinate": { "ID": "Sales" } },
- { "ID": "EMEA Central", "Name": "EMEA Central",
- "SubOrgCount": 0, "Superordinate": { "ID": "EMEA" } },
- { "ID": "EMEA", "Name": "EMEA",
- "SubOrgCount": 1, "Superordinate": { "ID": "Sales" } },
- { "ID": "Sales", "Name": "Sales",
- "SubOrgCount": 5, "Superordinate": null }
- ]
- }
{
+"@context":
+ "$metadata#SalesOrganizations(ID,Name,SubOrgCnt,Superordinate(ID))",
+ "value": [
+ { "ID": "US West", "Name": "US West",
+ "SubOrgCount": 0, "Superordinate": { "ID": "US" } },
+ { "ID": "US East", "Name": "US East",
+ "SubOrgCount": 0, "Superordinate": { "ID": "US" } },
+ { "ID": "US", "Name": "US",
+ "SubOrgCount": 2, "Superordinate": { "ID": "Sales" } },
+ { "ID": "EMEA Central", "Name": "EMEA Central",
+ "SubOrgCount": 0, "Superordinate": { "ID": "EMEA" } },
+ { "ID": "EMEA", "Name": "EMEA",
+ "SubOrgCount": 1, "Superordinate": { "ID": "Sales" } },
+ { "ID": "Sales", "Name": "Sales",
+ "SubOrgCount": 5, "Superordinate": null }
+ ]
+ }
The value of the property \(χ_N\) in the rolluprecursive
algorithm is the node \(x\) at recursion level \(N\). In a common expression, \(χ_N\) cannot be accessed by its name, but can only be read as the return value of the unbound function \({\tt rollupnode}({\tt Position}=N)\) defined in the Aggregation
vocabulary OData-VocAggr, with \(1≤N≤M\), and only during the application of the transformation sequence \(T\) in the formula for \(R(x)\) above (the function is undefined otherwise). If \(N=1\), the Position
parameter can be omitted.
⚠ Example 67: Total sales amounts per organization, both including and excluding sub-organizations, in the US sub-hierarchy defined in Hierarchy Examples with \(p=p'/q={\tt SalesOrganization}/{\tt ID}\) and \(p'={\tt SalesOrganization}\) (case 2 of the definition of \(σ(x)\)). The Boolean expression \(p'\hbox{\tt\ eq Aggregation.rollupnode}()\) is true for sales in the organization for which the aggregate is computed, but not for sales in sub-organizations.
+⚠ Example 66: Total sales amounts per organization, both including and excluding sub-organizations, in the US sub-hierarchy defined in Hierarchy Examples with \(p=p'/q={\tt SalesOrganization}/{\tt ID}\) and \(p'={\tt SalesOrganization}\) (case 2 of the definition of \(σ(x)\)). The Boolean expression \(p'\hbox{\tt\ eq Aggregation.rollupnode}()\) is true for sales in the organization for which the aggregate is computed, but not for sales in sub-organizations.
GET /service/Sales?$apply=groupby(
(rolluprecursive(
$root/SalesOrganizations,
@@ -3034,24 +2998,24 @@
results in
-{
-"@context": "$metadata#Sales(SalesOrganization(),
- TotalAmountIncl,TotalAmountExcl)",
-"value": [
- { "SalesOrganization": { "ID": "US West", "Name": "US West" },
- "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 7,
- "TotalAmountExcl@type": "Decimal" ,"TotalAmountExcl": 7 },
- { "SalesOrganization": { "ID": "US", "Name": "US" },
- "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 19,
- "TotalAmountExcl": null },
- { "SalesOrganization": { "ID": "US East", "Name": "US East" },
- "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 12,
- "TotalAmountExcl@type": "Decimal", "TotalAmountExcl": 12 }
- ]
- }
⚠ Example 68: When requesting a sub-hierarchy consisting of the US East sales organization and its ancestors, the total sales amounts can either include the descendants outside this sub-hierarchy ("actual totals") or can exclude them ("visual totals").
+{
+"@context": "$metadata#Sales(SalesOrganization(),
+ TotalAmountIncl,TotalAmountExcl)",
+"value": [
+ { "SalesOrganization": { "ID": "US West", "Name": "US West" },
+ "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 7,
+ "TotalAmountExcl@type": "Decimal" ,"TotalAmountExcl": 7 },
+ { "SalesOrganization": { "ID": "US", "Name": "US" },
+ "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 19,
+ "TotalAmountExcl": null },
+ { "SalesOrganization": { "ID": "US East", "Name": "US East" },
+ "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 12,
+ "TotalAmountExcl@type": "Decimal", "TotalAmountExcl": 12 }
+ ]
+ }
⚠ Example 67: When requesting a sub-hierarchy consisting of the US East sales organization and its ancestors, the total sales amounts can either include the descendants outside this sub-hierarchy ("actual totals") or can exclude them ("visual totals").
Actual totals are computed when rolluprecursive
is restricted to the sub-hierarchy by setting the optional parameter \(S\) to an ancestors
transformation:
GET /service/Sales?$apply=groupby((rolluprecursive(
$root/SalesOrganizations,SalesOrgHierarchy,SalesOrganization/ID,
@@ -3059,17 +3023,17 @@
results in
-{
-"@context": "$metadata#Sales(SalesOrganization(),Total)",
- "value": [
- { "SalesOrganization": { "ID": "US East", "Name": "US East" },
- "Total@type": "Decimal", "Total": 12 },
- { "SalesOrganization": { "ID": "US", "Name": "US" },
- "Total@type": "Decimal", "Total": 19 },
- { "SalesOrganization": { "ID": "Sales", "Name": "Sales" },
- "Total@type": "Decimal", "Total": 24 }
- ]
- }
{
+"@context": "$metadata#Sales(SalesOrganization(),Total)",
+ "value": [
+ { "SalesOrganization": { "ID": "US East", "Name": "US East" },
+ "Total@type": "Decimal", "Total": 12 },
+ { "SalesOrganization": { "ID": "US", "Name": "US" },
+ "Total@type": "Decimal", "Total": 19 },
+ { "SalesOrganization": { "ID": "Sales", "Name": "Sales" },
+ "Total@type": "Decimal", "Total": 24 }
+ ]
+ }
Visual totals are computed when the ancestors
transformation is additionally carried out before the rolluprecursive
:
GET /service/Sales?$apply=
ancestors($root/SalesOrganizations,SalesOrgHierarchy,SalesOrganization/ID,
@@ -3080,37 +3044,37 @@
results in
-{
-"@context": "$metadata#Sales(SalesOrganization(),Total)",
- "value": [
- { "SalesOrganization": { "ID": "US East", "Name": "US East" },
- "Total@type": "Decimal", "Total": 12 },
- { "SalesOrganization": { "ID": "US", "Name": "US" },
- "Total@type": "Decimal", "Total": 12 },
- { "SalesOrganization": { "ID": "Sales", "Name": "Sales" },
- "Total@type": "Decimal", "Total": 12 }
- ]
- }
{
+"@context": "$metadata#Sales(SalesOrganization(),Total)",
+ "value": [
+ { "SalesOrganization": { "ID": "US East", "Name": "US East" },
+ "Total@type": "Decimal", "Total": 12 },
+ { "SalesOrganization": { "ID": "US", "Name": "US" },
+ "Total@type": "Decimal", "Total": 12 },
+ { "SalesOrganization": { "ID": "Sales", "Name": "Sales" },
+ "Total@type": "Decimal", "Total": 12 }
+ ]
+ }
⚠ Example 69: Although \(p={\tt ID}\) and \(q={\tt ID}\), they are not equal in the sense of case 1, because they are evaluated relative to different entity sets. Hence, this is an example of case 3 of the definition of \(σ(x)\), where no Sales/ID
matches a SalesOrganizations/ID
, that is, all \(F(x)\) have empty output sets.
⚠ Example 68: Although \(p={\tt ID}\) and \(q={\tt ID}\), they are not equal in the sense of case 1, because they are evaluated relative to different entity sets. Hence, this is an example of case 3 of the definition of \(σ(x)\), where no Sales/ID
matches a SalesOrganizations/ID
, that is, all \(F(x)\) have empty output sets.
GET /service/Sales?$apply=
groupby((rolluprecursive(
$root/SalesOrganizations,SalesOrgHierarchy,ID))),
aggregate(Amount with sum as TotalAmount))
results in
-{
-"@context": "$metadata#Sales(SalesOrganization(),TotalAmount)",
- "value": [
- { "SalesOrganization": { "ID": "Sales", "Name": "Corporate Sales" },
- "TotalAmount": null },
- { "SalesOrganization": { "ID": "EMEA", "Name": "EMEA" },
- "TotalAmount": null },
- { "SalesOrganization": { "ID": "US", "Name": "US" },
- "TotalAmount": null },
- ...
- ]
- }
{
+"@context": "$metadata#Sales(SalesOrganization(),TotalAmount)",
+ "value": [
+ { "SalesOrganization": { "ID": "Sales", "Name": "Corporate Sales" },
+ "TotalAmount": null },
+ { "SalesOrganization": { "ID": "EMEA", "Name": "EMEA" },
+ "TotalAmount": null },
+ { "SalesOrganization": { "ID": "US", "Name": "US" },
+ "TotalAmount": null },
+ ...
+ ]
+ }
Grouping without specifying a set transformation returns the distinct combination of the grouping properties.
Example 70:
+Example 69:
GET /service/Customers?$apply=groupby((Name))
results in
-{
-"@context": "$metadata#Customers(Name)",
- "value": [
- { "Name": "Luc" },
- { "Name": "Joe" },
- { "Name": "Sue" }
- ]
- }
{
+"@context": "$metadata#Customers(Name)",
+ "value": [
+ { "Name": "Luc" },
+ { "Name": "Joe" },
+ { "Name": "Sue" }
+ ]
+ }
Note that "Sue" appears only once although the customer base contains two different Sues.
Aggregation is also possible across related entities.
Example 71: customers that bought something
+Example 70: customers that bought something
GET /service/Sales?$apply=groupby((Customer/Name))
results in
-{
-"@context": "$metadata#Sales(Customer(Name))",
- "value": [
- { "Customer": { "Name": "Joe" } },
- { "Customer": { "Name": "Sue" } }
- ]
- }
{
+"@context": "$metadata#Sales(Customer(Name))",
+ "value": [
+ { "Customer": { "Name": "Joe" } },
+ { "Customer": { "Name": "Sue" } }
+ ]
+ }
Since groupby
expands navigation properties in grouping properties by default, this is the same result as if the request would include a $expand=Customer($select=Name)
. The groupby
removes all other properties.
Note that "Luc" does not appear in the aggregated result as he hasn't bought anything and therefore there are no sales entities that refer/navigate to Luc.
However, even though both Sues bought products, only one "Sue" appears in the aggregate result. Including properties that guarantee the right level of uniqueness in the grouping can repair that.
Example 72:
+Example 71:
GET /service/Sales?$apply=groupby((Customer/Name,Customer/ID))
results in
-{
-"@context": "$metadata#Sales(Customer(Name,ID))",
- "value": [
- { "Customer": { "Name": "Joe", "ID": "C1" } },
- { "Customer": { "Name": "Sue", "ID": "C2" } },
- { "Customer": { "Name": "Sue", "ID": "C3" } }
- ]
- }
{
+"@context": "$metadata#Sales(Customer(Name,ID))",
+ "value": [
+ { "Customer": { "Name": "Joe", "ID": "C1" } },
+ { "Customer": { "Name": "Sue", "ID": "C2" } },
+ { "Customer": { "Name": "Sue", "ID": "C3" } }
+ ]
+ }
This could also have been formulated as
GET /service/Sales?$apply=groupby((Customer))
&$expand=Customer($select=Name,ID)
Example 73: Grouping by navigation property Customer
Example 72: Grouping by navigation property Customer
GET /service/Sales?$apply=groupby((Customer))
results in
-{
-"@context": "$metadata#Sales(Customer())",
- "value": [
- { "Customer": { "ID": "C1", "Name": "Joe", "Country": "USA" } },
- { "Customer": { "ID": "C2", "Name": "Sue", "Country": "USA" } },
- { "Customer": { "ID": "C3", "Name": "Sue", "Country": "Netherlands" } }
- ]
- }
{
+"@context": "$metadata#Sales(Customer())",
+ "value": [
+ { "Customer": { "ID": "C1", "Name": "Joe", "Country": "USA" } },
+ { "Customer": { "ID": "C2", "Name": "Sue", "Country": "USA" } },
+ { "Customer": { "ID": "C3", "Name": "Sue", "Country": "Netherlands" } }
+ ]
+ }
Example 74: the first question in the motivating example in section 2.3, which customers bought which products, can now be expressed as
+Example 73: the first question in the motivating example in section 2.3, which customers bought which products, can now be expressed as
GET /service/Sales?$apply=groupby((Customer/Name,Customer/ID,Product/Name))
and results in
-{
-"@context": "$metadata#Sales(Customer(Name,ID),Product(Name))",
- "value": [
- { "Customer": { "Name": "Joe", "ID": "C1" },
- "Product": { "Name": "Coffee"} },
- { "Customer": { "Name": "Joe", "ID": "C1" },
- "Product": { "Name": "Paper" } },
- { "Customer": { "Name": "Joe", "ID": "C1" },
- "Product": { "Name": "Sugar" } },
- { "Customer": { "Name": "Sue", "ID": "C2" },
- "Product": { "Name": "Coffee"} },
- { "Customer": { "Name": "Sue", "ID": "C2" },
- "Product": { "Name": "Paper" } },
- { "Customer": { "Name": "Sue", "ID": "C3" },
- "Product": { "Name": "Paper" } },
- { "Customer": { "Name": "Sue", "ID": "C3" },
- "Product": { "Name": "Sugar" } }
- ]
- }
⚠ Example 75: grouping by properties of subtypes
+{
+"@context": "$metadata#Sales(Customer(Name,ID),Product(Name))",
+ "value": [
+ { "Customer": { "Name": "Joe", "ID": "C1" },
+ "Product": { "Name": "Coffee"} },
+ { "Customer": { "Name": "Joe", "ID": "C1" },
+ "Product": { "Name": "Paper" } },
+ { "Customer": { "Name": "Joe", "ID": "C1" },
+ "Product": { "Name": "Sugar" } },
+ { "Customer": { "Name": "Sue", "ID": "C2" },
+ "Product": { "Name": "Coffee"} },
+ { "Customer": { "Name": "Sue", "ID": "C2" },
+ "Product": { "Name": "Paper" } },
+ { "Customer": { "Name": "Sue", "ID": "C3" },
+ "Product": { "Name": "Paper" } },
+ { "Customer": { "Name": "Sue", "ID": "C3" },
+ "Product": { "Name": "Sugar" } }
+ ]
+ }
⚠ Example 74: grouping by properties of subtypes
GET /service/Products?$apply=groupby((SalesModel.FoodProduct/Rating,
SalesModel.NonFoodProduct/RatingClass))
results in
-{
-"@context": "$metadata#Products(SalesModel.FoodProduct/Rating,
- SalesModel.NonFoodProduct/RatingClass)",
-"value": [
- { "@type": "#SalesModel.FoodProduct", "Rating": 5 },
- { "@type": "#SalesModel.FoodProduct", "Rating": null },
- { "@type": "#SalesModel.NonFoodProduct", "RatingClass": "average" },
- { "@type": "#SalesModel.NonFoodProduct", "RatingClass": null }
- ]
- }
⚠ Example 76: grouping by a property of a subtype
+{
+"@context": "$metadata#Products(SalesModel.FoodProduct/Rating,
+ SalesModel.NonFoodProduct/RatingClass)",
+"value": [
+ { "@type": "#SalesModel.FoodProduct", "Rating": 5 },
+ { "@type": "#SalesModel.FoodProduct", "Rating": null },
+ { "@type": "#SalesModel.NonFoodProduct", "RatingClass": "average" },
+ { "@type": "#SalesModel.NonFoodProduct", "RatingClass": null }
+ ]
+ }
⚠ Example 75: grouping by a property of a subtype
GET /service/Products?$apply=groupby((SalesModel.FoodProduct/Rating))
results in a third group representing entities with no SalesModel.FoodProduct/Rating
, including the SalesModel.NonFoodProduct
s:
{
-"@context": "$metadata#Products(@Core.AnyStructure)",
- "value": [
- { "@type": "#SalesModel.FoodProduct", "Rating": 5 },
- { "@type": "#SalesModel.FoodProduct", "Rating": null },
- { }
- ]
- }
{
+"@context": "$metadata#Products(@Core.AnyStructure)",
+ "value": [
+ { "@type": "#SalesModel.FoodProduct", "Rating": 5 },
+ { "@type": "#SalesModel.FoodProduct", "Rating": null },
+ { }
+ ]
+ }
The client may specify one of the predefined aggregation methods min
, max
, sum
, average
, and countdistinct
, or a custom aggregation method, to aggregate an aggregatable expression. Expressions defining an aggregate method specify an alias. The aggregated values are returned in a dynamic property whose name is determined by the alias.
Example 77:
+Example 76:
GET /service/Products?$apply=groupby((Name),
aggregate(Sales/Amount with sum as Total))
results in
-{
-"@context": "$metadata#Products(Name,Total)",
- "value": [
- { "Name": "Coffee", "Total@type": "Decimal", "Total": 12 },
- { "Name": "Paper", "Total@type": "Decimal", "Total": 8 },
- { "Name": "Pencil", "Total": null },
- { "Name": "Sugar", "Total@type": "Decimal", "Total": 4 }
- ]
- }
{
+"@context": "$metadata#Products(Name,Total)",
+ "value": [
+ { "Name": "Coffee", "Total@type": "Decimal", "Total": 12 },
+ { "Name": "Paper", "Total@type": "Decimal", "Total": 8 },
+ { "Name": "Pencil", "Total": null },
+ { "Name": "Sugar", "Total@type": "Decimal", "Total": 4 }
+ ]
+ }
Note that the base set of the request is Products
, so there is a result item for product Pencil
even though there are no sales items. The input set for the aggregation in the third row is \(I\) consisting of the pencil, \(p=q/r={\tt Sales}/{\tt Amount}\), \(E=\Gamma(I,q)\) is empty and \(A=\Gamma(E,r)\) is also empty. The sum over the empty collection is null.
Example 78: Alternatively, the request could ask for the aggregated amount to be nested inside a clone of Sales
+Example 77: Alternatively, the request could ask for the aggregated amount to be nested inside a clone of Sales
GET /service/Products?$apply=addnested(Sales,
aggregate(Amount with sum as Total) as AggregatedSales)
results in
+{
+"@context": "$metadata#Products(AggregatedSales())",
+ "value": [
+ { "ID": "P2", "Name": "Coffee", "Color": "Brown", "TaxRate": 0.06,
+ "AggregatedSales@context": "#Sales(Total)",
+ "AggregatedSales": [ { "Total@type": "Decimal", "Total": 12 } ] },
+ { "ID": "P3", "Name": "Paper", "Color": "White", "TaxRate": 0.14,
+ "AggregatedSales@context": "#Sales(Total)",
+ "AggregatedSales": [ { "Total@type": "Decimal", "Total": 8 } ] },
+ { "ID": "P4", "Name": "Pencil", "Color": "Black", "TaxRate": 0.14,
+ "AggregatedSales@context": "#Sales(Total)",
+ "AggregatedSales": [ { "Total": null } ] },
+ { "ID": "P1", "Name": "Sugar", "Color": "White", "TaxRate": 0.06,
+ "AggregatedSales@context": "#Sales(Total)",
+ "AggregatedSales": [ { "Total@type": "Decimal", "Total": 4 } ] }
+ ]
+ }
Example 78: To compute the aggregate as a property without nesting, use the aggregate function in $compute
rather than the aggregate transformation in $apply
:
GET /service/Products?$compute=Sales/aggregate(Amount with sum) as Total
+results in
{
-"@context": "$metadata#Products(AggregatedSales())",
+ "@context": "$metadata#Products(*,Total)",
"value": [
{ "ID": "P2", "Name": "Coffee", "Color": "Brown", "TaxRate": 0.06,
- "AggregatedSales@context": "#Sales(Total)",
- "AggregatedSales": [ { "Total@type": "Decimal", "Total": 12 } ] },
- { "ID": "P3", "Name": "Paper", "Color": "White", "TaxRate": 0.14,
- "AggregatedSales@context": "#Sales(Total)",
- "AggregatedSales": [ { "Total@type": "Decimal", "Total": 8 } ] },
- { "ID": "P4", "Name": "Pencil", "Color": "Black", "TaxRate": 0.14,
- "AggregatedSales@context": "#Sales(Total)",
- "AggregatedSales": [ { "Total": null } ] },
- { "ID": "P1", "Name": "Sugar", "Color": "White", "TaxRate": 0.06,
- "AggregatedSales@context": "#Sales(Total)",
- "AggregatedSales": [ { "Total@type": "Decimal", "Total": 4 } ] }
- ]
- }
Example 79: To compute the aggregate as a property without nesting, use the aggregate function in $compute
rather than the aggregate transformation in $apply
:
GET /service/Products?$compute=Sales/aggregate(Amount with sum) as Total
-results in
-{
-"@context": "$metadata#Products(*,Total)",
- "value": [
- { "ID": "P2", "Name": "Coffee", "Color": "Brown", "TaxRate": 0.06,
- "Total@type": "Decimal", "Total": 12 },
- { "ID": "P3", "Name": "Paper", "Color": "White", "TaxRate": 0.14,
- "Total@type": "Decimal", "Total": 8 },
- { "ID": "P4", "Name": "Pencil", "Color": "Black", "TaxRate": 0.14,
- "Total": null },
- { "ID": "P1", "Name": "Sugar", "Color": "White", "TaxRate": 0.06,
- "Total@type": "Decimal", "Total": 4 }
- ]
- }
The expression $it/Sales
refers to the sales of the current product. Without $it
, all sales of all products would be aggregated, because the input collection for the aggregate
function consists of all products.
Example 80: Alternatively, join
could be applied to yield a flat structure:
Example 79: Alternatively, join
could be applied to yield a flat structure:
GET /service/Products?$apply=
join(Sales as TotalSales,aggregate(Amount with sum as Total))
/groupby((Name,TotalSales/Total))
results in
-{
-"@context": "$metadata#Products(Name,TotalSales())",
- "value": [
- { "Name": "Coffee",
- "TotalSales@context": "#Sales(Total)/$entity",
- "TotalSales": { "Total@type": "Decimal", "Total": 12 } },
- { "Name": "Paper",
- "TotalSales@context": "#Sales(Total)/$entity",
- "TotalSales": { "Total@type": "Decimal", "Total": 8 } },
- { "Name": "Sugar",
- "TotalSales@context": "#Sales(Total)/$entity",
- "TotalSales": { "Total@type": "Decimal", "Total": 4 } }
- ]
- }
{
+"@context": "$metadata#Products(Name,TotalSales())",
+ "value": [
+ { "Name": "Coffee",
+ "TotalSales@context": "#Sales(Total)/$entity",
+ "TotalSales": { "Total@type": "Decimal", "Total": 12 } },
+ { "Name": "Paper",
+ "TotalSales@context": "#Sales(Total)/$entity",
+ "TotalSales": { "Total@type": "Decimal", "Total": 8 } },
+ { "Name": "Sugar",
+ "TotalSales@context": "#Sales(Total)/$entity",
+ "TotalSales": { "Total@type": "Decimal", "Total": 4 } }
+ ]
+ }
Applying outerjoin
instead would return an additional entity for product with ID
"Pencil" and TotalSales
having a null value.
Example 81:
+Example 80:
GET /service/Sales?$apply=groupby((Customer/Country),
aggregate(Amount with average as AverageAmount))
results in
-{
-"@context": "$metadata#Sales(Customer(Country),AverageAmount)",
- "value": [
- { "Customer": { "Country": "Netherlands" },
- "AverageAmount": 1.6666666666666667 },
- { "Customer": { "Country": "USA" },
- "AverageAmount": 3.8 }
- ]
- }
{
+"@context": "$metadata#Sales(Customer(Country),AverageAmount)",
+ "value": [
+ { "Customer": { "Country": "Netherlands" },
+ "AverageAmount": 1.6666666666666667 },
+ { "Customer": { "Country": "USA" },
+ "AverageAmount": 3.8 }
+ ]
+ }
Here the AverageAmount
is of type Edm.Double
.
Example 82: $count
after navigation property
Example 81: $count
after navigation property
GET /service/Products?$apply=groupby((Name),
aggregate(Sales/$count as SalesCount))
results in
-{
-"@context": "$metadata#Products(Name,SalesCount)",
- "value": [
- { "Name": "Coffee", "SalesCount@type": "Decimal", "SalesCount": 2 },
- { "Name": "Paper", "SalesCount@type": "Decimal", "SalesCount": 4 },
- { "Name": "Pencil", "SalesCount@type": "Decimal", "SalesCount": 0 },
- { "Name": "Sugar", "SalesCount@type": "Decimal", "SalesCount": 2 }
- ]
- }
{
+"@context": "$metadata#Products(Name,SalesCount)",
+ "value": [
+ { "Name": "Coffee", "SalesCount@type": "Decimal", "SalesCount": 2 },
+ { "Name": "Paper", "SalesCount@type": "Decimal", "SalesCount": 4 },
+ { "Name": "Pencil", "SalesCount@type": "Decimal", "SalesCount": 0 },
+ { "Name": "Sugar", "SalesCount@type": "Decimal", "SalesCount": 2 }
+ ]
+ }
To place the number of instances in a group next to other aggregated values, the aggregate expression $count
can be used:
⚠ Example 83: The effect of the groupby
is to create transient entities and avoid in the result structural properties other than Name
.
⚠ Example 82: The effect of the groupby
is to create transient entities and avoid in the result structural properties other than Name
.
GET /service/Products?$apply=groupby((Name),addnested(Sales,
aggregate($count as SalesCount,
Amount with sum as TotalAmount) as AggregatedSales))
results in
-{
-"@context": "$metadata#Products(Name,AggregatedSales())",
- "value": [
- { "Name": "Coffee",
- "AggregatedSales@context": "#Sales(SalesCount,TotalAmount)",
- "AggregatedSales": [ { "SalesCount": 2,
- "TotalAmount@type": "Decimal", "TotalAmount": 12 } ] },
- { "Name": "Paper",
- "AggregatedSales@context": "#Sales(SalesCount,TotalAmount)",
- "AggregatedSales": [ { "SalesCount": 4,
- "TotalAmount@type": "Decimal", "TotalAmount": 8 } ] },
- { "Name": "Pencil",
- "AggregatedSales@context": "#Sales(SalesCount,TotalAmount)",
- "AggregatedSales": [ { "SalesCount": 0, "TotalAmount": null } ] },
- { "Name": "Sugar",
- "AggregatedSales@context": "#Sales(SalesCount,TotalAmount)",
- "AggregatedSales": [ { "SalesCount": 2,
- "TotalAmount@type": "Decimal", "TotalAmount": 4 } ] }
- ]
- }
{
+"@context": "$metadata#Products(Name,AggregatedSales())",
+ "value": [
+ { "Name": "Coffee",
+ "AggregatedSales@context": "#Sales(SalesCount,TotalAmount)",
+ "AggregatedSales": [ { "SalesCount": 2,
+ "TotalAmount@type": "Decimal", "TotalAmount": 12 } ] },
+ { "Name": "Paper",
+ "AggregatedSales@context": "#Sales(SalesCount,TotalAmount)",
+ "AggregatedSales": [ { "SalesCount": 4,
+ "TotalAmount@type": "Decimal", "TotalAmount": 8 } ] },
+ { "Name": "Pencil",
+ "AggregatedSales@context": "#Sales(SalesCount,TotalAmount)",
+ "AggregatedSales": [ { "SalesCount": 0, "TotalAmount": null } ] },
+ { "Name": "Sugar",
+ "AggregatedSales@context": "#Sales(SalesCount,TotalAmount)",
+ "AggregatedSales": [ { "SalesCount": 2,
+ "TotalAmount@type": "Decimal", "TotalAmount": 4 } ] }
+ ]
+ }
The aggregate
function can not only be used in $compute
but also in $filter
and $orderby
:
Example 84: Products with an aggregated sales volume of ten or more
+Example 83: Products with an aggregated sales volume of ten or more
GET /service/Products?$filter=Sales/aggregate(Amount with sum) ge 10
results in
-{
-"@context": "$metadata#Products",
- "value": [
- { "ID": "P2", "Name": "Coffee", "Color": "Brown", "TaxRate": 0.06 },
- { "ID": "P3", "Name": "Paper", "Color": "White", "TaxRate": 0.14 }
- ]
- }
{
+"@context": "$metadata#Products",
+ "value": [
+ { "ID": "P2", "Name": "Coffee", "Color": "Brown", "TaxRate": 0.06 },
+ { "ID": "P3", "Name": "Paper", "Color": "White", "TaxRate": 0.14 }
+ ]
+ }
Example 85: Customers in descending order of their aggregated sales volume
+Example 84: Customers in descending order of their aggregated sales volume
GET /service/Customers?$orderby=Sales/aggregate(Amount with sum) desc
results in
-{
-"@context": "$metadata#Customers",
- "value": [
- { "ID": "C2", "Name": "Sue", "Country": "USA" },
- { "ID": "C1", "Name": "Joe", "Country": "USA" },
- { "ID": "C3", "Name": "Sue", "Country": "Netherlands" },
- { "ID": "C4", "Name": "Luc", "Country": "France" }
- ]
- }
{
+"@context": "$metadata#Customers",
+ "value": [
+ { "ID": "C2", "Name": "Sue", "Country": "USA" },
+ { "ID": "C1", "Name": "Joe", "Country": "USA" },
+ { "ID": "C3", "Name": "Sue", "Country": "Netherlands" },
+ { "ID": "C4", "Name": "Luc", "Country": "France" }
+ ]
+ }
Example 86: Contribution of each sales to grand total sales amount
+Example 85: Contribution of each sales to grand total sales amount
GET /service/Sales?$compute=Amount divby $these/aggregate(Amount with sum)
as Contribution
results in
-{
-"@context": "$metadata#Sales(*,Contribution)",
- "value": [
- { "ID": 1, "Amount": 1, "Contribution@type": "Decimal",
- "Contribution": 0.0416666666666667 },
- { "ID": 2, "Amount": 2, "Contribution@type": "Decimal",
- "Contribution": 0.0833333333333333 },
- { "ID": 3, "Amount": 4, "Contribution@type": "Decimal",
- "Contribution": 0.1666666666666667 },
- { "ID": 4, "Amount": 8, "Contribution@type": "Decimal",
- "Contribution": 0.3333333333333333 },
- { "ID": 5, "Amount": 4, "Contribution@type": "Decimal",
- "Contribution": 0.1666666666666667 },
- { "ID": 6, "Amount": 2, "Contribution@type": "Decimal",
- "Contribution": 0.0833333333333333 },
- { "ID": 7, "Amount": 1, "Contribution@type": "Decimal",
- "Contribution": 0.0416666666666667 },
- { "ID": 8, "Amount": 2, "Contribution@type": "Decimal",
- "Contribution": 0.0833333333333333 }
- ]
- }
Example 87: Product categories with at least one product having an aggregated sales amount greater than 10
+{
+"@context": "$metadata#Sales(*,Contribution)",
+ "value": [
+ { "ID": 1, "Amount": 1, "Contribution@type": "Decimal",
+ "Contribution": 0.0416666666666667 },
+ { "ID": 2, "Amount": 2, "Contribution@type": "Decimal",
+ "Contribution": 0.0833333333333333 },
+ { "ID": 3, "Amount": 4, "Contribution@type": "Decimal",
+ "Contribution": 0.1666666666666667 },
+ { "ID": 4, "Amount": 8, "Contribution@type": "Decimal",
+ "Contribution": 0.3333333333333333 },
+ { "ID": 5, "Amount": 4, "Contribution@type": "Decimal",
+ "Contribution": 0.1666666666666667 },
+ { "ID": 6, "Amount": 2, "Contribution@type": "Decimal",
+ "Contribution": 0.0833333333333333 },
+ { "ID": 7, "Amount": 1, "Contribution@type": "Decimal",
+ "Contribution": 0.0416666666666667 },
+ { "ID": 8, "Amount": 2, "Contribution@type": "Decimal",
+ "Contribution": 0.0833333333333333 }
+ ]
+ }
Example 86: Product categories with at least one product having an aggregated sales amount greater than 10
GET /service/Categories?$filter=Products/any(
p:p/Sales/aggregate(Amount with sum) gt 10)
results in
-{
-"@context": "$metadata#Categories",
- "value": [
- { "ID": "PG1", "Name": "Food" }
- ]
- }
{
+"@context": "$metadata#Categories",
+ "value": [
+ { "ID": "PG1", "Name": "Food" }
+ ]
+ }
The aggregate
function can also be applied inside $apply
:
Example 88: Sales volume per customer in relation to total volume
+Example 87: Sales volume per customer in relation to total volume
GET /service/Sales?$apply=
groupby((Customer),aggregate(Amount with sum as CustomerAmount))
/compute(CustomerAmount divby $these/aggregate(CustomerAmount with sum)
as Contribution)
&$expand=Customer/$ref
results in
-{
-"@context": "$metadata#Sales(Customer(),CustomerAmount,Contribution)",
- "value": [
- { "Customer": { "@id": "Customers('C1')" },
- "Contribution@type": "Decimal", "Contribution": 0.2916667 },
- { "Customer": { "@id": "Customers('C2')" },
- "Contribution@type": "Decimal", "Contribution": 0.5 },
- { "Customer": { "@id": "Customers('C3')" },
- "Contribution@type": "Decimal", "Contribution": 0.2083333 }
- ]
- }
Example 89: rule 1 for keyword from
applied repeatedly
{
+"@context": "$metadata#Sales(Customer(),CustomerAmount,Contribution)",
+ "value": [
+ { "Customer": { "@id": "Customers('C1')" },
+ "Contribution@type": "Decimal", "Contribution": 0.2916667 },
+ { "Customer": { "@id": "Customers('C2')" },
+ "Contribution@type": "Decimal", "Contribution": 0.5 },
+ { "Customer": { "@id": "Customers('C3')" },
+ "Contribution@type": "Decimal", "Contribution": 0.2083333 }
+ ]
+ }
Example 88: rule 1 for keyword from
applied repeatedly
GET /service/Sales?$apply=aggregate(Amount with sum
from Time with average
from Customer/Country with max
@@ -3478,66 +3442,66 @@
7.3 Requesting Expanded Results
-Example 90: Assuming an extension of the data model where Customer
contains an additional collection-valued complex property Addresses
and these contain a single-valued navigation property ResponsibleSalesOrganization
, addnested
can be used to compute a nested dynamic property:
+Example 89: Assuming an extension of the data model where Customer
contains an additional collection-valued complex property Addresses
and these contain a single-valued navigation property ResponsibleSalesOrganization
, addnested
can be used to compute a nested dynamic property:
GET /service/Customers?$apply=
addnested(Addresses/ResponsibleSalesOrganization,
compute(Superordinate/Name as SalesRegion)
as AugmentedSalesOrganization)
results in
-{
-"@context": "$metadata#Customers(Addresses(AugmentedSalesOrganization())",
- "value": [
- { "ID": "C1", "Name": "Joe", "Country": "US",
- "Addresses": [
- { "Locality": "Seattle",
- "AugmentedSalesOrganization":
- { "@context": "#SalesOrganizations/$entity",
- "ID": "US West", "SalesRegion": "US" } },
- { "Locality": "DC",
- "AugmentedSalesOrganization":
- { "@context": "#SalesOrganizations/$entity",
- "ID": "US", "SalesRegion": "Corporate Sales" } },
- ]
- }, ...
- ]
- }
+{
+"@context": "$metadata#Customers(Addresses(AugmentedSalesOrganization())",
+ "value": [
+ { "ID": "C1", "Name": "Joe", "Country": "US",
+ "Addresses": [
+ { "Locality": "Seattle",
+ "AugmentedSalesOrganization":
+ { "@context": "#SalesOrganizations/$entity",
+ "ID": "US West", "SalesRegion": "US" } },
+ { "Locality": "DC",
+ "AugmentedSalesOrganization":
+ { "@context": "#SalesOrganizations/$entity",
+ "ID": "US", "SalesRegion": "Corporate Sales" } },
+ ]
+ }, ...
+ ]
+ }
addnested
transformations can be nested.
-Example 91: nested addnested
transformations
+Example 90: nested addnested
transformations
GET /service/Categories?$apply=
addnested(Products,
addnested(Sales,filter(Amount gt 3) as FilteredSales)
as FilteredProducts)
results in
-{
-"@context": "$metadata#Categories(FilteredProducts()",
- "value": [
- { "ID": "PG1", "Name": "Food",
- "FilteredProducts@context": "#Products(FilteredSales())",
- "FilteredProducts": [
- { "ID": "P1", "Name": "Sugar", "Color": "White",
- "FilteredSales@context": "#Sales",
- "FilteredSales": [] },
- { "ID": "P2", "Name": "Coffee", "Color": "Brown",
- "FilteredSales@context": "#Sales",
- "FilteredSales": [ { "ID": 3, "Amount": 4 },
- { "ID": 4, "Amount": 8 } ] }
- ]
- },
- { "ID": "PG2", "Name": "Non-Food",
- "FilteredProducts@context": "#Products(FilteredSales())",
- "FilteredProducts": [
- { "ID": "P3", "Name": "Paper", "Color": "White",
- "FilteredSales@context": "#Sales",
- "FilteredSales": [ { "ID": 5, "Amount": 4 } ] },
- { "ID": "P4", "Name": "Pencil", "Color": "Black",
- "FilteredSales@context": "#Sales",
- "FilteredSales": [] }
- ]
- }
- ]
- }
+{
+"@context": "$metadata#Categories(FilteredProducts()",
+ "value": [
+ { "ID": "PG1", "Name": "Food",
+ "FilteredProducts@context": "#Products(FilteredSales())",
+ "FilteredProducts": [
+ { "ID": "P1", "Name": "Sugar", "Color": "White",
+ "FilteredSales@context": "#Sales",
+ "FilteredSales": [] },
+ { "ID": "P2", "Name": "Coffee", "Color": "Brown",
+ "FilteredSales@context": "#Sales",
+ "FilteredSales": [ { "ID": 3, "Amount": 4 },
+ { "ID": 4, "Amount": 8 } ] }
+ ]
+ },
+ { "ID": "PG2", "Name": "Non-Food",
+ "FilteredProducts@context": "#Products(FilteredSales())",
+ "FilteredProducts": [
+ { "ID": "P3", "Name": "Paper", "Color": "White",
+ "FilteredSales@context": "#Sales",
+ "FilteredSales": [ { "ID": 5, "Amount": 4 } ] },
+ { "ID": "P4", "Name": "Pencil", "Color": "Black",
+ "FilteredSales@context": "#Sales",
+ "FilteredSales": [] }
+ ]
+ }
+ ]
+ }
Instead of keeping all related entities from navigation properties that addnested
expanded by default, an explicit $expand
controls which of them to include in the response:
GET /service/Categories?$apply=
addnested(Products,
@@ -3547,101 +3511,101 @@ results in the response before without the FilteredSales dynamic navigation properties expanded in the result.
-Example 92: Here only the GroupedSales
are expanded, because they are named in $expand
, the related Product
entity is not:
+Example 91: Here only the GroupedSales
are expanded, because they are named in $expand
, the related Product
entity is not:
GET /service/Customers?$apply=addnested(Sales,
groupby((Product/Name)) as GroupedSales)
&$expand=GroupedSales
results in
-{
-"@context": "$metadata#Customers(GroupedSales())",
- "value": [
- { "ID": "C1", "Name": "Joe", "Country": "USA",
- "GroupedSales@context": "#Sales(@Core.AnyStructure)",
- "GroupedSales": [
- { },
- { },
- { }
- ] },
- { "ID": "C2", "Name": "Sue", "Country": "USA",
- "GroupedSales@context": "#Sales(@Core.AnyStructure)",
- "GroupedSales": [
- { },
- { }
- ] },
- { "ID": "C3", "Name": "Joe", "Country": "Netherlands",
- "GroupedSales@context": "#Sales(@Core.AnyStructure)",
- "GroupedSales": [
- { },
- { }
- ] },
- { "ID": "C4", "Name": "Luc", "Country": "France",
- "GroupedSales@context": "#Sales(@Core.AnyStructure)",
- "GroupedSales": [ ] }
- ]
- }
-
-
-Example 93: use outerjoin
to split up collection-valued navigation properties for grouping
+{
+"@context": "$metadata#Customers(GroupedSales())",
+ "value": [
+ { "ID": "C1", "Name": "Joe", "Country": "USA",
+ "GroupedSales@context": "#Sales(@Core.AnyStructure)",
+ "GroupedSales": [
+ { },
+ { },
+ { }
+ ] },
+ { "ID": "C2", "Name": "Sue", "Country": "USA",
+ "GroupedSales@context": "#Sales(@Core.AnyStructure)",
+ "GroupedSales": [
+ { },
+ { }
+ ] },
+ { "ID": "C3", "Name": "Joe", "Country": "Netherlands",
+ "GroupedSales@context": "#Sales(@Core.AnyStructure)",
+ "GroupedSales": [
+ { },
+ { }
+ ] },
+ { "ID": "C4", "Name": "Luc", "Country": "France",
+ "GroupedSales@context": "#Sales(@Core.AnyStructure)",
+ "GroupedSales": [ ] }
+ ]
+ }
+
+
+Example 92: use outerjoin
to split up collection-valued navigation properties for grouping
GET /service/Customers?$apply=outerjoin(Sales as ProductSales)
/groupby((Country,ProductSales/Product/Name))
returns the different combinations of products sold per country:
-{
-"@context":"$metadata#Customers(Country,ProductSales())",
- "value": [
- { "Country": "Netherlands",
- "ProductSales@context": "#Sales(Product(Name))/$entity",
- "ProductSales": { "Product": { "Name": "Paper" } } },
- { "Country": "Netherlands",
- "ProductSales@context": "#Sales(Product(Name))/$entity",
- "ProductSales": { "Product": { "Name": "Sugar" } } },
- { "Country": "USA",
- "ProductSales@context": "#Sales(Product(Name))/$entity",
- "ProductSales": { "Product": { "Name": "Coffee" } } },
- { "Country": "USA",
- "ProductSales@context": "#Sales(Product(Name))/$entity",
- "ProductSales": { "Product": { "Name": "Paper" } } },
- { "Country": "USA",
- "ProductSales@context": "#Sales(Product(Name))/$entity",
- "ProductSales": { "Product": { "Name": "Sugar" } } },
- { "Country": "France", "ProductSales": null }
- ]
- }
+{
+"@context":"$metadata#Customers(Country,ProductSales())",
+ "value": [
+ { "Country": "Netherlands",
+ "ProductSales@context": "#Sales(Product(Name))/$entity",
+ "ProductSales": { "Product": { "Name": "Paper" } } },
+ { "Country": "Netherlands",
+ "ProductSales@context": "#Sales(Product(Name))/$entity",
+ "ProductSales": { "Product": { "Name": "Sugar" } } },
+ { "Country": "USA",
+ "ProductSales@context": "#Sales(Product(Name))/$entity",
+ "ProductSales": { "Product": { "Name": "Coffee" } } },
+ { "Country": "USA",
+ "ProductSales@context": "#Sales(Product(Name))/$entity",
+ "ProductSales": { "Product": { "Name": "Paper" } } },
+ { "Country": "USA",
+ "ProductSales@context": "#Sales(Product(Name))/$entity",
+ "ProductSales": { "Product": { "Name": "Sugar" } } },
+ { "Country": "France", "ProductSales": null }
+ ]
+ }
7.4 Requesting Custom Aggregates
Custom aggregates are defined through the CustomAggregate
annotation. They can be associated with an entity set, a collection or an entity container.
A custom aggregate can be used by specifying the name of the custom aggregate in the aggregate
clause.
-Example 94:
+Example 93:
GET /service/Sales?$apply=groupby((Customer/Country),
aggregate(Amount with sum as Actual,Forecast))
results in
-{
-"@context": "$metadata#Sales(Customer(Country),Actual,Forecast)",
- "value": [
- { "Customer": { "Country": "Netherlands" },
- "Actual@type": "Decimal", "Actual": 5,
- "Forecast@type": "Decimal", "Forecast": 4 },
- { "Customer": { "Country": "USA" },
- "Actual@type": "Decimal", "Actual": 19,
- "Forecast@type": "Decimal", "Forecast": 21 }
- ]
- }
+{
+"@context": "$metadata#Sales(Customer(Country),Actual,Forecast)",
+ "value": [
+ { "Customer": { "Country": "Netherlands" },
+ "Actual@type": "Decimal", "Actual": 5,
+ "Forecast@type": "Decimal", "Forecast": 4 },
+ { "Customer": { "Country": "USA" },
+ "Actual@type": "Decimal", "Actual": 19,
+ "Forecast@type": "Decimal", "Forecast": 21 }
+ ]
+ }
When associated with an entity set a custom aggregate MAY have the same name as a property of the underlying entity type with the same type as the type returned by the custom aggregate. This is typically done when the aggregate is used as a default aggregate for that property.
-Example 95: A custom aggregate can be defined with the same name as a property of the same type in order to define a default aggregate for that property.
+Example 94: A custom aggregate can be defined with the same name as a property of the same type in order to define a default aggregate for that property.
GET /service/Sales?$apply=groupby((Customer/Country),aggregate(Amount))
results in
-{
-"@context": "$metadata#Sales(Customer(Country),Amount)",
- "value": [
- { "Customer": { "Country": "Netherlands" }, "Amount": 5 },
- { "Customer": { "Country": "USA" }, "Amount": 19 }
- ]
- }
+{
+"@context": "$metadata#Sales(Customer(Country),Amount)",
+ "value": [
+ { "Customer": { "Country": "Netherlands" }, "Amount": 5 },
+ { "Customer": { "Country": "USA" }, "Amount": 19 }
+ ]
+ }
-Example 96: illustrates rule 1 for keyword from
: maximal sales forecast for a product
+Example 95: illustrates rule 1 for keyword from
: maximal sales forecast for a product
GET /service/Sales?$apply=aggregate(Forecast from Product with max
as MaxProductForecast)
is equivalent to
@@ -3650,7 +3614,7 @@
-Example 97: illustrates rule 2 for keyword from
: the forecast is computed in two steps
+Example 96: illustrates rule 2 for keyword from
: the forecast is computed in two steps
GET /service/Sales?$apply=aggregate(Forecast from Product as ProductForecast)
is equivalent to the following (except that the property name is Forecast
instead of ProductForecast
)
GET /service/Sales?$apply=
@@ -3658,7 +3622,7 @@
-Example 98: illustrates rule 1 followed by rule 2 for keyword from
: a forecast based on the average daily forecasts per country
+Example 97: illustrates rule 1 followed by rule 2 for keyword from
: a forecast based on the average daily forecasts per country
GET /service/Sales?$apply=aggregate(Forecast from Time with average
from Customer/Country
as CountryForecast)
@@ -3672,72 +3636,72 @@ 7.5 Aliasing
A property can be aggregated in multiple ways, each with a different alias.
-Example 99:
+Example 98:
GET /service/Sales?$apply=groupby((Customer/Country),
aggregate(Amount with sum as Total,
Amount with average as AvgAmt))
results in
-{
-"@context": "$metadata#Sales(Customer(Country),Total,AvgAmt)",
- "value": [
- { "Customer": { "Country": "Netherlands" },
- "Total@type": "Decimal", "Total": 5,
- "AvgAmt@type": "Decimal", "AvgAmt": 1.6666667 },
- { "Customer": { "Country": "USA" },
- "Total@type": "Decimal", "Total": 19,
- "AvgAmt@type": "Decimal", "AvgAmt": 3.8 }
- ]
- }
+{
+"@context": "$metadata#Sales(Customer(Country),Total,AvgAmt)",
+ "value": [
+ { "Customer": { "Country": "Netherlands" },
+ "Total@type": "Decimal", "Total": 5,
+ "AvgAmt@type": "Decimal", "AvgAmt": 1.6666667 },
+ { "Customer": { "Country": "USA" },
+ "Total@type": "Decimal", "Total": 19,
+ "AvgAmt@type": "Decimal", "AvgAmt": 3.8 }
+ ]
+ }
The introduced dynamic property is added to the context where the aggregate expression is applied to:
-Example 100:
+Example 99:
GET /service/Products?$apply=groupby((Name),
aggregate(Sales/Amount with sum as Total))
/groupby((Name),
addnested(Sales,aggregate(Amount with average as AvgAmt)
as AggregatedSales))
results in
-{
-"@context": "$metadata#Products(Name,Total,AggregatedSales())",
- "value": [
- { "Name": "Coffee", "Total": 12,
- "AggregatedSales@context": "#Sales(AvgAmt)",
- "AggregatedSales": [ { "AvgAmt@type": "Decimal",
- "AvgAmt": 6 } ] },
- { "Name": "Paper", "Total": 8,
- "AggregatedSales@context": "#Sales(AvgAmt)",
- "AggregatedSales": [ { "AvgAmt@type": "Decimal",
- "AvgAmt": 2 } ] },
- { "Name": "Pencil", "Total": null,
- "AggregatedSales@context": "#Sales(AvgAmt)",
- "AggregatedSales": [ { "AvgAmt": null } ] },
- { "Name": "Sugar", "Total": 4,
- "AggregatedSales@context": "#Sales(AvgAmt)",
- "AggregatedSales": [ { "AvgAmt@type": "Decimal",
- "AvgAmt": 2 } ] }
- ]
- }
+{
+"@context": "$metadata#Products(Name,Total,AggregatedSales())",
+ "value": [
+ { "Name": "Coffee", "Total": 12,
+ "AggregatedSales@context": "#Sales(AvgAmt)",
+ "AggregatedSales": [ { "AvgAmt@type": "Decimal",
+ "AvgAmt": 6 } ] },
+ { "Name": "Paper", "Total": 8,
+ "AggregatedSales@context": "#Sales(AvgAmt)",
+ "AggregatedSales": [ { "AvgAmt@type": "Decimal",
+ "AvgAmt": 2 } ] },
+ { "Name": "Pencil", "Total": null,
+ "AggregatedSales@context": "#Sales(AvgAmt)",
+ "AggregatedSales": [ { "AvgAmt": null } ] },
+ { "Name": "Sugar", "Total": 4,
+ "AggregatedSales@context": "#Sales(AvgAmt)",
+ "AggregatedSales": [ { "AvgAmt@type": "Decimal",
+ "AvgAmt": 2 } ] }
+ ]
+ }
There is no hard distinction between groupable and aggregatable properties: the same property can be aggregated and used to group the aggregated results.
-Example 101:
+Example 100:
GET /service/Sales?$apply=groupby((Amount),aggregate(Amount with sum as Total))
will return all distinct amounts appearing in sales orders and how much money was made with deals of this amount
-{
-"@context": "$metadata#Sales(Amount,Total)",
- "value": [
- { "Amount": 1, "Total@type": "Decimal", "Total": 2 },
- { "Amount": 2, "Total@type": "Decimal", "Total": 6 },
- { "Amount": 4, "Total@type": "Decimal", "Total": 8 },
- { "Amount": 8, "Total@type": "Decimal", "Total": 8 }
- ]
- }
+{
+"@context": "$metadata#Sales(Amount,Total)",
+ "value": [
+ { "Amount": 1, "Total@type": "Decimal", "Total": 2 },
+ { "Amount": 2, "Total@type": "Decimal", "Total": 6 },
+ { "Amount": 4, "Total@type": "Decimal", "Total": 8 },
+ { "Amount": 8, "Total@type": "Decimal", "Total": 8 }
+ ]
+ }
7.6 Combining Transformations per Group
Dynamic property names may be reused in different transformation sequences passed to concat
.
-Example 102: to get the best-selling product per country with sub-totals for every country, the partial results of a transformation sequence and a groupby
transformation are concatenated:
+Example 101: to get the best-selling product per country with sub-totals for every country, the partial results of a transformation sequence and a groupby
transformation are concatenated:
GET /service/Sales?$apply=concat(
groupby((Customer/Country,Product/Name),
aggregate(Amount with sum as Total))
@@ -3745,87 +3709,87 @@ {
+"@context": "$metadata#Sales(Customer(Country),Total)",
+ "value": [
+ { "Customer":{ "Country": "USA" }, "Product":{ "Name": "Coffee" },
+ "Total@type": "Decimal", "Total": 12
+ },
+ { "Customer":{ "Country": "Netherlands" }, "Product":{ "Name": "Paper" },
+ "Total@type": "Decimal", "Total": 3
+ },
+ { "Customer":{ "Country": "USA" },
+ "Total@type": "Decimal", "Total": 19
+ },
+ { "Customer":{ "Country": "Netherlands" },
+ "Total@type": "Decimal", "Total": 5
+ }
+ ]
+ }
+
+
+Example 102: transformation sequences are also useful inside groupby
: Aggregate the amount by only considering the top two sales amounts per product and country:
+GET /service/Sales?$apply=groupby((Customer/Country,Product/Name),
+ topcount(2,Amount)/aggregate(Amount with sum as Total))
+results in
{
-"@context": "$metadata#Sales(Customer(Country),Total)",
+ "@context": "$metadata#Sales(Customer(Country),Product(Name),Total)",
"value": [
- { "Customer":{ "Country": "USA" }, "Product":{ "Name": "Coffee" },
- "Total@type": "Decimal", "Total": 12
+ { "Customer":{ "Country": "Netherlands" }, "Product":{ "Name": "Paper" },
+ "Total@type": "Decimal", "Total": 3
},
- { "Customer":{ "Country": "Netherlands" }, "Product":{ "Name": "Paper" },
- "Total@type": "Decimal", "Total": 3
+ { "Customer":{ "Country": "Netherlands" }, "Product":{ "Name": "Sugar" },
+ "Total@type": "Decimal", "Total": 2
},
- { "Customer":{ "Country": "USA" },
- "Total@type": "Decimal", "Total": 19
+ { "Customer":{ "Country": "USA" }, "Product":{ "Name": "Sugar" },
+ "Total@type": "Decimal", "Total": 2
},
- { "Customer":{ "Country": "Netherlands" },
- "Total@type": "Decimal", "Total": 5
- }
- ]
- }
+{ "Customer":{ "Country": "USA" }, "Product":{ "Name": "Coffee" },
+ "Total@type": "Decimal", "Total": 12
+ },
+ { "Customer":{ "Country": "USA" }, "Product":{ "Name": "Paper" },
+ "Total@type": "Decimal", "Total": 5
+ }
+ ]
+ }
Example 103: transformation sequences are also useful inside groupby
: Aggregate the amount by only considering the top two sales amounts per product and country:
GET /service/Sales?$apply=groupby((Customer/Country,Product/Name),
- topcount(2,Amount)/aggregate(Amount with sum as Total))
-results in
-{
-"@context": "$metadata#Sales(Customer(Country),Product(Name),Total)",
- "value": [
- { "Customer":{ "Country": "Netherlands" }, "Product":{ "Name": "Paper" },
- "Total@type": "Decimal", "Total": 3
- },
- { "Customer":{ "Country": "Netherlands" }, "Product":{ "Name": "Sugar" },
- "Total@type": "Decimal", "Total": 2
- },
- { "Customer":{ "Country": "USA" }, "Product":{ "Name": "Sugar" },
- "Total@type": "Decimal", "Total": 2
- },
- { "Customer":{ "Country": "USA" }, "Product":{ "Name": "Coffee" },
- "Total@type": "Decimal", "Total": 12
- },
- { "Customer":{ "Country": "USA" }, "Product":{ "Name": "Paper" },
- "Total@type": "Decimal", "Total": 5
- }
- ]
- }
Example 104: concatenation of two different groupings "biggest sale per customer" and "biggest sale per product", made distinguishable by a dynamic property:
+Example 103: concatenation of two different groupings "biggest sale per customer" and "biggest sale per product", made distinguishable by a dynamic property:
GET /service/Sales?$apply=concat(
groupby((Customer),topcount(1,Amount))/compute('Customer' as per),
groupby((Product),topcount(1,Amount))/compute('Product' as per))
&$expand=Customer($select=ID),Product($select=ID)
In the result, Sales
entities 4 and 6 occur twice each with contradictory values of the dynamic property per
. If a UI consuming the response presents the two groupings in separate columns based on the per
property, no contradiction effectively arises.
{
-"@context": "$metadata#Sales(*,per,Customer(ID),Product(ID))",
- "value": [
- { "Customer": { "ID": "C1" }, "Product": { "ID": "P2" },
- "ID": "3", "Amount": 4, "per": "Customer" },
- { "Customer": { "ID": "C2" }, "Product": { "ID": "P2" },
- "ID": "4", "Amount": 8, "per": "Customer" },
- { "Customer": { "ID": "C3" }, "Product": { "ID": "P1" },
- "ID": "6", "Amount": 2, "per": "Customer" },
- { "Customer": { "ID": "C3" }, "Product": { "ID": "P1" },
- "ID": "6", "Amount": 2, "per": "Product" },
- { "Customer": { "ID": "C2" }, "Product": { "ID": "P2" },
- "ID": "4", "Amount": 8, "per": "Product" },
- { "Customer": { "ID": "C2" }, "Product": { "ID": "P3" },
- "ID": "5", "Amount": 4, "per": "Product" }
- ]
- }
{
+"@context": "$metadata#Sales(*,per,Customer(ID),Product(ID))",
+ "value": [
+ { "Customer": { "ID": "C1" }, "Product": { "ID": "P2" },
+ "ID": "3", "Amount": 4, "per": "Customer" },
+ { "Customer": { "ID": "C2" }, "Product": { "ID": "P2" },
+ "ID": "4", "Amount": 8, "per": "Customer" },
+ { "Customer": { "ID": "C3" }, "Product": { "ID": "P1" },
+ "ID": "6", "Amount": 2, "per": "Customer" },
+ { "Customer": { "ID": "C3" }, "Product": { "ID": "P1" },
+ "ID": "6", "Amount": 2, "per": "Product" },
+ { "Customer": { "ID": "C2" }, "Product": { "ID": "P2" },
+ "ID": "4", "Amount": 8, "per": "Product" },
+ { "Customer": { "ID": "C2" }, "Product": { "ID": "P3" },
+ "ID": "5", "Amount": 4, "per": "Product" }
+ ]
+ }
Example 105: As a variation of example 102, a query for returning the best-selling product per country and the total amount of the remaining products can be formulated with the help of a model function.
+Example 104: As a variation of example 101, a query for returning the best-selling product per country and the total amount of the remaining products can be formulated with the help of a model function.
For this purpose, the model includes a definition of a TopCountAndRemainder
function that accepts a count and a numeric property for the top entities:
edm:Function Name="TopCountAndRemainder"
- < IsBound="true">
-edm:Parameter Name="EntityCollection"
- < Type="Collection(Edm.EntityType)" />
-edm:Parameter Name="Count" Type="Edm.Int16" />
- <edm:Parameter Name="Property" Type="Edm.String" />
- <edm:ReturnType Type="Collection(Edm.EntityType)" />
- <edm:Function> </
edm:Function Name="TopCountAndRemainder"
+ < IsBound="true">
+edm:Parameter Name="EntityCollection"
+ < Type="Collection(Edm.EntityType)" />
+edm:Parameter Name="Count" Type="Edm.Int16" />
+ <edm:Parameter Name="Property" Type="Edm.String" />
+ <edm:ReturnType Type="Collection(Edm.EntityType)" />
+ <edm:Function> </
The function retains those entities that topcount
also would retain, and replaces the remaining entities by a single aggregated entity, where only the numeric property has a value, which is the sum over those remaining entities:
GET /service/Sales?$apply=
groupby((Customer/Country,Product/Name),
@@ -3833,27 +3797,27 @@ {
-"@context": "$metadata#Sales(Customer(Country),Total)",
- "value": [
- { "Customer": { "Country": "Netherlands" },
- "Product": { "Name": "Paper" },
- "Total@type": "Decimal", "Total": 3 },
- { "Customer": { "Country": "Netherlands" },
- "Total@type": "Decimal", "Total": 2 },
- { "Customer": { "Country": "USA" },
- "Product": { "Name": "Coffee" },
- "Total@type": "Decimal", "Total": 12 },
- { "Customer": { "Country": "USA" },
- "Total@type": "Decimal", "Total": 7 }
- ]
- }
{
+"@context": "$metadata#Sales(Customer(Country),Total)",
+ "value": [
+ { "Customer": { "Country": "Netherlands" },
+ "Product": { "Name": "Paper" },
+ "Total@type": "Decimal", "Total": 3 },
+ { "Customer": { "Country": "Netherlands" },
+ "Total@type": "Decimal", "Total": 2 },
+ { "Customer": { "Country": "USA" },
+ "Product": { "Name": "Coffee" },
+ "Total@type": "Decimal", "Total": 12 },
+ { "Customer": { "Country": "USA" },
+ "Total@type": "Decimal", "Total": 7 }
+ ]
+ }
Note that these two entities get their values for the Country property from the groupby transformation, which ensures that they contain all grouping properties with the correct values.
For a leveled hierarchy, consumers may specify a different aggregation method per level for every property passed to rollup
as a hierarchy level below the root level.
Example 106: get the average of the overall amount by month per product.
+Example 105: get the average of the overall amount by month per product.
Using a transformation sequence:
GET /service/Sales?$apply=groupby((Product/ID,Product/Name,Time/Month),
aggregate(Amount with sum) as Total))
@@ -3866,7 +3830,7 @@
-Example 107: get the total amount per customer, the average of the total customer amounts per country, and the overall average of these averages
+Example 106: get the total amount per customer, the average of the total customer amounts per country, and the overall average of these averages
GET /service/Sales?$apply=concat(
groupby((rollup(Customer/Country,Customer/ID)),
aggregate(Amount with sum
@@ -3877,34 +3841,34 @@ {
-"@context": "$metadata#Sales(CustomerCountryAverage)",
- "value": [
- { "Customer": { "Country": "USA", "ID": "C1" },
- "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 7 },
- { "Customer": { "Country": "USA", "ID": "C2" },
- "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 12 },
- { "Customer": { "Country": "USA" },
- "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 9.5 },
- { "Customer": { "Country": "Netherlands", "ID": "C3" },
- "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 5 },
- { "Customer": { "Country": "Netherlands" },
- "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 5 },
- { "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 7.25 }
- ]
- }
{
+"@context": "$metadata#Sales(CustomerCountryAverage)",
+ "value": [
+ { "Customer": { "Country": "USA", "ID": "C1" },
+ "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 7 },
+ { "Customer": { "Country": "USA", "ID": "C2" },
+ "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 12 },
+ { "Customer": { "Country": "USA" },
+ "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 9.5 },
+ { "Customer": { "Country": "Netherlands", "ID": "C3" },
+ "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 5 },
+ { "Customer": { "Country": "Netherlands" },
+ "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 5 },
+ { "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 7.25 }
+ ]
+ }
Note that this example extends the result of rollup
with concat
and aggregate
to append the overall average.
If aggregation along a recursive hierarchy does not apply to the entire hierarchy, transformations ancestors
and descendants
may be used to restrict it as needed.
Example 108: Total sales amounts for sales orgs in 'US' in the SalesOrgHierarchy
defined in Hierarchy Examples
Example 107: Total sales amounts for sales orgs in 'US' in the SalesOrgHierarchy
defined in Hierarchy Examples
GET /service/Sales?$apply=
descendants(
$root/SalesOrganizations,SalesOrgHierarchy,SalesOrganization/ID,
@@ -3914,25 +3878,25 @@ {
-"@context": "$metadata#Sales(TotalAmount,SalesOrganization())",
- "value": [
- { "TotalAmount@type": "Decimal", "TotalAmount": 19,
- "SalesOrganization": { "ID": "US", "Name": "US",
- "Superordinate": { "@id": "SalesOrganizations('Sales')" } } },
- { "TotalAmount@type": "Decimal", "TotalAmount": 12,
- "SalesOrganization": { "ID": "US East", "Name": "US East",
- "Superordinate": { "@id": "SalesOrganizations('US')" } } },
- { "TotalAmount@type": "Decimal", "TotalAmount": 7,
- "SalesOrganization": { "ID": "US West", "Name": "US West",
- "Superordinate": { "@id": "SalesOrganizations('US')" } } }
- ]
- }
{
+"@context": "$metadata#Sales(TotalAmount,SalesOrganization())",
+ "value": [
+ { "TotalAmount@type": "Decimal", "TotalAmount": 19,
+ "SalesOrganization": { "ID": "US", "Name": "US",
+ "Superordinate": { "@id": "SalesOrganizations('Sales')" } } },
+ { "TotalAmount@type": "Decimal", "TotalAmount": 12,
+ "SalesOrganization": { "ID": "US East", "Name": "US East",
+ "Superordinate": { "@id": "SalesOrganizations('US')" } } },
+ { "TotalAmount@type": "Decimal", "TotalAmount": 7,
+ "SalesOrganization": { "ID": "US West", "Name": "US West",
+ "Superordinate": { "@id": "SalesOrganizations('US')" } } }
+ ]
+ }
Note that this example returns the actual total sums regardless of whether the descendants
transformation comes before or after the groupby
with rolluprecursive
.
The order of transformations becomes relevant if groupby
with rolluprecursive
shall aggregate over a thinned-out hierarchy, like here:
Example 109: Number of Paper sales per sales org aggregated along the the SalesOrgHierarchy
defined in Hierarchy Examples
Example 108: Number of Paper sales per sales org aggregated along the the SalesOrgHierarchy
defined in Hierarchy Examples
GET /service/Sales?$apply=
filter(Product/Name eq 'Paper')
/groupby((rolluprecursive((
@@ -3940,32 +3904,32 @@ {
-"@context": "$metadata#Sales(PaperSalesCount,SalesOrganization())",
- "value": [
- { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 2,
- "SalesOrganization": { "ID": "US", "Name": "US",
- "Superordinate": { "@id": "SalesOrganizations('Sales')" } } },
- { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 1,
- "SalesOrganization": { "ID": "US East", "Name": "US East",
- "Superordinate": { "@id": "SalesOrganizations('US')" } } },
- { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 1,
- "SalesOrganization": { "ID": "US West", "Name": "US West",
- "Superordinate": { "@id": "SalesOrganizations('US')" } } },
- { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 2,
- "SalesOrganization": { "ID": "EMEA", "Name": "EMEA",
- "Superordinate": { "@id": "SalesOrganizations('Sales')" } } },
- { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 2,
- "SalesOrganization": { "ID": "EMEA Central", "Name": "EMEA Central",
- "Superordinate": { "@id": "SalesOrganizations('EMEA')" } } },
- { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 4,
- "SalesOrganization": { "ID": "Sales", "Name": "Sales",
- "Superordinate": null } }
- ]
- }
⚠ Example 110: The input set Sales
is filtered along a hierarchy on a related entity (navigation property SalesOrganization
) before an aggregation
{
+"@context": "$metadata#Sales(PaperSalesCount,SalesOrganization())",
+ "value": [
+ { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 2,
+ "SalesOrganization": { "ID": "US", "Name": "US",
+ "Superordinate": { "@id": "SalesOrganizations('Sales')" } } },
+ { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 1,
+ "SalesOrganization": { "ID": "US East", "Name": "US East",
+ "Superordinate": { "@id": "SalesOrganizations('US')" } } },
+ { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 1,
+ "SalesOrganization": { "ID": "US West", "Name": "US West",
+ "Superordinate": { "@id": "SalesOrganizations('US')" } } },
+ { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 2,
+ "SalesOrganization": { "ID": "EMEA", "Name": "EMEA",
+ "Superordinate": { "@id": "SalesOrganizations('Sales')" } } },
+ { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 2,
+ "SalesOrganization": { "ID": "EMEA Central", "Name": "EMEA Central",
+ "Superordinate": { "@id": "SalesOrganizations('EMEA')" } } },
+ { "PaperSalesCount@type": "Decimal", "PaperSalesCount": 4,
+ "SalesOrganization": { "ID": "Sales", "Name": "Sales",
+ "Superordinate": null } }
+ ]
+ }
⚠ Example 109: The input set Sales
is filtered along a hierarchy on a related entity (navigation property SalesOrganization
) before an aggregation
GET /service/Sales?$apply=
descendants($root/SalesOrganizations,
SalesOrgHierarchy,
@@ -3983,7 +3947,7 @@
-⚠ Example 111: total sales amount aggregated along the sales organization sub-hierarchy with root EMEA restricted to 3 levels
+⚠ Example 110: total sales amount aggregated along the sales organization sub-hierarchy with root EMEA restricted to 3 levels
GET /service/Sales?$apply=
groupby((rolluprecursive($root/SalesOrganizations,
SalesOrgHierarchy,
@@ -4017,7 +3981,7 @@
-Example 112: Return the result of example 67 in preorder
+Example 111: Return the result of example 66 in preorder
GET /service/Sales?$apply=groupby(
(rolluprecursive(
$root/SalesOrganizations,
@@ -4037,24 +4001,24 @@ {
-"@context": "$metadata#Sales(SalesOrganization(ID),
- TotalAmountIncl,TotalAmountExcl)",
-"value": [
- { "SalesOrganization": { "ID": "US", "Name": "US" },
- "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 19,
- "TotalAmountExcl": null },
- { "SalesOrganization": { "ID": "US East", "Name": "US East" },
- "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 12,
- "TotalAmountExcl@type": "Decimal", "TotalAmountExcl": 12 },
- { "SalesOrganization": { "ID": "US West", "Name": "US West" },
- "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 7,
- "TotalAmountExcl@type": "Decimal" ,"TotalAmountExcl": 7 }
- ]
- }
Example 113: Preorder traversal of a hierarchy with 1:N relationship with collection-valued segment \(p_1={\tt Sales}\) and \(r={\tt SalesOrganization}/{\tt ID}\).
+{
+"@context": "$metadata#Sales(SalesOrganization(ID),
+ TotalAmountIncl,TotalAmountExcl)",
+"value": [
+ { "SalesOrganization": { "ID": "US", "Name": "US" },
+ "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 19,
+ "TotalAmountExcl": null },
+ { "SalesOrganization": { "ID": "US East", "Name": "US East" },
+ "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 12,
+ "TotalAmountExcl@type": "Decimal", "TotalAmountExcl": 12 },
+ { "SalesOrganization": { "ID": "US West", "Name": "US West" },
+ "TotalAmountIncl@type": "Decimal", "TotalAmountIncl": 7,
+ "TotalAmountExcl@type": "Decimal" ,"TotalAmountExcl": 7 }
+ ]
+ }
Example 112: Preorder traversal of a hierarchy with 1:N relationship with collection-valued segment \(p_1={\tt Sales}\) and \(r={\tt SalesOrganization}/{\tt ID}\).
GET /service/Products?$apply=traverse(
$root/SalesOrganizations,
SalesOrgHierarchy,
@@ -4063,32 +4027,32 @@ \(x\) with \(x/{\tt ID}={}\)"US"
has \(σ(x)={}\){"Sales": [{"SalesOrganization": {"ID": "US"}}]}
.
-{
-"@context":
- "$metadata#Products(ID,Sales(SalesOrganization(ID)))",
- "value": [
- { "ID": "P1", "Sales": [ { "SalesOrganization": { "ID": "Sales" } } ] },
- { "ID": "P2", "Sales": [ { "SalesOrganization": { "ID": "Sales" } } ] },
- { "ID": "P3", "Sales": [ { "SalesOrganization": { "ID": "Sales" } } ] },
- { "ID": "P1", "Sales": [ { "SalesOrganization": { "ID": "EMEA" } } ] },
- { "ID": "P3", "Sales": [ { "SalesOrganization": { "ID": "EMEA" } } ] },
- { "ID": "P1",
- "Sales": [ { "SalesOrganization": { "ID": "EMEA Central" } } ] },
- { "ID": "P3",
- "Sales": [ { "SalesOrganization": { "ID": "EMEA Central" } } ] },
- { "ID": "P1", "Sales": [ { "SalesOrganization": { "ID": "US" } } ] },
- { "ID": "P2", "Sales": [ { "SalesOrganization": { "ID": "US" } } ] },
- { "ID": "P3", "Sales": [ { "SalesOrganization": { "ID": "US" } } ] },
- { "ID": "P2", "Sales": [ { "SalesOrganization": { "ID": "US East" } } ] },
- { "ID": "P3", "Sales": [ { "SalesOrganization": { "ID": "US East" } } ] },
- { "ID": "P1", "Sales": [ { "SalesOrganization": { "ID": "US West" } } ] },
- { "ID": "P2", "Sales": [ { "SalesOrganization": { "ID": "US West" } } ] },
- { "ID": "P3", "Sales": [ { "SalesOrganization": { "ID": "US West" } } ] }
- ]
- }
-
Example 114: Aggregation along a hierarchy with 1:N relationship: Sold products per sales organization
+{
+"@context":
+ "$metadata#Products(ID,Sales(SalesOrganization(ID)))",
+ "value": [
+ { "ID": "P1", "Sales": [ { "SalesOrganization": { "ID": "Sales" } } ] },
+ { "ID": "P2", "Sales": [ { "SalesOrganization": { "ID": "Sales" } } ] },
+ { "ID": "P3", "Sales": [ { "SalesOrganization": { "ID": "Sales" } } ] },
+ { "ID": "P1", "Sales": [ { "SalesOrganization": { "ID": "EMEA" } } ] },
+ { "ID": "P3", "Sales": [ { "SalesOrganization": { "ID": "EMEA" } } ] },
+ { "ID": "P1",
+ "Sales": [ { "SalesOrganization": { "ID": "EMEA Central" } } ] },
+ { "ID": "P3",
+ "Sales": [ { "SalesOrganization": { "ID": "EMEA Central" } } ] },
+ { "ID": "P1", "Sales": [ { "SalesOrganization": { "ID": "US" } } ] },
+ { "ID": "P2", "Sales": [ { "SalesOrganization": { "ID": "US" } } ] },
+ { "ID": "P3", "Sales": [ { "SalesOrganization": { "ID": "US" } } ] },
+ { "ID": "P2", "Sales": [ { "SalesOrganization": { "ID": "US East" } } ] },
+ { "ID": "P3", "Sales": [ { "SalesOrganization": { "ID": "US East" } } ] },
+ { "ID": "P1", "Sales": [ { "SalesOrganization": { "ID": "US West" } } ] },
+ { "ID": "P2", "Sales": [ { "SalesOrganization": { "ID": "US West" } } ] },
+ { "ID": "P3", "Sales": [ { "SalesOrganization": { "ID": "US West" } } ] }
+ ]
+ }
Example 113: Aggregation along a hierarchy with 1:N relationship: Sold products per sales organization
GET /service/Products?$apply=
groupby((rolluprecursive(
$root/SalesOrganizations,
@@ -4096,26 +4060,26 @@ {
-"@context": "$metadata#Products(Sales(SalesOrganization(ID)),SoldProducts)",
- "value": [
- { "Sales": [ { "SalesOrganization": { "ID": "Sales" } } ],
- "SoldProducts": "P1,P2,P3" },
- { "Sales": [ { "SalesOrganization": { "ID": "EMEA" } } ],
- "SoldProducts": "P1,P3" },
- { "Sales": [ { "SalesOrganization": { "ID": "EMEA Central" } } ],
- "SoldProducts": "P1,P3" },
- { "Sales": [ { "SalesOrganization": { "ID": "US" } } ],
- "SoldProducts": "P1,P2,P3" },
- { "Sales": [ { "SalesOrganization": { "ID": "US East" } } ],
- "SoldProducts": "P2,P3" },
- { "Sales": [ { "SalesOrganization": { "ID": "US West" } } ],
- "SoldProducts": "P1,P2,P3" }
- ]
- }
⚠ Example 115: Assume an extension of the data model where a SalesOrganization
is associated with one or more instances of ProductCategory
, and ProductCategory
also organizes categories in a recursive hierarchy:
{
+"@context": "$metadata#Products(Sales(SalesOrganization(ID)),SoldProducts)",
+ "value": [
+ { "Sales": [ { "SalesOrganization": { "ID": "Sales" } } ],
+ "SoldProducts": "P1,P2,P3" },
+ { "Sales": [ { "SalesOrganization": { "ID": "EMEA" } } ],
+ "SoldProducts": "P1,P3" },
+ { "Sales": [ { "SalesOrganization": { "ID": "EMEA Central" } } ],
+ "SoldProducts": "P1,P3" },
+ { "Sales": [ { "SalesOrganization": { "ID": "US" } } ],
+ "SoldProducts": "P1,P2,P3" },
+ { "Sales": [ { "SalesOrganization": { "ID": "US East" } } ],
+ "SoldProducts": "P2,P3" },
+ { "Sales": [ { "SalesOrganization": { "ID": "US West" } } ],
+ "SoldProducts": "P1,P2,P3" }
+ ]
+ }
⚠ Example 114: Assume an extension of the data model where a SalesOrganization
is associated with one or more instances of ProductCategory
, and ProductCategory
also organizes categories in a recursive hierarchy: