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 @@

3.1 Fundamentals of Input and Output Sets

The definitions of italicized terms made in this section are used throughout this text, always with a hyperlink to this section.

3.1.1 Type, Structure and Context URL

-

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.)

3.1.2 Sameness and Order

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.

@@ -1569,7 +1569,7 @@

OData-Protocol, section 4.1) and (2) the structural and navigation properties contained in both have the same values (for non-primitive properties the sameness of values is decided by a recursive invocation of this definition).

Collections are the same if there is a one-to-one correspondence \(f\) between them such that

@@ -1618,7 +1618,7 @@

\(I\) be the input set. If \(p\) is absent, let \(A=I\) with null values removed.

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 @@

3.2.3 Transformation 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.

3.2.3.1 Simple Grouping

-

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.

    @@ -2574,8 +2574,8 @@

    grouping with 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.

    5.5.2.1 Hierarchy Functions

    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:

  1. If \(u\) equals the special symbol \(ε\), set \(u\) to a new instance of the input type without properties and without entity id.
  2. @@ -2870,7 +2870,7 @@

    \(t_1\) is collection-valued, let \(u[t_1]\) be a collection consisting of one item \(x'\).
  3. Return \(u\).
-

(See example 113.)

+

(See example 112.)

6.2.2.1 Standard Case of 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 @@

}

6.2.2.2 General Case of 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:

@@ -2931,48 +2931,12 @@

\(x\), let \(ρ_0(x)\) be the node \(x\) with the annotation \(ρ_0(x)/@\hbox{\tt Aggregation.UpPath}\#Q\) set to an empty collection.

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:

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.

6.3 Grouping with rolluprecursive

@@ -2981,17 +2945,17 @@

Navigation properties specified in \(p\) are expanded by default.

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 lambdaVariableExprs 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 lambdaVariableExprs 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 },
+    ...
+  ]
+}

7 Examples

@@ -3118,348 +3082,348 @@

7 Examples

7.1 Requesting Distinct Values

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.NonFoodProducts:

-
{
-  "@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 },
+    { }
+  ]
+}

7.2 Standard Aggregation Methods

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 }
-  ]
-}
+ "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" }
+  ]
+}

7.7 Model Functions as Set Transformations

-

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.

7.8 Controlling Aggregation per Rollup Level

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.

7.9 Aggregation in Recursive Hierarchies

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:

@@ -4159,20 +4123,20 @@

{
-  "@context": "$metadata#Sales(SalesOrganization(ID),TotalAmount)",
-  "value": [
-    { "SalesOrganization": { "ID": "Sales",   "ProductCategories": [ ] },
-      "TotalAmount@type": "Decimal", "TotalAmount": 24 },
-    { "SalesOrganization": { "ID": "US",      "ProductCategories": [
-      { "@id": "ProductCategories('Food')" },
-      { "@id": "ProductCategories('Cereals')" } ] },
-      "TotalAmount@type": "Decimal", "TotalAmount": 19 },
-    { "SalesOrganization": { "ID": "US West", "ProductCategories": [
-      { "@id": "ProductCategories('Organic cereals')" } ] },
-      "TotalAmount@type": "Decimal", "TotalAmount":  7 }
-  ]
-}
+
{
+  "@context": "$metadata#Sales(SalesOrganization(ID),TotalAmount)",
+  "value": [
+    { "SalesOrganization": { "ID": "Sales",   "ProductCategories": [ ] },
+      "TotalAmount@type": "Decimal", "TotalAmount": 24 },
+    { "SalesOrganization": { "ID": "US",      "ProductCategories": [
+      { "@id": "ProductCategories('Food')" },
+      { "@id": "ProductCategories('Cereals')" } ] },
+      "TotalAmount@type": "Decimal", "TotalAmount": 19 },
+    { "SalesOrganization": { "ID": "US West", "ProductCategories": [
+      { "@id": "ProductCategories('Organic cereals')" } ] },
+      "TotalAmount@type": "Decimal", "TotalAmount":  7 }
+  ]
+}

traverse acts here as a filter, hence preorder could be changed to postorder without changing the result. filter is the parameter \(S\) of traverse and operates on the product category hierarchy being traversed.

Replacing the traverse transformation with a descendants transformation, as in

ancestors(
@@ -4189,72 +4153,72 @@ 

7.10 Maintaining Recursive Hierarchies

Besides changes to the structural properties of the entities in a hierarchical collection, hierarchy maintenance involves changes to the parent-child relationships.

-

Example 116: Move a sales organization Switzerland under the parent EMEA Central by binding the parent navigation property to EMEA Central OData-JSON, section 8.5:

-
PATCH /service/SalesOrganizations('Switzerland')
-Content-Type: application/json
-
-{ "Superordinate": { "@id": "SalesOrganizations('EMEA Central')" } }
+

Example 115: Move a sales organization Switzerland under the parent EMEA Central by binding the parent navigation property to EMEA Central OData-JSON, section 8.5:

+
PATCH /service/SalesOrganizations('Switzerland')
+Content-Type: application/json
+
+{ "Superordinate": { "@id": "SalesOrganizations('EMEA Central')" } }

results in 204 No Content.

Deleting the parent from the sales organization Switzerland (making it a root) can be achieved either with:

-
PATCH /service/SalesOrganizations('Switzerland')
-Content-Type: application/json
-
-{ "Superordinate": { "@id": null } }
+
PATCH /service/SalesOrganizations('Switzerland')
+Content-Type: application/json
+
+{ "Superordinate": { "@id": null } }

or with:

DELETE /service/SalesOrganizations('Switzerland')/Superordinate/$ref
-

Example 117: If the parent navigation property contained a referential constraint for the key of the target OData-CSDL, section 8.5,

-
<EntityType Name="SalesOrganization">
-  <Key>
-    <PropertyRef Name="ID" />
-  </Key>
-  <Property Name="ID" Type="Edm.String" Nullable="false" />
-  <Property Name="Name" Type="Edm.String" />
-  <Property Name="SuperordinateID" Type="Edm.String" />
-  <NavigationProperty Name="Superordinate"
-                      Type="SalesModel.SalesOrganization">
-    <ReferentialConstraint Property="SuperordinateID"
-                           ReferencedProperty="ID" />
-  </NavigationProperty>
-</EntityType>
+

Example 116: If the parent navigation property contained a referential constraint for the key of the target OData-CSDL, section 8.5,

+
<EntityType Name="SalesOrganization">
+  <Key>
+    <PropertyRef Name="ID" />
+  </Key>
+  <Property Name="ID" Type="Edm.String" Nullable="false" />
+  <Property Name="Name" Type="Edm.String" />
+  <Property Name="SuperordinateID" Type="Edm.String" />
+  <NavigationProperty Name="Superordinate"
+                      Type="SalesModel.SalesOrganization">
+    <ReferentialConstraint Property="SuperordinateID"
+                           ReferencedProperty="ID" />
+  </NavigationProperty>
+</EntityType>

then alternatively the property taking part in the referential constraint OData-Protocol, section 11.4.9.1 could be changed to EMEA Central:

-
PATCH /service/SalesOrganizations('Switzerland')
-Content-Type: application/json
-
-{ "SuperordinateID": "EMEA Central" }
+
PATCH /service/SalesOrganizations('Switzerland')
+Content-Type: application/json
+
+{ "SuperordinateID": "EMEA Central" }

If the parent-child relationship between sales organizations is maintained in a separate entity set, a node can have multiple parents, with additional information on each parent-child relationship.

-

⚠ Example 118: Assume the relation from a node to its parent nodes contains a weight:

-
<EntityType Name="SalesOrganizationRelation">
-  <Key>
-    <PropertyRef Name="Superordinate/ID" Alias="SuperordinateID" />
-  </Key>
-  <Property Name="Weight" Type="Edm.Decimal"
-                          Nullable="false" DefaultValue="1" />
-  <NavigationProperty Name="Superordinate"
-                      Type="SalesModel.SalesOrganization" Nullable="false" />
-</EntityType>
-<EntityType Name="SalesOrganization">
-  <Key>
-    <PropertyRef Name="ID" />
-  </Key>
-  <Property Name="ID" Type="Edm.String" Nullable="false" />
-  <Property Name="Name" Type="Edm.String" />
-  <NavigationProperty Name="Relations"
-                      Type="Collection(SalesModel.SalesOrganizationRelation)"
-                      Nullable="false" ContainsTarget="true" />
-  <Annotation Term="Aggregation.RecursiveHierarchy"
-              Qualifier="MultiParentHierarchy">
-    <Record>
-      <PropertyValue Property="NodeProperty"
-                     PropertyPath="ID" />
-      <PropertyValue Property="ParentNavigationProperty"
-                     NavigationPropertyPath="Relations/Superordinate" />
-    </Record>
-  </Annotation>
-</EntityType>
+

⚠ Example 117: Assume the relation from a node to its parent nodes contains a weight:

+
<EntityType Name="SalesOrganizationRelation">
+  <Key>
+    <PropertyRef Name="Superordinate/ID" Alias="SuperordinateID" />
+  </Key>
+  <Property Name="Weight" Type="Edm.Decimal"
+                          Nullable="false" DefaultValue="1" />
+  <NavigationProperty Name="Superordinate"
+                      Type="SalesModel.SalesOrganization" Nullable="false" />
+</EntityType>
+<EntityType Name="SalesOrganization">
+  <Key>
+    <PropertyRef Name="ID" />
+  </Key>
+  <Property Name="ID" Type="Edm.String" Nullable="false" />
+  <Property Name="Name" Type="Edm.String" />
+  <NavigationProperty Name="Relations"
+                      Type="Collection(SalesModel.SalesOrganizationRelation)"
+                      Nullable="false" ContainsTarget="true" />
+  <Annotation Term="Aggregation.RecursiveHierarchy"
+              Qualifier="MultiParentHierarchy">
+    <Record>
+      <PropertyValue Property="NodeProperty"
+                     PropertyPath="ID" />
+      <PropertyValue Property="ParentNavigationProperty"
+                     NavigationPropertyPath="Relations/Superordinate" />
+    </Record>
+  </Annotation>
+</EntityType>

Further assume the following relationships between sales organizations:

@@ -4297,30 +4261,30 @@

example 119).

+

Then Atlantis is a node with two parents. The standard hierarchical transformations disregard the weight property and consider both parents equally valid (but see example 118).

In a traversal with start node Sales only, Mars and Phobos cannot be reached and hence are orphans:

GET /service/SalesOrganizations?$apply=
   traverse($root/SalesOrganizations,MultiParentHierarchy,ID,preorder,
            filter(ID eq 'Sales'))

But Mars and Phobos can be made descendants of the start node Sales by adding a relationship. Note the collection-valued segment of the ParentNavigationProperty appears at the end of the resource path and the subsequent single-valued segment appears in the payload:

-
POST /service/SalesOrganizations('Mars')/Relations
-Content-Type: application/json
-
-{ "Superordinate": { "@id": "SalesOrganizations('Sales')" } }
-

Since this example contains no referential constraint, there is no analogy to example 117. The alias SuperordinateID cannot be used in the payload, the following request is invalid:

-
POST /service/SalesOrganizations('Mars')/Relations
-Content-Type: application/json
-
-{ "SuperordinateID": "Sales" }
+
POST /service/SalesOrganizations('Mars')/Relations
+Content-Type: application/json
+
+{ "Superordinate": { "@id": "SalesOrganizations('Sales')" } }
+

Since this example contains no referential constraint, there is no analogy to example 116. The alias SuperordinateID cannot be used in the payload, the following request is invalid:

+
POST /service/SalesOrganizations('Mars')/Relations
+Content-Type: application/json
+
+{ "SuperordinateID": "Sales" }

The alias SuperordinateID is used in the request to delete the added relationship again:

DELETE /service/SalesOrganizations('Mars')/Relations('Sales')
-

⚠ Example 119: Continuing example 118, assume a custom aggregate MultiParentWeightedTotal that computes the total sales amount weighted by the SalesOrganizationRelation/Weight properties along the @Aggregation.UpPath#MultiParentHierarchy of a sales organization:

-
<Annotations Target="SalesData.Sales">
-  <Annotation Term="Aggregation.CustomAggregate"
-    Qualifier="MultiParentWeightedTotal" String="Edm.Decimal" />
-</Annotations>
+

⚠ Example 118: Continuing example 117, assume a custom aggregate MultiParentWeightedTotal that computes the total sales amount weighted by the SalesOrganizationRelation/Weight properties along the @Aggregation.UpPath#MultiParentHierarchy of a sales organization:

+
<Annotations Target="SalesData.Sales">
+  <Annotation Term="Aggregation.CustomAggregate"
+    Qualifier="MultiParentWeightedTotal" String="Edm.Decimal" />
+</Annotations>

Then rolluprecursive can be used to aggregate the weighted sales amounts with the request below. The traverse transformation produces an output set \(H'\) in which sales organizations with multiple parents occur multiple times. For each occurrence \(x\) in \(H'\), the rolluprecursive algorithm determines a sales collection \(F(x)\) and the custom aggregate MultiParentWeightedTotal evaluates the path SalesOrganization/@Aggregation.UpPath#MultiParentHierarchy relative to that collection:

GET /service/Sales?$apply=groupby(
     (rolluprecursive(
@@ -4335,62 +4299,62 @@ 

example data there are sales of 10 in Atlantis. Then 60% of them would contribute to the US sales organization and 40% to the EMEA sales organization. Without the weights, all duplicate nodes would contribute the same aggregate result, therefore this example only makes sense in connection with a custom aggregate that considers the weights.

Note that rolluprecursive must preserve the preorder established by traverse:

-
{
-  "@context": "$metadata#Sales(SalesOrganization(),MultiParentWeightedTotal)",
-  "value": [
-    { "SalesOrganization": { "ID": "Sales", "Name": "Corporate Sales",
-        "@Aggregation.UpPath#MultiParentHierarchy": [ ] },
-      "MultiParentWeightedTotal": 34 },
-    { "SalesOrganization": { "ID": "US", "Name": "US",
-        "@Aggregation.UpPath#MultiParentHierarchy": [ "Sales" ] },
-      "MultiParentWeightedTotal": 25 },
-    { "SalesOrganization": { "ID": "Atlantis", "Name": "Atlantis",
-        "@Aggregation.UpPath#MultiParentHierarchy": [ "US", "Sales" ] },
-      "MultiParentWeightedTotal": 6 },
-    ...
-    { "SalesOrganization": { "ID": "EMEA", "Name": "EMEA",
-        "@Aggregation.UpPath#MultiParentHierarchy": [ "Sales" ] },
-      "MultiParentWeightedTotal": 9 },
-    { "SalesOrganization": { "ID": "Atlantis", "Name": "Atlantis",
-        "@Aggregation.UpPath#MultiParentHierarchy": [ "EMEA", "Sales" ] },
-      "MultiParentWeightedTotal": 4 },
-    ...
-  ]
-}
+
{
+  "@context": "$metadata#Sales(SalesOrganization(),MultiParentWeightedTotal)",
+  "value": [
+    { "SalesOrganization": { "ID": "Sales", "Name": "Corporate Sales",
+        "@Aggregation.UpPath#MultiParentHierarchy": [ ] },
+      "MultiParentWeightedTotal": 34 },
+    { "SalesOrganization": { "ID": "US", "Name": "US",
+        "@Aggregation.UpPath#MultiParentHierarchy": [ "Sales" ] },
+      "MultiParentWeightedTotal": 25 },
+    { "SalesOrganization": { "ID": "Atlantis", "Name": "Atlantis",
+        "@Aggregation.UpPath#MultiParentHierarchy": [ "US", "Sales" ] },
+      "MultiParentWeightedTotal": 6 },
+    ...
+    { "SalesOrganization": { "ID": "EMEA", "Name": "EMEA",
+        "@Aggregation.UpPath#MultiParentHierarchy": [ "Sales" ] },
+      "MultiParentWeightedTotal": 9 },
+    { "SalesOrganization": { "ID": "Atlantis", "Name": "Atlantis",
+        "@Aggregation.UpPath#MultiParentHierarchy": [ "EMEA", "Sales" ] },
+      "MultiParentWeightedTotal": 4 },
+    ...
+  ]
+}

7.11 Transformation Sequences

Applying aggregation first covers the most prominent use cases. The slightly more sophisticated question "how much money is earned with small sales" requires filtering the base set before applying the aggregation. To enable this type of question several transformations can be specified in $apply in the order they are to be applied, separated by a forward slash.

-

Example 120:

+

Example 119:

GET /service/Sales?$apply=filter(Amount le 1)
     /aggregate(Amount with sum as Total)

means "filter first, then aggregate", and results in

-
{
-  "@context": "$metadata#Sales(Total)",
-  "value": [
-    { "Total@type": "Decimal", "Total": 2 }
-  ]
-}
+
{
+  "@context": "$metadata#Sales(Total)",
+  "value": [
+    { "Total@type": "Decimal", "Total": 2 }
+  ]
+}

Using filter within $apply does not preclude using it as a normal system query option.

-

Example 121:

+

Example 120:

GET /service/Sales?$apply=filter(Amount le 2)/groupby((Product/Name),
                                          aggregate(Amount with sum as Total))
            &$filter=Total ge 4

results in

-
{
-  "@context": "$metadata#Sales(Product(Name),Total)",
-  "value": [
-    { "Product": { "Name": "Paper" },
-      "Total@type": "Decimal", "Total": 4 },
-    { "Product": { "Name": "Sugar" },
-      "Total@type": "Decimal", "Total": 4 }
-  ]
-}
-
-
-

Example 122: Revisiting example 16 for using the from keyword with the aggregate function, the request

+
{
+  "@context": "$metadata#Sales(Product(Name),Total)",
+  "value": [
+    { "Product": { "Name": "Paper" },
+      "Total@type": "Decimal", "Total": 4 },
+    { "Product": { "Name": "Sugar" },
+      "Total@type": "Decimal", "Total": 4 }
+  ]
+}
+
+
+

Example 121: Revisiting example 16 for using the from keyword with the aggregate function, the request

GET /service/Sales?$apply=aggregate(Amount from Time with average
                                     as DailyAverage)

could be rewritten in a more procedural way using a transformation sequence returning the same result

@@ -4399,30 +4363,30 @@

-

Example 123: getting the population per country with

+

Example 122: getting the population per country with

GET /service/Cities?$apply=groupby((Continent/Name,Country/Name),
                             aggregate(Population with sum as TotalPopulation))

results in

-
{
-  "@context": "$metadata#Cities(Continent(Name),Country(Name),
-                                TotalPopulation)",
-  "value": [
-    { "Continent": { "Name": "Asia" }, "Country": { "Name": "China" },
-      "TotalPopulation@type": "Int32", "TotalPopulation": 1412000000 },
-    { "Continent": { "Name": "Asia" }, "Country": { "Name": "India" },
-      "TotalPopulation@type": "Int32", "TotalPopulation": 1408000000 },
-    ...
-  ]
-}
-

-
-

Example 124: all countries with megacities and their continents

+
{
+  "@context": "$metadata#Cities(Continent(Name),Country(Name),
+                                TotalPopulation)",
+  "value": [
+    { "Continent": { "Name": "Asia" }, "Country": { "Name": "China" },
+      "TotalPopulation@type": "Int32", "TotalPopulation": 1412000000 },
+    { "Continent": { "Name": "Asia" }, "Country": { "Name": "India" },
+      "TotalPopulation@type": "Int32", "TotalPopulation": 1408000000 },
+    ...
+  ]
+}
+
+
+

Example 123: all countries with megacities and their continents

GET /service/Cities?$apply=filter(Population ge 10000000)
                    /groupby((Continent/Name,Country/Name),
                             aggregate(Population with sum as TotalPopulation))
-

Example 128: assuming that Amount is a custom aggregate in addition to the property, determine the total for countries with an Amount greater than 1000

+

Example 127: assuming that Amount is a custom aggregate in addition to the property, determine the total for countries with an Amount greater than 1000

GET /service/SalesOrders?$apply=
   groupby((Customer/Country),aggregate(Amount))
   /filter(Amount gt 1000)
   /aggregate(Amount)
-

Example 129: The output set of the concat transformation contains Sales entities multiple times with conflicting related AugmentedProduct entities that cannot be aggregated by the second transformation.

+

Example 128: The output set of the concat transformation contains Sales entities multiple times with conflicting related AugmentedProduct entities that cannot be aggregated by the second transformation.

GET /service/Sales?$apply=
   concat(addnested(Product,compute(0.1 as Discount) as AugmentedProduct),
          addnested(Product,compute(0.2 as Discount) as AugmentedProduct))
@@ -4470,25 +4434,25 @@ 

-

Example 130: The nest transformation can be used inside groupby to produce one or more collection-valued properties per group.

+

Example 129: The nest transformation can be used inside groupby to produce one or more collection-valued properties per group.

GET /service/Sales?$apply=groupby((Product/Category/ID),
                       nest(groupby((Customer/ID)) as Customers))

results in

-
{
-  "@context":"$metadata#Sales(Product(Category(ID)),Customers())",
-  "value": [
-    { "Product": { "Category": { "ID": "PG1" } },
-      "Customers@context": "#Sales(Customer(ID))",
-      "Customers": [ { "Customer": { "ID": "C1" } },
-                     { "Customer": { "ID": "C2" } },
-                     { "Customer": { "ID": "C3" } } ] },
-    { "Product": { "Category": { "ID": "PG2" } },
-      "Customers@context": "#Sales(Customer(ID))",
-      "Customers": [ { "Customer": { "ID": "C1" } },
-                     { "Customer": { "ID": "C2" } },
-                     { "Customer": { "ID": "C3" } } ] }
-  ]
-}
+
{
+  "@context":"$metadata#Sales(Product(Category(ID)),Customers())",
+  "value": [
+    { "Product": { "Category": { "ID": "PG1" } },
+      "Customers@context": "#Sales(Customer(ID))",
+      "Customers": [ { "Customer": { "ID": "C1" } },
+                     { "Customer": { "ID": "C2" } },
+                     { "Customer": { "ID": "C3" } } ] },
+    { "Product": { "Category": { "ID": "PG2" } },
+      "Customers@context": "#Sales(Customer(ID))",
+      "Customers": [ { "Customer": { "ID": "C1" } },
+                     { "Customer": { "ID": "C2" } },
+                     { "Customer": { "ID": "C3" } } ] }
+  ]
+}


8 Conformance

diff --git a/docs/odata-data-aggregation-ext/odata-data-aggregation-ext.md b/docs/odata-data-aggregation-ext/odata-data-aggregation-ext.md index cbda2cf7..03448c58 100644 --- a/docs/odata-data-aggregation-ext/odata-data-aggregation-ext.md +++ b/docs/odata-data-aggregation-ext/odata-data-aggregation-ext.md @@ -1031,7 +1031,7 @@ The definitions of italicized terms made in this section are used throughout thi ### 3.1.1 Type, Structure and Context URL -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](#ODataProtocol). Individual instances in an input or output set can have a subtype of the input type. (See [example 75](#subinputtype).) 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](#ODataProtocol). Individual instances in an input or output set can have a subtype of the input type. (See [example 74](#subinputtype).) 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: - Declared properties of the input type or a nested or related type thereof or of a subtype of one of these MUST have their declared type and meaning when they occur in an input or output set. @@ -1048,7 +1048,7 @@ Here is an overview of the structural changes made by different transformations: 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](#ODataCSDL) 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](#ODataProtocol) MUST describe only properties that are present or annotated as absent (for example, if `Core.Permissions` is `None` [OData-Protocol, section 11.2.2](#ODataProtocol)) 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](#ODataVocCore). (See [example 76](#anystructure).) +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](#ODataProtocol) MUST describe only properties that are present or annotated as absent (for example, if `Core.Permissions` is `None` [OData-Protocol, section 11.2.2](#ODataProtocol)) 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](#ODataVocCore). (See [example 75](#anystructure).) ### 3.1.2 Sameness and Order @@ -1082,7 +1082,7 @@ The output set of a [basic aggregation](#BasicAggregation) transformation can co - both are instances of entity types without entity id (transient entities, see [OData-Protocol, section 4.3](#ODataProtocol)) and both are null or both have the same structure and same values with null considered different from absent (informally speaking, they are compared like complex instances) or - (1) both are instances of the same entity type with the same entity id (non-transient entities, see [OData-Protocol, section 4.1](#ODataProtocol)) and (2) the structural and navigation properties contained in both have the same values (for non-primitive properties the sameness of values is decided by a recursive invocation of this definition). - If this is fulfilled, the instances are called _complementary representations of the same non-transient entity_. If this case is encountered at some recursion level while the sameness of non-transient entities $u_1$ and $u_2$ is established, a merged representation of the entity $u_1=u_2$ exists that contains all properties of $u_1$ and $u_2$. But if the instances both occur in the last output set, services MUST represent each with its own structure in the response payload. - - If the first condition is fulfilled but not the second, the instances are not the same and are called _contradictory representations of the same non-transient entity_. ([Example 104](#contradict) describes a use case for this.) + - If the first condition is fulfilled but not the second, the instances are not the same and are called _contradictory representations of the same non-transient entity_. ([Example 103](#contradict) describes a use case for this.) Collections are _the same_ if there is a one-to-one correspondence $f$ between them such that - corresponding occurrences are of the same value and @@ -1136,7 +1136,7 @@ _Determination of $A$:_ Let $I$ be the input set. If $p$ is absent, let $A=I$ with null values removed. 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](#EvaluationofDataAggregationPaths): -- If $q$ is non-empty, let $E=\Gamma(I,q)$ and remove duplicates from that entity collection: If [multiple representations of the same non-transient entity](#SamenessandOrder) are reached, the service MUST merge them into one occurrence in $E$ if they are complementary and MUST reject the request if they are contradictory. (See [example 129](#aggrconflict).) If [multiple occurrences of the same transient entity](#SamenessandOrder) are reached, the service MUST keep only one occurrence in $E$. +- If $q$ is non-empty, let $E=\Gamma(I,q)$ and remove duplicates from that entity collection: If [multiple representations of the same non-transient entity](#SamenessandOrder) are reached, the service MUST merge them into one occurrence in $E$ if they are complementary and MUST reject the request if they are contradictory. (See [example 128](#aggrconflict).) If [multiple occurrences of the same transient entity](#SamenessandOrder) are reached, the service MUST keep only one occurrence in $E$. - If $q$ is empty, let $E=I$. 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. @@ -1449,7 +1449,7 @@ The `groupby` transformation takes one or two parameters where the second is a l #### 3.2.3.1 Simple Grouping -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](#DataAggregationPath) 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](#groupbynav)). +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](#DataAggregationPath) 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](#groupbynav)). 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. @@ -2458,9 +2458,9 @@ A _root node_ is a node without parent nodes. A recursive hierarchy can have one 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 _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. 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](#Transformationtraverse). +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](#Transformationtraverse). #### 5.5.2.1 Hierarchy Functions @@ -2826,15 +2826,15 @@ The definition of $σ(x)$ makes use of a function $a(ε,t,x)$, which returns a s Three cases are distinguished: 1. _Case where the recursive hierarchy is defined on the input set_ This case applies if the paths $p$ and $q$ are equal. Let $σ(x)=x$ and let $G$ be a list containing all structural and navigation properties of the entity type of $H$. - In this case $\Pi_G(σ(x))$ injects all properties of $x$ into the instances of the output set. (See [example 66](#caseone).) + In this case $\Pi_G(σ(x))$ injects all properties of $x$ into the instances of the output set. (See [example 65](#caseone).) 2. _Case where the recursive hierarchy is defined on the related entity type addressed by a navigation property path_ This case applies if $p'$ is a non-empty navigation property path and $p''$ an optional type-cast segment such that $p$ equals the concatenated path $p'/p''/q$. Let $σ(x)=a(ε,p'/p'',x)$ and let $G=(p')$. - In this case $\Pi_G(σ(x))$ injects the whole related entity $x$ into the instances of the output set. The navigation property path $p'$ is expanded by default. (See [example 67](#rollupnode).) + In this case $\Pi_G(σ(x))$ injects the whole related entity $x$ into the instances of the output set. The navigation property path $p'$ is expanded by default. (See [example 66](#rollupnode).) 3. _Case where the recursive hierarchy is related to the input set only through equality of node identifiers, not through navigation_ If neither case 1 nor case 2 applies, let $σ(x)=a(ε,p,x[q])$ and let $G=(p)$. In this case $\Pi_G(σ(x))$ injects only the node identifier of $x$ into the instances of the output set. -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](#pathequals)). +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](#pathequals)). The function $a(u,t,x)$ takes an instance, a path and another instance as arguments and is defined recursively as follows: 1. If $u$ equals the special symbol $ε$, set $u$ to a new instance of the [input type](#TypeStructureandContextURL) without properties and without entity id. @@ -2846,7 +2846,7 @@ The function $a(u,t,x)$ takes an instance, a path and another instance as argume 7. If $t_1$ is collection-valued, let $u[t_1]$ be a collection consisting of one item $x'$. 8. Return $u$. -(See [example 113](#traversecoll).) +(See [example 112](#traversecoll).) #### 6.2.2.1 Standard Case of `traverse` @@ -2899,7 +2899,7 @@ results in #### 6.2.2.2 General Case of `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](#weight)). +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](#weight)). More precisely, in the general case every node $y$ is annotated with the term `UpPath` from the `Aggregation` vocabulary [OData-VocAggr](#ODataVocAggr). 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. @@ -2939,60 +2939,14 @@ Given a start node $x$, let $ρ_0(x)$ be the node $x$ with the annotation $ρ_0( 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 -⚠ 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 -```json -{ - "@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](#SimpleGrouping): -- If $s$ is annotated with `Aggregation.UpPath` or `Aggregation.Cycle` and qualifier $Q$, copy these annotations from $s$ to $u$. +- If $s$ is annotated with `Aggregation.UpPath` and qualifier $Q$, copy this annotation from $s$ to $u$. Recall that instance annotations never appear in [data aggregation paths](#DataAggregationPath) or [aggregatable expressions](#AggregatableExpression). They are not considered when determining whether instances of structured types are [the same](#SamenessandOrder), 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 +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))).$$ @@ -3013,9 +2967,9 @@ Let $T$ be a transformation sequence, $P_1$ stand in for zero or more property p _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](#rollupnode)). $Z_N$ is a transformation whose output set is its input set with property $χ_N$ removed. +A property $χ_N$ appears in the algorithm, but is not present in the output set. It is explained later (see [example 66](#rollupnode)). $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`](#Transformationtraverse) transformation, as in [example 119](#weighted), 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 +Let $x_1,…,x_n$ be the nodes in $H'$, possibly with repetitions. If the optional transformation sequence $S$ ends with a [`traverse`](#Transformationtraverse) transformation, as in [example 118](#weighted), 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. @@ -3036,12 +2990,12 @@ $$\matrix{ F(x)={\tt filter}(\hbox{\tt Aggregation.isdescendant}(\hfill\\ \quad 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](#rollupcoll) for a case with $k=1$.) +where $y_1,…,y_k$ denote `lambdaVariableExpr`s and ${}/r$ may be absent. (See [example 113](#rollupcoll) 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 -Example 66: Total number of sub-organizations for all organizations in the hierarchy defined in [Hierarchy Examples](#HierarchyExamples) with $p=q={\tt ID}$ (case 1 of the [definition](#Transformationtraverse) 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](#HierarchyExamples) with $p=q={\tt ID}$ (case 1 of the [definition](#Transformationtraverse) 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( @@ -3076,7 +3030,7 @@ results in 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](#ODataVocAggr), 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 -⚠ Example 67: Total sales amounts per organization, both including and excluding sub-organizations, in the US sub-hierarchy defined in [Hierarchy Examples](#HierarchyExamples) with $p=p'/q={\tt SalesOrganization}/{\tt ID}$ and $p'={\tt SalesOrganization}$ (case 2 of the [definition](#Transformationtraverse) 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](#HierarchyExamples) with $p=p'/q={\tt SalesOrganization}/{\tt ID}$ and $p'={\tt SalesOrganization}$ (case 2 of the [definition](#Transformationtraverse) 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( @@ -3112,7 +3066,7 @@ results in ::: ::: example -⚠ 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"). +⚠ 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: ``` @@ -3165,7 +3119,7 @@ results in ::: ::: example -⚠ 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](#Transformationtraverse) 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](#Transformationtraverse) 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( @@ -3201,7 +3155,7 @@ The following examples show some common aggregation-related questions that can b Grouping without specifying a set transformation returns the distinct combination of the grouping properties. ::: example -Example 70: +Example 69: ``` GET /service/Customers?$apply=groupby((Name)) ``` @@ -3223,7 +3177,7 @@ Note that "Sue" appears only once although the customer base contains two differ Aggregation is also possible across related entities. ::: example -Example 71: customers that bought something +Example 70: customers that bought something ``` GET /service/Sales?$apply=groupby((Customer/Name)) ``` @@ -3246,7 +3200,7 @@ However, even though both Sues bought products, only one "Sue" appears in the ag ::: ::: example -Example 72: +Example 71: ``` GET /service/Sales?$apply=groupby((Customer/Name,Customer/ID)) ``` @@ -3269,7 +3223,7 @@ GET /service/Sales?$apply=groupby((Customer)) ::: ::: example -Example 73: Grouping by navigation property `Customer` +Example 72: Grouping by navigation property `Customer` ``` GET /service/Sales?$apply=groupby((Customer)) @@ -3288,7 +3242,7 @@ results in ::: ::: example -Example 74: the first question in the motivating example in [section 2.3](#ExampleUseCases), which customers bought which products, can now be expressed as +Example 73: the first question in the motivating example in [section 2.3](#ExampleUseCases), which customers bought which products, can now be expressed as ``` GET /service/Sales?$apply=groupby((Customer/Name,Customer/ID,Product/Name)) ``` @@ -3317,7 +3271,7 @@ and results in ::: ::: example -⚠ Example 75: grouping by properties of subtypes +⚠ Example 74: grouping by properties of subtypes ``` GET /service/Products?$apply=groupby((SalesModel.FoodProduct/Rating, SalesModel.NonFoodProduct/RatingClass)) @@ -3338,7 +3292,7 @@ results in ::: ::: example -⚠ Example 76: grouping by a property of a subtype +⚠ Example 75: grouping by a property of a subtype ``` GET /service/Products?$apply=groupby((SalesModel.FoodProduct/Rating)) ``` @@ -3360,7 +3314,7 @@ results in a third group representing entities with no `SalesModel.FoodProduct/R The client may specify one of the predefined aggregation methods [`min`](#StandardAggregationMethodmin), [`max`](#StandardAggregationMethodmax), [`sum`](#StandardAggregationMethodsum), [`average`](#StandardAggregationMethodaverage), and [`countdistinct`](#StandardAggregationMethodcountdistinct), or a [custom aggregation method](#CustomAggregationMethods), to aggregate an [aggregatable expression](#AggregatableExpression). Expressions defining an aggregate method specify an [alias](#Keywordas). The aggregated values are returned in a dynamic property whose name is determined by the alias. ::: example -Example 77: +Example 76: ``` GET /service/Products?$apply=groupby((Name), aggregate(Sales/Amount with sum as Total)) @@ -3382,7 +3336,7 @@ Note that the base set of the request is `Products`, so there is a result item f ::: ::: example -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) @@ -3410,7 +3364,7 @@ results in ::: ::: example -Example 79: To compute the aggregate as a property without nesting, use the aggregate function in `$compute` rather than the aggregate transformation in `$apply`: +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 ``` @@ -3435,7 +3389,7 @@ The expression `$it/Sales` refers to the sales of the current product. Without ` ::: ::: example -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)) @@ -3463,7 +3417,7 @@ Applying `outerjoin` instead would return an additional entity for product with ::: ::: example -Example 81: +Example 80: ``` GET /service/Sales?$apply=groupby((Customer/Country), aggregate(Amount with average as AverageAmount)) @@ -3484,7 +3438,7 @@ Here the `AverageAmount` is of type `Edm.Double`. ::: ::: example -Example 82: `$count` after navigation property +Example 81: `$count` after navigation property ``` GET /service/Products?$apply=groupby((Name), aggregate(Sales/$count as SalesCount)) @@ -3506,7 +3460,7 @@ results in To place the number of instances in a group next to other aggregated values, the aggregate expression [`$count`](#AggregateExpressioncount) can be used: ::: example -⚠ 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, @@ -3540,7 +3494,7 @@ results in The `aggregate` function can not only be used in `$compute` but also in `$filter` and `$orderby`: ::: example -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 ``` @@ -3557,7 +3511,7 @@ results in ::: ::: example -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 ``` @@ -3576,7 +3530,7 @@ results in ::: ::: example -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 @@ -3608,7 +3562,7 @@ results in ::: ::: example -Example 87: Product categories with at least one product having an aggregated sales amount greater than 10 +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) @@ -3627,7 +3581,7 @@ results in The `aggregate` function can also be applied inside `$apply`: ::: example -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)) @@ -3652,7 +3606,7 @@ results in ::: ::: example -Example 89: rule 1 for [keyword `from`](#Keywordfrom) applied repeatedly +Example 88: rule 1 for [keyword `from`](#Keywordfrom) applied repeatedly ``` GET /service/Sales?$apply=aggregate(Amount with sum from Time with average @@ -3679,7 +3633,7 @@ GET /service/Sales?$apply= ## 7.3 Requesting Expanded Results ::: example -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, @@ -3711,7 +3665,7 @@ results in `addnested` transformations can be nested. ::: example -Example 91: nested `addnested` transformations +Example 90: nested `addnested` transformations ``` GET /service/Categories?$apply= addnested(Products, @@ -3762,7 +3716,7 @@ results in the response before without the FilteredSales dynamic navigation prop ::: ::: example -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) @@ -3801,7 +3755,7 @@ results in ::: ::: example -Example 93: use `outerjoin` to split up collection-valued navigation properties for grouping +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)) @@ -3839,7 +3793,7 @@ Custom aggregates are defined through the [`CustomAggregate`](#CustomAggregates) A custom aggregate can be used by specifying the name of the custom aggregate in the [`aggregate`](#Transformationaggregate) clause. ::: example -Example 94: +Example 93: ``` GET /service/Sales?$apply=groupby((Customer/Country), aggregate(Amount with sum as Actual,Forecast)) @@ -3863,7 +3817,7 @@ results in 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 -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)) ``` @@ -3880,7 +3834,7 @@ results in ::: ::: example -Example 96: illustrates rule 1 for [keyword `from`](#Keywordfrom): maximal sales forecast for a product +Example 95: illustrates rule 1 for [keyword `from`](#Keywordfrom): maximal sales forecast for a product ``` GET /service/Sales?$apply=aggregate(Forecast from Product with max as MaxProductForecast) @@ -3894,7 +3848,7 @@ GET /service/Sales?$apply= ::: ::: example -Example 97: illustrates rule 2 for [keyword `from`](#Keywordfrom): the forecast is computed in two steps +Example 96: illustrates rule 2 for [keyword `from`](#Keywordfrom): the forecast is computed in two steps ``` GET /service/Sales?$apply=aggregate(Forecast from Product as ProductForecast) ``` @@ -3907,7 +3861,7 @@ GET /service/Sales?$apply= ::: ::: example -Example 98: illustrates rule 1 followed by rule 2 for [keyword `from`](#Keywordfrom): a forecast based on the average daily forecasts per country +Example 97: illustrates rule 1 followed by rule 2 for [keyword `from`](#Keywordfrom): a forecast based on the average daily forecasts per country ``` GET /service/Sales?$apply=aggregate(Forecast from Time with average from Customer/Country @@ -3928,7 +3882,7 @@ GET /service/Sales?$apply= A property can be aggregated in multiple ways, each with a different alias. ::: example -Example 99: +Example 98: ``` GET /service/Sales?$apply=groupby((Customer/Country), aggregate(Amount with sum as Total, @@ -3953,7 +3907,7 @@ results in The introduced dynamic property is added to the context where the aggregate expression is applied to: ::: example -Example 100: +Example 99: ``` GET /service/Products?$apply=groupby((Name), aggregate(Sales/Amount with sum as Total)) @@ -3989,7 +3943,7 @@ results in There is no hard distinction between groupable and aggregatable properties: the same property can be aggregated and used to group the aggregated results. ::: example -Example 101: +Example 100: ``` GET /service/Sales?$apply=groupby((Amount),aggregate(Amount with sum as Total)) ``` @@ -4012,7 +3966,7 @@ will return all distinct amounts appearing in sales orders and how much money wa Dynamic property names may be reused in different transformation sequences passed to `concat`. ::: example -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), @@ -4044,7 +3998,7 @@ results in ::: ::: example -Example 103: transformation sequences are also useful inside `groupby`: Aggregate the amount by only considering the top two sales amounts per product and country: +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)) @@ -4075,7 +4029,7 @@ results in ::: ::: example -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), @@ -4107,7 +4061,7 @@ In the result, `Sales` entities 4 and 6 occur twice each with contradictory valu ## 7.7 Model Functions as Set Transformations ::: example -Example 105: As a variation of [example 102](#bestselling), 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](#bestselling), 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: ```xml @@ -4155,7 +4109,7 @@ Note that these two entities get their values for the Country property from the For a leveled hierarchy, consumers may specify a different aggregation method per level for every property passed to [`rollup`](#Groupingwithrollup) as a hierarchy level below the root level. ::: example -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: ``` @@ -4175,7 +4129,7 @@ GET /service/Sales?$apply=groupby((Product/ID,Product/Name), ::: ::: example -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)), @@ -4222,7 +4176,7 @@ 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 -Example 108: Total sales amounts for sales orgs in 'US' in the `SalesOrgHierarchy` defined in [Hierarchy Examples](#HierarchyExamples) +Example 107: Total sales amounts for sales orgs in 'US' in the `SalesOrgHierarchy` defined in [Hierarchy Examples](#HierarchyExamples) ``` GET /service/Sales?$apply= descendants( @@ -4257,7 +4211,7 @@ Note that this example returns the actual total sums regardless of whether the ` The order of transformations becomes relevant if `groupby` with `rolluprecursive` shall aggregate over a thinned-out hierarchy, like here: ::: example -Example 109: Number of Paper sales per sales org aggregated along the the `SalesOrgHierarchy` defined in [Hierarchy Examples](#HierarchyExamples) +Example 108: Number of Paper sales per sales org aggregated along the the `SalesOrgHierarchy` defined in [Hierarchy Examples](#HierarchyExamples) ``` GET /service/Sales?$apply= filter(Product/Name eq 'Paper') @@ -4295,7 +4249,7 @@ results in ::: ::: example -⚠ Example 110: The input set `Sales` is filtered along a hierarchy on a related entity (navigation property `SalesOrganization`) before an aggregation +⚠ 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, @@ -4319,7 +4273,7 @@ GET /service/SalesOrganizations?$apply= ::: ::: example -⚠ 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, @@ -4358,7 +4312,7 @@ GET /service/Sales?$apply= ::: ::: example -Example 112: Return the result of [example 67](#rollupnode) in preorder +Example 111: Return the result of [example 66](#rollupnode) in preorder ``` GET /service/Sales?$apply=groupby( (rolluprecursive( @@ -4400,7 +4354,7 @@ results in ::: ::: example -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}$. +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, @@ -4440,7 +4394,7 @@ The result contains multiple instances of the same `Product` that differ in thei ::: ::: example -Example 114: Aggregation along a hierarchy with 1:N relationship: Sold products per sales organization +Example 113: Aggregation along a hierarchy with 1:N relationship: Sold products per sales organization ``` GET /service/Products?$apply= groupby((rolluprecursive( @@ -4472,7 +4426,7 @@ results in ::: ::: example -⚠ 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: +⚠ 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: ProductCategory|parent ProductCategory|associated SalesOrganizations ---------------|----------------------|----------------------------- @@ -4538,7 +4492,7 @@ works differently: `descendants` is the parameter $T$ of `ancestors` and operate Besides changes to the structural properties of the entities in a hierarchical collection, hierarchy maintenance involves changes to the parent-child relationships. ::: example -Example 116: Move a sales organization Switzerland under the parent EMEA Central by binding the parent navigation property to EMEA Central [OData-JSON, section 8.5](#ODataJSON): +Example 115: Move a sales organization Switzerland under the parent EMEA Central by binding the parent navigation property to EMEA Central [OData-JSON, section 8.5](#ODataJSON): ```json PATCH /service/SalesOrganizations('Switzerland') Content-Type: application/json @@ -4561,7 +4515,7 @@ DELETE /service/SalesOrganizations('Switzerland')/Superordinate/$ref ::: ::: example -Example 117: If the parent navigation property contained a referential constraint for the key of the target [OData-CSDL, section 8.5](#ODataCSDL), +Example 116: If the parent navigation property contained a referential constraint for the key of the target [OData-CSDL, section 8.5](#ODataCSDL), ```xml @@ -4589,7 +4543,7 @@ Content-Type: application/json If the parent-child relationship between sales organizations is maintained in a separate entity set, a node can have multiple parents, with additional information on each parent-child relationship. ::: example -⚠ Example 118: Assume the relation from a node to its parent nodes contains a weight: +⚠ Example 117: Assume the relation from a node to its parent nodes contains a weight: ```xml @@ -4632,7 +4586,7 @@ Atlantis|US|0.6 Atlantis|EMEA|0.4 Phobos|Mars|1 -Then Atlantis is a node with two parents. The standard hierarchical transformations disregard the weight property and consider both parents equally valid (but see [example 119](#weighted)). +Then Atlantis is a node with two parents. The standard hierarchical transformations disregard the weight property and consider both parents equally valid (but see [example 118](#weighted)). In a traversal with start node Sales only, Mars and Phobos cannot be reached and hence are orphans: ``` @@ -4649,7 +4603,7 @@ Content-Type: application/json { "Superordinate": { "@id": "SalesOrganizations('Sales')" } } ``` -Since this example contains no referential constraint, there is no analogy to [example 117](#refconstr). The alias `SuperordinateID` cannot be used in the payload, the following request is invalid: +Since this example contains no referential constraint, there is no analogy to [example 116](#refconstr). The alias `SuperordinateID` cannot be used in the payload, the following request is invalid: ```json POST /service/SalesOrganizations('Mars')/Relations Content-Type: application/json @@ -4664,7 +4618,7 @@ DELETE /service/SalesOrganizations('Mars')/Relations('Sales') ::: ::: example -⚠ Example 119: Continuing [example 118](#weight), assume a [custom aggregate](#CustomAggregates) `MultiParentWeightedTotal` that computes the total sales amount weighted by the `SalesOrganizationRelation/Weight` properties along the `@Aggregation.UpPath#MultiParentHierarchy` of a sales organization: +⚠ Example 118: Continuing [example 117](#weight), assume a [custom aggregate](#CustomAggregates) `MultiParentWeightedTotal` that computes the total sales amount weighted by the `SalesOrganizationRelation/Weight` properties along the `@Aggregation.UpPath#MultiParentHierarchy` of a sales organization: ```xml 129: The output set of the `concat` transformation contains `Sales` entities multiple times with conflicting related `AugmentedProduct` entities that cannot be aggregated by the second transformation. +Example 128: The output set of the `concat` transformation contains `Sales` entities multiple times with conflicting related `AugmentedProduct` entities that cannot be aggregated by the second transformation. ``` GET /service/Sales?$apply= concat(addnested(Product,compute(0.1 as Discount) as AugmentedProduct), @@ -4871,7 +4825,7 @@ results in an error. ::: ::: example -Example 130: The `nest` transformation can be used inside `groupby` to produce one or more collection-valued properties per group. +Example 129: The `nest` transformation can be used inside `groupby` to produce one or more collection-valued properties per group. ``` GET /service/Sales?$apply=groupby((Product/Category/ID), nest(groupby((Customer/ID)) as Customers)) diff --git a/odata-data-aggregation-ext/5 Vocabulary for Data Aggregation.md b/odata-data-aggregation-ext/5 Vocabulary for Data Aggregation.md index 99a6cb69..775e9569 100644 --- a/odata-data-aggregation-ext/5 Vocabulary for Data Aggregation.md +++ b/odata-data-aggregation-ext/5 Vocabulary for Data Aggregation.md @@ -188,9 +188,9 @@ A _root node_ is a node without parent nodes. A recursive hierarchy can have one 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 _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. 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 ##Transformationtraverse]. +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 ##Transformationtraverse]. #### ##subsubsubsec Hierarchy Functions diff --git a/odata-data-aggregation-ext/6 Hierarchical Transformations.md b/odata-data-aggregation-ext/6 Hierarchical Transformations.md index 22606737..c5fe2972 100644 --- a/odata-data-aggregation-ext/6 Hierarchical Transformations.md +++ b/odata-data-aggregation-ext/6 Hierarchical Transformations.md @@ -322,60 +322,14 @@ Given a start node $x$, let $ρ_0(x)$ be the node $x$ with the annotation $ρ_0( 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 -⚠ Example ##ex: If the child of Atlantis is also a parent of Atlantis: -``` -GET /service/SalesOrganizations?$apply= - /traverse($root/SalesOrganizations,MultiParentHierarchy,ID,preorder) -``` -results in -```json -{ - "@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](#SimpleGrouping): -- If $s$ is annotated with `Aggregation.UpPath` or `Aggregation.Cycle` and qualifier $Q$, copy these annotations from $s$ to $u$. +- If $s$ is annotated with `Aggregation.UpPath` and qualifier $Q$, copy this annotation from $s$ to $u$. Recall that instance annotations never appear in [data aggregation paths](#DataAggregationPath) or [aggregatable expressions](#AggregatableExpression). They are not considered when determining whether instances of structured types are [the same](#SamenessandOrder), 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 +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))).$$