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 b07d0837f..533c6c1db 100644 --- a/docs/odata-data-aggregation-ext/odata-data-aggregation-ext.html +++ b/docs/odata-data-aggregation-ext/odata-data-aggregation-ext.html @@ -95,7 +95,7 @@
https://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/csd04/odata-data-aggregation-ext-v4.0-csd04.md (Authoritative)
@@ -157,7 +157,7 @@
When referencing this specification the following citation format should be used:
[OData-Data-Agg-v4.0]
-OData Extension for Data Aggregation Version 4.0. Edited by Ralf Handl, Hubert Heijkers, Gerald Krause, Michael Pizzo, Heiko Theißen, and Martin Zurmuehl. 14 June 2023. OASIS Committee Specification Draft 01. https://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/csd04/odata-data-aggregation-ext-v4.0-csd04.html. Latest stage: https://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/odata-data-aggregation-ext-v4.0.html.
+OData Extension for Data Aggregation Version 4.0. Edited by Ralf Handl, Hubert Heijkers, Gerald Krause, Michael Pizzo, Heiko Theißen, and Martin Zurmuehl. 28 June 2023. OASIS Committee Specification Draft 01. https://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/csd04/odata-data-aggregation-ext-v4.0-csd04.html. Latest stage: https://docs.oasis-open.org/odata/odata-data-aggregation-ext/v4.0/odata-data-aggregation-ext-v4.0.html.
Copyright © OASIS Open 2023. All Rights Reserved.
Distributed under the terms of the OASIS IPR Policy.
@@ -274,7 +274,11 @@rolluprecursive
This specification defines the following terms:
aggregate
transformation or function defined in section 3.2.1.1Edm.Stream
or subtypes of Edm.Geography
or Edm.Geometry
/
). Segments are names of declared or dynamic structural or navigation properties, or type-cast segments consisting of the (optionally qualified) name of a structured type that is derived from the type identified by the preceding path segment to reach properties declared by the derived type.The definitions of italicized terms made in this section are used throughout this text, always with a hyperlink to this section.
All input sets and output sets in one transformation sequence are collections of the input type, that is the entity type or complex type of the first input set, or in other words, of the resource to which the transformation sequence is applied. The input type is determined by the entity model element identified within the metadata document by the context URL of that resource OData-Protocol, section 10. Individual instances in an input or output set can have a subtype of the input type. (See example 72.) 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 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.
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 73.)
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.)
Input sets and output sets are not sets of instances in the mathematical sense but collections, because the same instance can occur multiple times in them. In other words: A collection contains values (which can be instances of structured types or primitive values), possibly with repetitions. The occurrences of the values in the collection form a set in the mathematical sense. The cardinality of a collection is the total number of occurrences in it. When this text describes a transformation algorithmically and stipulates that certain steps are carried out for each occurrence in a collection, this means that the steps are carried out multiple times for the same value if it occurs multiple times in the collection.
A collection addressed by the resource path is returned by the service either as an ordered collection OData-Protocol, section 11.4.10 or as an unordered collection. The same applies to collections that are nested in or related to the addressed resource as well as to collections that are the result of evaluating an expression starting with $root
, which occur, for example, as the first parameter of a hierarchical transformation.
Collections are the same if there is a one-to-one correspondence \(f\) between them such that
@@ -1597,7 +1603,7 @@Otherwise, let \(q\) be the portion of \(p\) up to and including the last navigation property, if any, and any type-cast segment that immediately follows, and let \(r\) be the remainder, if any, of \(p\) that contains no navigation properties, such that \(p\) equals the concatenated path \(q⁄r\). The aggregate transformation considers each entity reached via the path \(q\) exactly once. To this end, using the \(\Gamma\) notation:
Then, if \(r\) is empty, let \(A=E\), otherwise let \(A=\Gamma(E,r)\), this consists of instances of structured types or primitive values, possibly with repetitions.
@@ -1816,7 +1822,7 @@groupby
The groupby
transformation takes one or two parameters where the second is a list of set transformations, separated by forward slashes to express that they are consecutively applied. If the second parameter is not specified, it defaults to a single transformation whose output set consists of a single instance of the input type without properties and without entity id.
In its simplest form the first parameter of groupby
specifies the grouping properties, a comma-separated parenthesized list \(G\) of one or more data aggregation paths with single-valued segments. The same path SHOULD NOT appear more than once; redundant property paths MAY be considered valid, but MUST NOT alter the meaning of the request. Navigation properties and stream properties specified in grouping properties are expanded by default (see example 70).
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).
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.
rollup
.
A recursive hierarchy organizes entities of a collection as nodes of one or more tree structures. This structure does not need to be as uniform as a leveled hierarchy. It is described by a complex term RecursiveHierarchy
with these properties:
A recursive hierarchy is defined on a collection of entities by
+The recursive hierarchy is described in the model by an annotation of the entity type with the complex term RecursiveHierarchy
with these properties:
NodeProperty
allows identifying a node in the hierarchy. It MUST be a path with single-valued segments ending in a primitive property.ParentNavigationProperty
allows navigation to the instance or instances representing the parent nodes. It MUST be a collection-valued or nullable single-valued navigation property path that addresses the entity type annotated with this term. Nodes MUST NOT form cycles when following parent navigation properties.NodeProperty
MUST be a path with single-valued segments ending in a primitive property. This property holds the node identifier of an entity that is a node in the hierarchy.ParentNavigationProperty
MUST be a collection-valued or nullable single-valued navigation property path that addresses the entity type annotated with this term. It navigates from an entity that is a node in the hierarchy to its parent nodes.The term RecursiveHierarchy
can only be applied to entity types, and MUST be applied with a qualifier, which is used to reference the hierarchy in transformations operating on recursive hierarchies, in grouping with rolluprecursive
, and in hierarchy functions.
A node is an instance of an entity type annotated with RecursiveHierarchy
. It may have a parent node that is the instance reached via the ParentNavigationProperty
. A recursive hierarchy is a collection of such nodes with unique node identifiers.
A node without parent node is a root node, a node is a child node of its parent node, a node without child nodes is a leaf node. Nodes with the same parent node are sibling nodes and so are root nodes. The descendants of a node are its child nodes, their child nodes, and so on, up to and including all leaf nodes that can be reached. A node together with its descendants forms a sub-hierarchy of the hierarchy. The ancestors of a node are its parent node, the parent of its parent node, and so on, up to and including a root node that can be reached. A recursive hierarchy can have one or more root nodes.
-The term UpNode
can be used in hierarchical result sets to associate with each instance one of its ancestors, which is again annotated with UpNode
and so on until a path to the root is constructed.
The term RecursiveHierarchy
can only be applied to entity types, and MUST be applied with a qualifier, which is used to reference the hierarchy in transformations operating on recursive hierarchies, in 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.
For testing the position of a given entity in a recursive hierarchy, the Aggregation vocabulary OData-VocAggr defines unbound functions. These have
HierarchyNodes
, HierarchyQualifier
where HierarchyNodes
is a collection and HierarchyQualifier
is the qualifier of a RecursiveHierarchy
annotation on its common entity type. The node identifiers in this collection define the recursive hierarchy.Node
that contains the node identifier of the entity to be tested. Note that the test result depends only on this node identifier, not on any other property of the given entityThe following functions are defined:
isroot
tests if the given entity is a root of the hierarchyisdescendant
tests if the given entity is a descendant of an ancestor node (whose node identifier is given in a parameter Ancestor
) with a maximum distance MaxDistance
, or equals the ancestor if IncludeSelf
is trueisancestor
tests if the given entity is an ancestor of a descendant node (whose node identifier is given in a parameter Descendant
) with a maximum distance MaxDistance
, or equals the descendant if IncludeSelf
is trueissibling
tests if the given entity and another entity (whose node identifier is given in a parameter Other
) have the same parent node or both are roots, but are not the sameisleaf
tests if the given entity is without descendants.isroot
tests if the given entity is a root node of the hierarchy.isdescendant
tests if the given entity is a descendant with maximum distance MaxDistance
of an ancestor node (whose node identifier is given in a parameter Ancestor
), or equals the ancestor if IncludeSelf
is true.isancestor
tests if the given entity is an ancestor with maximum distance MaxDistance
of a descendant node (whose node identifier is given in a parameter Descendant
), or equals the descendant if IncludeSelf
is true.issibling
tests if the given entity and another entity (whose node identifier is given in a parameter Other
) are sibling nodes.isleaf
tests if the given entity is a leaf node.Another function rollupnode
is defined that can only be used in connection with rolluprecursive
.
The hierarchy terms can be applied to the Example Data Model.
Example 53: leveled hierarchies for products and time, and a recursive hierarchy for the sales organizations
+⚠ Example 53: leveled hierarchies for products and time, and a recursive hierarchy for the sales organizations:
edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx"
< Version="4.0">
edmx:Reference Uri="https://docs.oasis-open.org/odata/odata-data-
@@ -2604,28 +2617,28 @@ < </Annotation>
Annotations>
</
-Annotations Target="SalesModel.Time">
- <Annotation Term="Aggregation.LeveledHierarchy"
- < Qualifier="TimeHierarchy">
-Collection>
- <PropertyPath>Year</PropertyPath>
- <PropertyPath>Quarter</PropertyPath>
- <PropertyPath>Month</PropertyPath>
- <Collection>
- </Annotation>
- </Annotations>
+ </Annotations Target="SalesModel.Time">
+ <Annotation Term="Aggregation.LeveledHierarchy"
+ < Qualifier="TimeHierarchy">
+Collection>
+ <PropertyPath>Year</PropertyPath>
+ <PropertyPath>Quarter</PropertyPath>
+ <PropertyPath>Month</PropertyPath>
+ <Collection>
+ </Annotation>
+ </Annotations>
</
-Annotations Target="SalesModel.SalesOrganization">
- <Annotation Term="Aggregation.RecursiveHierarchy"
- < Qualifier="SalesOrgHierarchy">
-Record>
- <PropertyValue Property="NodeProperty"
- < PropertyPath="ID" />
-PropertyValue Property="ParentNavigationProperty"
- < PropertyPath="Superordinate" />
-Record>
- </Annotation>
- </Annotations>
+ </Annotations Target="SalesModel.SalesOrganization">
+ <Annotation Term="Aggregation.RecursiveHierarchy"
+ < Qualifier="SalesOrgHierarchy">
+Record>
+ <PropertyValue Property="NodeProperty"
+ < PropertyPath="ID" />
+PropertyValue Property="ParentNavigationProperty"
+ < PropertyPath="Superordinate" />
+Record>
+ </Annotation>
+ </Annotations>
</Schema>
</edmx:DataServices>
</edmx:Edmx> </
The transformations and the rolluprecursive
operator defined in this section are called hierarchical, because they make use of a recursive hierarchy and are defined in terms of hierarchy functions introduced in the previous section.
With the exception of traverse
, the hierarchical transformations do not define an order on the output set. An order can be reinstated by a subsequent orderby
or traverse
transformation or a $orderby
.
With the exceptions of traverse
and rolluprecursive
whose fourth parameter ends with traverse
, the hierarchical transformations do not define an order on the output set. An order can be reinstated by a subsequent orderby
or traverse
transformation or a $orderby
.
The algorithmic descriptions of the transformations make use of a union of collections, this is defined as an unordered collection containing the items from all these collections and from which duplicates have been removed.
The notation \(u[t]\) is used to denote the value of a property \(t\), possibly preceded by a type-cast segment, in an instance \(u\). It is also used to denote the value of a single-valued data aggregation path \(t\), evaluated relative to \(u\). The value of a collection-valued data aggregation path is denoted in the \(\Gamma\) notation by \(γ(u,t)\).
The notations introduced here are used throughout the following subsections.
The parameter lists defined in the following subsections have three mandatory parameters and one optional parameter in common.
+The parameter lists defined in the following subsections have three mandatory parameters in common.
The recursive hierarchy is defined by a parameter pair \((H,Q)\), where \(H\) and \(Q\) MUST be specified as the first and second parameter. Here, \(H\) MUST be an expression of type Collection(Edm.EntityType)
starting with $root
that has no multiple occurrences of the same entity. \(H\) identifies the collection of node entities forming a recursive hierarchy based on an annotation of their common entity type with term RecursiveHierarchy
with a Qualifier
attribute whose value MUST be provided in \(Q\). The property paths referenced by NodeProperty
and ParentNavigationProperty
in the RecursiveHierarchy
annotation must be evaluable for the nodes in the recursive hierarchy, otherwise the service MUST reject the request. The NodeProperty
is denoted by \(q\) in this section.
The third parameter MUST be a data aggregation path \(p\) with single- or collection-valued segments whose last segment MUST be a primitive property. The node identifier(s) of an instance \(u\) in the input set are the primitive values in \(γ(u,p)\), they are reached via \(p\) starting from \(u\). Let \(p=p_1/…/p_k/r\) with \(k≥0\) be the concatenation where each sub-path \(p_1,…,p_k\) consists of a collection-valued segment that is preceded by zero or more single-valued segments, and either \(r\) consists of one or more single-valued segments or \(k≥1\) and \({}/r\) is absent. Each segment can be prefixed with a type cast.
-The recursive hierarchy to be processed can also be a subset \(H'\) of \(H\). For this case a non-empty sequence \(S\) of transformations MAY be specified as an optional parameter whose position varies from transformation to transformation and is given below. In general, let \(H'\) be the output set of the transformation sequence \(S\) applied to \(H\), or \(H'=H\) if \(S\) is not specified. The transformations in \(S\) MUST be listed in section 3.3 or section 6.2 or be service-defined bound functions whose output set is a subset of their input set.
+Some parameter lists allow as optional fourth or fifth parameter a non-empty sequence \(S\) of transformations. The transformation sequence \(S\) will be applied to the node collection \(H\). It MUST consist of transformations listed in section 3.3 or section 6.2 or service-defined bound functions whose output set is a subset of their input set.
These transformations produce an output set that consists of certain instances from their input set, possibly with repetitions or in a different order.
ancestors
and descendants
In the simple case, the ancestors
transformation takes an input set consisting of instances that belong to a recursive hierarchy \((H',Q)\). It determines a subset \(A\) of the input set and then determines the set of ancestors of \(A\) that were already contained in the input set. Its output set is the ancestors set, optionally including \(A\).
In the simple case, the ancestors
transformation takes an input set consisting of instances that belong to a recursive hierarchy \((H,Q)\). It determines a subset \(A\) of the input set and then determines the set of ancestors of \(A\) that were already contained in the input set. Its output set is the ancestors set, optionally including \(A\).
In the more complex case, the instances in the input set are instead related to nodes in a recursive hierarchy. Then the ancestors
transformation determines a subset \(A\) of the input set consisting of instances that are related to certain nodes in the hierarchy, called start nodes. The ancestors of these start nodes are then determined, and the output set consists of instances of the input set that are related to the ancestors, or optionally to the start nodes.
The descendants
transformation works analogously, but with descendants.
\(H\), \(Q\) and \(p\) are the first three parameters defined above,
-The fourth parameter is a transformation sequence \(T\) composed of transformations listed section 3.3 or section 6.2 and of service-defined bound functions whose output set is a subset of their input set. \(A\) is the output set of this sequence applied to the input set.
-\(S\) is an optional fifth parameter as defined above that restricts \(H\) to a subset \(H'\). The following parameter \(d\) is optional and takes an integer greater than or equal to 1 that specifies the maximum distance between start nodes and ancestors or descendants to be considered. An optional final keep start
parameter drives the optional inclusion of the subset or start nodes.
The output set of the transformation \({\tt ancestors}(H,Q,p,T,S,d,{\tt keep\ start})\) or \({\tt descendants}(H,Q,p,T,S,d,{\tt keep\ start})\) is defined as the union of the output sets of transformations \(F(u)\) applied to the input set for all \(u\) in \(A\). For a given instance \(u\), the transformation \(F(u)\) determines all instances of the input set whose node identifier is an ancestor or descendant of the node identifier of \(u\):
-If \(p\) contains only single-valued segments, then, for ancestors
, \[\matrix{ F(u)={\tt filter}(\hbox{\tt Aggregation.isancestor}(\hfill\\ \quad {\tt HierarchyNodes}=H',\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \quad {\tt Node}=p,\;{\tt Descendant}=u[p],\;{\tt MaxDistance}=d,\;{\tt IncludeSelf}={\tt true}))\hfill }\] or, for descendants
, \[\matrix{ F(u)={\tt filter}(\hbox{\tt Aggregation.isdescendant}(\hfill\\ \quad {\tt HierarchyNodes}=H',\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \quad {\tt Node}=p,\;{\tt Ancestor}=u[p],\;{\tt MaxDistance}=d,\;{\tt IncludeSelf}={\tt true})).\hfill }\]
\(H\), \(Q\) and \(p\) are the first three parameters defined above.
+The fourth parameter is a transformation sequence \(T\) composed of transformations listed section 3.3 or section 6.2.1 and of service-defined bound functions whose output set is a subset of their input set. \(A\) is the output set of this sequence applied to the input set.
+The fifth parameter \(d\) is optional and takes an integer greater than or equal to 1 that specifies the maximum distance between start nodes and ancestors or descendants to be considered. An optional final keep start
parameter drives the optional inclusion of the subset or start nodes.
The output set of the transformation \({\tt ancestors}(H,Q,p,T,d,{\tt keep\ start})\) or \({\tt descendants}(H,Q,p,T,d,{\tt keep\ start})\) is defined as the union of the output sets of transformations \(F(u)\) applied to the input set for all \(u\) in \(A\). For a given instance \(u\), the transformation \(F(u)\) determines all instances of the input set whose node identifier is an ancestor or descendant of the node identifier of \(u\):
+If \(p\) contains only single-valued segments, then, for ancestors
, \[\matrix{ F(u)={\tt filter}(\hbox{\tt Aggregation.isancestor}(\hfill\\ \quad {\tt HierarchyNodes}=H,\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \quad {\tt Node}=p,\;{\tt Descendant}=u[p],\;{\tt MaxDistance}=d,\;{\tt IncludeSelf}={\tt true}))\hfill }\] or, for descendants
, \[\matrix{ F(u)={\tt filter}(\hbox{\tt Aggregation.isdescendant}(\hfill\\ \quad {\tt HierarchyNodes}=H,\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \quad {\tt Node}=p,\;{\tt Ancestor}=u[p],\;{\tt MaxDistance}=d,\;{\tt IncludeSelf}={\tt true})).\hfill }\]
Otherwise \(p=p_1/…/p_k/r\) with \(k≥1\), in this case the output set of the transformation \(F(u)\) is defined as the union of the output sets of transformations \(G(n)\) applied to the input set for all \(n\) in \(γ(u,p)\). The output set of \(G(n)\) consists of the instances of the input set whose node identifier is an ancestor or descendant of the node identifier \(n\):
-For ancestors
, \[\matrix{ G(n)={\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.isancestor}(\hfill\\ \hskip6pc {\tt HierarchyNodes}=H',\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \hskip6pc {\tt Node}=y_k/r,\;{\tt Descendant}=n,\;{\tt MaxDistance}=d,\;{\tt IncludeSelf}={\tt true}\hfill\\ \hskip5pc )\hfill\\ \hskip4pc )\hfill\\ \hskip3pc ⋰\hfill\\ \hskip2pc )\hfill\\ \hskip1pc )\hfill\\ )\hfill }\] or, for descendants
, \[\matrix{ G(n)={\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}=n,\;{\tt MaxDistance}=d,\;{\tt IncludeSelf}={\tt true}\hfill\\ \hskip5pc )\hfill\\ \hskip4pc )\hfill\\ \hskip3pc ⋰\hfill\\ \hskip2pc )\hfill\\ \hskip1pc )\hfill\\ )\hfill }\] where \(y_1,…,y_k\) denote lambdaVariableExpr
s as defined in OData-ABNF and \({}/r\) may be absent.
For ancestors
, \[\matrix{ G(n)={\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.isancestor}(\hfill\\ \hskip6pc {\tt HierarchyNodes}=H,\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \hskip6pc {\tt Node}=y_k/r,\;{\tt Descendant}=n,\;{\tt MaxDistance}=d,\;{\tt IncludeSelf}={\tt true}\hfill\\ \hskip5pc )\hfill\\ \hskip4pc )\hfill\\ \hskip3pc ⋰\hfill\\ \hskip2pc )\hfill\\ \hskip1pc )\hfill\\ )\hfill }\] or, for descendants
, \[\matrix{ G(n)={\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}=n,\;{\tt MaxDistance}=d,\;{\tt IncludeSelf}={\tt true}\hfill\\ \hskip5pc )\hfill\\ \hskip4pc )\hfill\\ \hskip3pc ⋰\hfill\\ \hskip2pc )\hfill\\ \hskip1pc )\hfill\\ )\hfill }\] where \(y_1,…,y_k\) denote lambdaVariableExpr
s as defined in OData-ABNF and \({}/r\) may be absent.
If parameter \(d\) is absent, the parameter \({\tt MaxDistance}=d\) is omitted. If keep start
is absent, the parameter \({\tt IncludeSelf}={\tt true}\) is omitted.
Since the output set of ancestors
is constructed as a union, no instance from the input set will occur more than once in it, even if, for example, a sale is related to both a sales organization and one of its ancestor organizations. For descendants
, analogously.
traverse
The traverse transformation returns instances of the input set that are or are related to nodes of a given recursive hierarchy in a specified tree order.
\(H\), \(Q\) and \(p\) are the first three parameters defined above.
-The fourth parameter \(h\) of the traverse
transformation is either preorder
or postorder
. \(S\) is an optional fifth parameter as defined above that restricts \(H\) to a subset \(H'\). All following parameters are optional and form a list \(o\) of expressions that could also be passed as a $orderby
system query option. If \(o\) is present, the transformation stable-sorts \(H'\) by \(o\).
The fourth parameter \(h\) of the traverse
transformation is either preorder
or postorder
. \(S\) is an optional fifth parameter as defined above. Let \(H'\) be the output set of the transformation sequence \(S\) applied to \(H\), or let \(H'\) be the collection of root nodes in the recursive hierarchy \((H,Q)\) if \(S\) is not specified. Nodes in \(H'\) are called start nodes in this subsection.
All following parameters are optional and form a list \(o\) of expressions that could also be passed as a $orderby
system query option. If \(o\) is present, the transformation stable-sorts \(H'\) by \(o\).
The instances in the input set are related to one node (if \(p\) is single-valued) or multiple nodes (if \(p\) is collection-valued) in the recursive hierarchy. Given a node \(x\), denote by \(\hat F(x)\) the collection of all instances in the input set that are related to \(x\); these collections can overlap. For each \(u\) in \(\hat F(x)\), the output set contains one instance that comprises the properties of \(u\) and additional properties that identify the node \(x\). These additional properties are independent of \(u\) and are bundled into an instance called \(σ(x)\). For example, if a sale \(u\) is related to two sales organizations and hence contained in both \(\hat F(x_1)\) and \(\hat F(x_2)\), the output set will contain two instances \((u,σ(x_1))\) and \((u,σ(x_2))\) and \(σ(x_i)\) contributes a navigation property SalesOrganization
.
A transformation \(F(x)\) is defined below such that \(\hat F(x)\) is the output set of \(F(x)\) applied to the input set of the traverse
transformation.
Given a node \(x\), the formulas below contain the transformation \(\Pi_G(σ(x))\) in order to inject the properties of \(σ(x)\) into the instances in \(\hat F(x)\); this uses the function \(\Pi_G\) that is defined in the simple grouping section. Further, \(G\) is a list of data aggregation paths that shall be present in the output set, and \(σ\) is a function that maps each hierarchy node \(x\) to an instance of the input type containing the paths from \(G\). As a consequence of the following definitions, only single-valued properties and "final segments from \(G\)" are nested into \(σ(x)\), therefore the behavior of \(\Pi_G(σ(x))\) is well-defined.
@@ -2836,15 +2850,15 @@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 66).
+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).
The function \(a(u,t,x)\) takes an instance, a path and another instance as arguments and is defined recursively as follows:
(See example 110.)
-Let \(r_1,…,r_n\) be a sequence of the root nodes of the recursive hierarchy \((H',Q)\) preserving the order of \(H'\) stable-sorted by \(o\). Then the transformation \({\tt traverse}(H,Q,p,h,S,o)\) is defined as equivalent to \[{\tt concat}(R(r_1),…,R(r_n)).\] \(R(x)\) is a transformation producing the specified tree order for a sub-hierarchy of \(H'\) with root node \(x\). Let \(c_1,…,c_m\) with \(m≥0\) be an order-preserving sequence of the children of \(x\) in \((H',Q)\). The recursive formula for \(R(x)\) is as follows:
+(See example 113.)
+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)).\]
+\(R(x)\) is a transformation producing the specified tree order for a sub-hierarchy of \(H'\) with root node \(x\). Let \(c_1,…,c_m\) with \(m≥0\) be an order-preserving sequence of the children of \(x\) in \((H,Q)\). The recursive formula for \(R(x)\) is as follows:
If \(h={\tt preorder}\), then \[R(x)={\tt concat}(F(x)/\Pi_G(σ(x)),R(c_1),…,R(c_m)).\]
If \(h={\tt postorder}\), then \[R(x)={\tt concat}(R(c_1),…,R(c_m),F(x)/\Pi_G(σ(x))).\]
\(F(x)\) is a transformation that determines for the specified node \(x\) the instances of the input set having the same node identifier as \(x\).
@@ -2883,34 +2900,98 @@The algorithm given so far is valid for a single-valued RecursiveHierarchy/ParentNavigationProperty
. The remainder of this section describes the case where it is collection-valued.
If the recursive algorithm reaches a node \(x\) multiple times, via different parents or ancestors, then the output set contains multiple instances that include \(σ(x)\). In order to distinguish these, information about the ancestors up to the root is injected into each \(σ(x)\) by annotating \(x\) differently before each \(σ(x)\) is computed.
-More precisely, a path-to-the-root is a node \(x\) that is annotated with the term UpNode
from the Aggregation
vocabulary OData-VocAggr where the annotation value is the parent node \(y\) such that \(R(x)\) appears on the right-hand side of the recursive formula for \(R(y)\). The annotation value \(y\) is again annotated with Aggregation.UpNode
and so on until a root is reached. Every instance in the output set of traverse
is related to one path-to-the-root.
Like structural and navigation properties, these instance annotations are considered part of the node \(x\) and are copied over to \(σ(x)\). The transformation \(\Pi_G(σ(x))\) is extended with an additional step between steps 2 and 3 of the function \(a_G(u,s,p)\) as defined in the simple grouping section:
+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).
+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:
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.UpPath#MultiParentHierarchy":
+ [ "EMEA", "Sales" ] },
+ { "ID": "AtlantisChild", "Name": "Child of Atlantis",
+ "@Aggregation.UpPath#MultiParentHierarchy":
+ [ "Atlantis", "EMEA", "Sales" ] },
+ ...
+ ]
+ }
Given a start node \(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:
Aggregation.UpNode
, copy the annotation from \(s\) to \(u\).Aggregation.UpPath
or Aggregation.Cycle
and qualifier \(Q\), copy these annotations from \(s\) to \(u\).Given a path-to-the-root \(x\) and a child \(c\) of \(x\), let \(ρ(c,x)\) be the path-to-the-root consisting of the node \(c\) annotated with Aggregation.UpNode
and value \(x\).
The Aggregation.UpNode
annotation of a root has value null. With \(r_1,…,r_n\) as above, the transformation \({\tt traverse}(H,Q,p,h,S,o)\) is defined as equivalent to \[{\tt concat}(R(ρ(r_1,{\tt null})),…,R(ρ(r_n,{\tt null}))\] where the function \(R(x)\) takes as argument a path-to-the-root. 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))).\]
If \(h={\tt postorder}\), then \[R(x)={\tt concat}(R(ρ(c_1,x)),…,R(ρ(c_m,x)),F(x)/\Pi_G(σ(x))).\]
-If there is only one parent, the result is the same as in the single-parent case, except for the presence of the Aggregation.UpNode
annotations.
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))).\]
+In the general case, servers MUST include the Aggregation.UpPath
annotations in the result of $apply
but MAY omit them if RecursiveHierarchy/ParentNavigationProperty
is single-valued and all start nodes are root nodes.
If RecursiveHierarchy/ParentNavigationProperty
is collection-valued but the parent collection never contains more than one parent and the optional parameter \(S\) is not specified, then the result is effectively like in the standard case, except for the presence of the Aggregation.UpPath
annotations.
rolluprecursive
Recall that simple grouping partitions the input set and applies a transformation sequence to each partition. By contrast, grouping with rolluprecursive
, informally speaking, transforms the input set into overlapping portions (like "US" and "US East"), one for each node \(x\) of a recursive hierarchy. The transformation \(F(x)\), defined below, outputs the portion whose node identifiers are among the descendants of \(x\) (including \(x\) itself). A transformation sequence is then applied to each portion, and they are made distinguishable in the output set through injection of information about the node \(x\), which is achieved through the transformation \(\Pi_G(σ(x))\) defined in the traverse
section.
As defined above, \(H\), \(Q\) and \(p\) are the first three parameters of rolluprecursive
, and \(S\) is an optional fourth parameter that restricts \(H\) to a subset \(H'\).
As defined above, \(H\), \(Q\) and \(p\) are the first three parameters of rolluprecursive
, \(S\) is an optional fourth parameter. Let \(H'\) be the output set of the transformation sequence \(S\) applied to \(H\), or \(H'=H\) if \(S\) is not specified.
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 65). \(Z_N\) is a transformation whose output set is its input set with property \(χ_N\) removed.
-If \(r_1,…,r_n\) are the root nodes of the recursive hierarchy \((H',Q)\), the transformation \({\tt groupby}((P_1,{\tt rolluprecursive}(H,Q,p,S),P_2),T)\) is defined as equivalent to \[{\tt concat}(R(r_1),…,R(r_n))\] with no order defined on the output set.
-\(R(x)\) is a transformation that processes the entire sub-hierarchy \(F(x)\) rooted at \(x\) (see (1) below) and then recurs for all children of \(x\) (see (2) below). Its output set is a collection of aggregated instances for all rollup results. Let \(c_1,…,c_m\) be the children of \(x\) in \((H',Q)\):
-If at least one of \(P_1\) or \(P_2\) is non-empty, then \[\matrix{ R(x)={\tt concat}(\hfill\\ \quad F(x)/{\tt compute}(x{\tt\ as\ }χ_N)/{\tt groupby}((P_1,P_2),T/Z_N/\Pi_G(σ(x))),\hfill&\tt(1)\\ \quad R(c_1),…,R(c_m)\hfill&\tt(2)\\ ).\hskip25pc }\]
-The property \(χ_N=x\) is present during the evaluation of \(T\), but not afterwards. If \(P_2\) contains a rolluprecursive
operator, the evaluation of row (1) involves a recursive invocation (with \(N\) increased by 1) of the rolluprecursive
algorithm.
Otherwise if \(P_1\) and \(P_2\) are empty, then \[\matrix{ R(x)={\tt concat}(\hfill\\ \quad F(x)/{\tt compute}(x{\tt\ as\ }χ_N)/T/Z_N/\Pi_G(σ(x)),\hfill&\tt(1)\\ \quad R(c_1),…,R(c_m)\hfill&\tt(2)\\ ).\hskip25pc }\]
-\(F(x)\) is defined as follows: If \(p\) contains only single-valued segments, then \[\matrix{ F(x)={\tt filter}(\hbox{\tt Aggregation.isdescendant}(\hfill\\ \quad {\tt HierarchyNodes}=H',\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \quad {\tt Node}=p,\;{\tt Ancestor}=x[q],\;{\tt IncludeSelf}={\tt true})).\hfill }\]
-Otherwise \(p=p_1/…/p_k/r\) with \(k≥1\) and \[\matrix{ F(x)={\tt filter}(\hfill\\ \hskip1pc p_1/{\tt any}(y_1:\hfill\\ \hskip2pc y_1/p_2/{\tt any}(y_2:\hfill\\ \hskip3pc ⋱\hfill\\ \hskip4pc y_{k-1}/p_k/{\tt any}(y_k:\hfill\\ \hskip5pc \hbox{\tt Aggregation.isdescendant}(\hfill\\ \hskip6pc {\tt HierarchyNodes}=H',\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \hskip6pc {\tt Node}=y_k/r,\;{\tt Ancestor}=x[q],\;{\tt IncludeSelf}={\tt true}\hfill\\ \hskip5pc )\hfill\\ \hskip4pc )\hfill\\ \hskip3pc ⋰\hfill\\ \hskip2pc )\hfill\\ \hskip1pc )\hfill\\ )\hfill }\] where \(y_1,…,y_k\) denote lambdaVariableExpr
s and \({}/r\) may be absent. (See example 111 for a case with \(k=1\).)
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.
\(R(x)\) is a transformation that processes the entire sub-hierarchy rooted at \(x\), which is the output set of \(F(x)\). The output set of \(R(x)\) is a collection of aggregated instances for all rollup results.
+If at least one of \(P_1\) or \(P_2\) is non-empty, then \[R(x)=F(x)/{\tt compute}(x{\tt\ as\ }χ_N)/{\tt groupby}((P_1,P_2),T/Z_N/\Pi_G(σ(x)))\] with no order defined on the output set.
+The property \(χ_N=x\) is present during the evaluation of \(T\), but not afterwards. If \(P_2\) contains a rolluprecursive
operator, the evaluation of the formula involves a recursive invocation (with \(N\) increased by 1) of the rolluprecursive
algorithm.
Otherwise if \(P_1\) and \(P_2\) are empty, then \[R(x)=F(x)/{\tt compute}(x{\tt\ as\ }χ_N)/T/Z_N/\Pi_G(σ(x))\] with no order defined on the output set.
+\(F(x)\) is defined as follows: If \(p\) contains only single-valued segments, then \[\matrix{ F(x)={\tt filter}(\hbox{\tt Aggregation.isdescendant}(\hfill\\ \quad {\tt HierarchyNodes}=H,\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \quad {\tt Node}=p,\;{\tt Ancestor}=x[q],\;{\tt IncludeSelf}={\tt true})).\hfill }\]
+Otherwise \(p=p_1/…/p_k/r\) with \(k≥1\) and \[\matrix{ F(x)={\tt filter}(\hfill\\ \hskip1pc p_1/{\tt any}(y_1:\hfill\\ \hskip2pc y_1/p_2/{\tt any}(y_2:\hfill\\ \hskip3pc ⋱\hfill\\ \hskip4pc y_{k-1}/p_k/{\tt any}(y_k:\hfill\\ \hskip5pc \hbox{\tt Aggregation.isdescendant}(\hfill\\ \hskip6pc {\tt HierarchyNodes}=H,\;{\tt HierarchyQualifier}=\hbox{\tt{'$Q$'}},\hfill\\ \hskip6pc {\tt Node}=y_k/r,\;{\tt Ancestor}=x[q],\;{\tt IncludeSelf}={\tt true}\hfill\\ \hskip5pc )\hfill\\ \hskip4pc )\hfill\\ \hskip3pc ⋰\hfill\\ \hskip2pc )\hfill\\ \hskip1pc )\hfill\\ )\hfill }\] where \(y_1,…,y_k\) denote lambdaVariableExpr
s and \({}/r\) may be absent. (See example 114 for a case with \(k=1\).)
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 64: 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 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.
GET /service/SalesOrganizations?$apply=
groupby((rolluprecursive(
$root/SalesOrganizations,SalesOrgHierarchy,ID)),
@@ -2918,28 +2999,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 }
- ]
- }
The value of the property \(χ_N\) in the 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 instance-bound 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 row labeled (1) in the formula \(R(x)\) above (the function is undefined otherwise). If \(N=1\), the Position
parameter can be omitted.
⚠ Example 65: 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.
+{
+"@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.
GET /service/Sales?$apply=groupby(
(rolluprecursive(
$root/SalesOrganizations,
@@ -2953,392 +3034,432 @@
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 66: 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.
{
+"@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").
+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,
+ ancestors($root/SalesOrganizations,SalesOrgHierarchy,ID,
+ filter(ID eq 'US East'),keep start))),
+ aggregate(Amount with sum as Total))
+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 }
+ ]
+ }
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,
+ filter(SalesOrganization/ID eq 'US East'),keep start))),
+ /groupby((rolluprecursive(
+ $root/SalesOrganizations,SalesOrgHierarchy,SalesOrganization/ID,
+ ancestors($root/SalesOrganizations,SalesOrgHierarchy,ID,
+ filter(ID eq 'US East'),keep start))),
+ aggregate(Amount with sum as Total))
+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 }
+ ]
+ }
⚠ 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.
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 },
- ...
- ]
- }
The algorithm given so far is valid for a single-valued RecursiveHierarchy/ParentNavigationProperty
. The remainder of this section describes the case where it is collection-valued. The function \(ρ(c,x)\) used below constructs a path-to-the-root and was defined in the traverse
section.
With \(r_1,…,r_n\) as above, \({\tt groupby}((P_1,{\tt rolluprecursive}(H,Q,p,S),P_2),T)\) is defined as equivalent to \[{\tt concat}(R(ρ(r_1,{\tt null}),…,R(ρ(r_n,{\tt null}))),\] where the function \(R(x)\) takes as argument a path-to-the-root. With \(F(x)\) and \(c_1,…,c_m\) as above, if at least one of \(P_1\) or \(P_2\) is non-empty, then \[\matrix{ R(x)={\tt concat}(\hfill\\ \quad F(x)/{\tt compute}(x{\tt\ as\ }χ_N)/{\tt groupby}((P_1,P_2),T/Z_N/\Pi_G(σ(x))),\hfill\\ \quad R(ρ(c_1,x)),…,R(ρ(c_m,x))\hfill\\ ),\hfill }\] otherwise \[\matrix{ R(x)={\tt concat}(\hfill\\ \quad F(x)/{\tt compute}(x{\tt\ as\ }χ_N)/T/Z_N/\Pi_G(σ(x)),\hfill\\ \quad R(ρ(c_1,x)),…,R(ρ(c_m,x))\hfill\\ ),\hfill }\] where \(χ_N\) is the path-to-the-root \(x\).
+{
+"@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 },
+ ...
+ ]
+ }
The following examples show some common aggregation-related questions that can be answered by combining the transformations defined in sections 3 and 6.
Grouping without specifying a set transformation returns the distinct combination of the grouping properties.
Example 67:
+Example 70:
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 68: customers that bought something
+Example 71: 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 69:
+Example 72:
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 70: Grouping by navigation property Customer
Example 73: 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 71: the first question in the motivating example in section 2.3, which customers bought which products, can now be expressed as
+Example 74: 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 72: 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 75: 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 73: 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 76: grouping by a property of a subtype
GET /service/Products?$apply=groupby((SalesModel.FoodProduct/Rating))
results in a third group representing entities with no SalesModel.FoodProduct/Rating
, including the SalesModel.NonFoodProduct
s:
{
-"@context": "$metadata#Products(@Core.AnyStructure)",
- "value": [
- { "@type": "#SalesModel.FoodProduct", "Rating": 5 },
- { "@type": "#SalesModel.FoodProduct", "Rating": null },
- { }
- ]
- }
{
+"@context": "$metadata#Products(@Core.AnyStructure)",
+ "value": [
+ { "@type": "#SalesModel.FoodProduct", "Rating": 5 },
+ { "@type": "#SalesModel.FoodProduct", "Rating": null },
+ { }
+ ]
+ }
The client may specify one of the predefined aggregation methods min
, max
, sum
, average
, and countdistinct
, or a custom aggregation method, to aggregate an aggregatable expression. Expressions defining an aggregate method specify an alias. The aggregated values are returned in a dynamic property whose name is determined by the alias.
Example 74:
+Example 77:
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 75: Alternatively, the request could ask for the aggregated amount to be nested inside a clone of Sales
+Example 78: 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 76: To compute the aggregate as a property without nesting, use the aggregate function in $compute
rather than the aggregate transformation in $apply
:
{
+"@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 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 }
- ]
- }
{
+"@context": "$metadata#Products(*,Total)",
+ "value": [
+ { "ID": "P2", "Name": "Coffee", "Color": "Brown", "TaxRate": 0.06,
+ "Total@type": "Decimal", "Total": 12 },
+ { "ID": "P3", "Name": "Paper", "Color": "White", "TaxRate": 0.14,
+ "Total@type": "Decimal", "Total": 8 },
+ { "ID": "P4", "Name": "Pencil", "Color": "Black", "TaxRate": 0.14,
+ "Total": null },
+ { "ID": "P1", "Name": "Sugar", "Color": "White", "TaxRate": 0.06,
+ "Total@type": "Decimal", "Total": 4 }
+ ]
+ }
The expression $it/Sales
refers to the sales of the current product. Without $it
, all sales of all products would be aggregated, because the input collection for the aggregate
function consists of all products.
Example 77: Alternatively, join
could be applied to yield a flat structure:
Example 80: 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 78:
+Example 81:
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 79: $count
after navigation property
Example 82: $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 80: The effect of the groupby
is to create transient entities and avoid in the result structural properties other than Name
.
⚠ Example 83: 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 81: Products with an aggregated sales volume of ten or more
+Example 84: 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 82: Customers in descending order of their aggregated sales volume
+Example 85: 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 83: Contribution of each sales to grand total sales amount
+Example 86: 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 84: 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 87: 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 85: Sales volume per customer in relation to total volume
+Example 88: 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 }
- ]
- }
{
+"@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 86: rule 1 for keyword from
applied repeatedly
Example 89: rule 1 for keyword from
applied repeatedly
GET /service/Sales?$apply=aggregate(Amount with sum
from Time with average
from Customer/Country with max
@@ -3357,66 +3478,66 @@
7.3 Requesting Expanded Results
-Example 87: 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 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:
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 88: nested addnested
transformations
+Example 91: 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,
@@ -3426,101 +3547,101 @@ results in the response before without the FilteredSales dynamic navigation properties expanded in the result.
-Example 89: Here only the GroupedSales
are expanded, because they are named in $expand
, the related Product
entity is not:
+Example 92: 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 90: 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 93: 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 91:
+Example 94:
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 92: 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 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.
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 93: illustrates rule 1 for keyword from
: maximal sales forecast for a product
+Example 96: 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
@@ -3529,7 +3650,7 @@
-Example 94: illustrates rule 2 for keyword from
: the forecast is computed in two steps
+Example 97: 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=
@@ -3537,7 +3658,7 @@
-Example 95: illustrates rule 1 followed by rule 2 for keyword from
: a forecast based on the average daily forecasts per country
+Example 98: 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)
@@ -3551,72 +3672,72 @@ 7.5 Aliasing
A property can be aggregated in multiple ways, each with a different alias.
-Example 96:
+Example 99:
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 97:
+Example 100:
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 98:
+Example 101:
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 99: 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 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:
GET /service/Sales?$apply=concat(
groupby((Customer/Country,Product/Name),
aggregate(Amount with sum as Total))
@@ -3624,87 +3745,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 100: transformation sequences are also useful inside groupby
: Aggregate the amount by only considering the top two sales amounts per product and country:
+{
+"@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 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 101: concatenation of two different groupings "biggest sale per customer" and "biggest sale per product", made distinguishable by a dynamic property:
+{
+"@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:
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 102: As a variation of example 99, 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 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.
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),
@@ -3712,27 +3833,27 @@ {
-"@context": "$metadata#Sales(Customer(Country),Total)",
- "value": [
- { "Customer": { "Country": "Netherlands" },
- "Product": { "Name": "Paper" },
- "Total@type": "Decimal", "Total": 3 },
- { "Customer": { "Country": "Netherlands" },
- "Total@type": "Decimal", "Total": 2 },
- { "Customer": { "Country": "USA" },
- "Product": { "Name": "Coffee" },
- "Total@type": "Decimal", "Total": 12 },
- { "Customer": { "Country": "USA" },
- "Total@type": "Decimal", "Total": 7 }
- ]
- }
+{
+"@context": "$metadata#Sales(Customer(Country),Total)",
+ "value": [
+ { "Customer": { "Country": "Netherlands" },
+ "Product": { "Name": "Paper" },
+ "Total@type": "Decimal", "Total": 3 },
+ { "Customer": { "Country": "Netherlands" },
+ "Total@type": "Decimal", "Total": 2 },
+ { "Customer": { "Country": "USA" },
+ "Product": { "Name": "Coffee" },
+ "Total@type": "Decimal", "Total": 12 },
+ { "Customer": { "Country": "USA" },
+ "Total@type": "Decimal", "Total": 7 }
+ ]
+ }
Note that these two entities get their values for the Country property from the groupby transformation, which ensures that they contain all grouping properties with the correct values.
For a leveled hierarchy, consumers may specify a different aggregation method per level for every property passed to rollup
as a hierarchy level below the root level.
Example 103: get the average of the overall amount by month per product.
+Example 106: 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))
@@ -3745,7 +3866,7 @@
-Example 104: get the total amount per customer, the average of the total customer amounts per country, and the overall average of these averages
+Example 107: 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
@@ -3756,34 +3877,34 @@ {
-"@context": "$metadata#Sales(CustomerCountryAverage)",
- "value": [
- { "Customer": { "Country": "USA", "ID": "C1" },
- "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 7 },
- { "Customer": { "Country": "USA", "ID": "C2" },
- "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 12 },
- { "Customer": { "Country": "USA" },
- "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 9.5 },
- { "Customer": { "Country": "Netherlands", "ID": "C3" },
- "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 5 },
- { "Customer": { "Country": "Netherlands" },
- "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 5 },
- { "CustomerCountryAverage@type":"Decimal",
- "CustomerCountryAverage": 7.25 }
- ]
- }
{
+"@context": "$metadata#Sales(CustomerCountryAverage)",
+ "value": [
+ { "Customer": { "Country": "USA", "ID": "C1" },
+ "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 7 },
+ { "Customer": { "Country": "USA", "ID": "C2" },
+ "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 12 },
+ { "Customer": { "Country": "USA" },
+ "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 9.5 },
+ { "Customer": { "Country": "Netherlands", "ID": "C3" },
+ "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 5 },
+ { "Customer": { "Country": "Netherlands" },
+ "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 5 },
+ { "CustomerCountryAverage@type":"Decimal",
+ "CustomerCountryAverage": 7.25 }
+ ]
+ }
Note that this example extends the result of rollup
with concat
and aggregate
to append the overall average.
If aggregation along a recursive hierarchy does not apply to the entire hierarchy, transformations ancestors
and descendants
may be used to restrict it as needed.
Example 105: Total sales amounts for sales orgs in 'US' in the SalesOrgHierarchy
defined in Hierarchy Examples
Example 108: 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,
@@ -3793,25 +3914,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 106: Number of Paper sales per sales org aggregated along the the SalesOrgHierarchy
defined in Hierarchy Examples
Example 109: 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((
@@ -3819,32 +3940,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 107: 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 110: 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,
@@ -3862,7 +3983,7 @@
-⚠ Example 108: total sales amount aggregated along the sales organization subhierarchy with root EMEA restricted to 3 levels
+⚠ Example 111: 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,
@@ -3896,7 +4017,7 @@
-Example 109: Return the result of example 65 in preorder
+Example 112: Return the result of example 67 in preorder
GET /service/Sales?$apply=groupby(
(rolluprecursive(
$root/SalesOrganizations,
@@ -3916,24 +4037,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 110: 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 113: 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,
@@ -3942,32 +4063,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" } } ] }
- ]
- }
+{
+"@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 111: Aggregation along a hierarchy with 1:N relationship: Sold products per sales organization
+Example 114: Aggregation along a hierarchy with 1:N relationship: Sold products per sales organization
GET /service/Products?$apply=
groupby((rolluprecursive(
$root/SalesOrganizations,
@@ -3975,26 +4096,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 112: 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 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:
ID |
+Relations/SuperordinateID |
+Relations/Weight |
+
---|---|---|
US | +Sales | +1 | +
EMEA | +Sales | +1 | +
EMEA Central | +EMEA | +1 | +
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).
+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" }
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> </
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(
+ $root/SalesOrganizations,
+ MultiParentHierarchy,
+ SalesOrganization/ID,
+ traverse(
+ $root/SalesOrganizations,
+ MultiParentHierarchy,
+ SalesOrganization/ID,
+ preorder))),
+ aggregate(MultiParentWeightedTotal))
+Assume that in addition to the sales in the 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 },
+ ...
+ ]
+ }
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 113:
+Example 120:
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 114:
+Example 121:
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 115: 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 122: 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
@@ -4110,30 +4399,30 @@Example 116: getting the population per country with
+Example 123: 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 117: 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 124: 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 118: all countries with tens of millions of city dwellers and the continents only for these countries
+Example 125: all countries with tens of millions of city dwellers and the continents only for these countries
GET /service/Cities?$apply=groupby((Continent/Name,Country/Name),
aggregate(Population with sum as CountryPopulation))
/filter(CountryPopulation ge 10000000)
@@ -4150,7 +4439,7 @@
-Example 119: all countries with tens of millions of city dwellers and all continents with cities independent of their size
+Example 126: all countries with tens of millions of city dwellers and all continents with cities independent of their size
GET /service/Cities?$apply=groupby((Continent/Name,Country/Name),
aggregate(Population with sum as CountryPopulation))
/concat(filter(CountryPopulation ge 10000000),
@@ -4159,21 +4448,21 @@
-Example 120: assuming the data model includes a sales order entity set with related sets for order items and customers, the base set as well as the related items can be filtered before aggregation
+Example 127: assuming the data model includes a sales order entity set with related sets for order items and customers, the base set as well as the related items can be filtered before aggregation
GET /service/SalesOrders?$apply=filter(Status eq 'incomplete')
/addnested(Items,filter(not Shipped) as FilteredItems)
/groupby((Customer/Country),
aggregate(FilteredItems/Amount with sum as ItemAmount))
Example 121: assuming that Amount
is a custom aggregate in addition to the property, determine the total for countries with an Amount
greater than 1000
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
GET /service/SalesOrders?$apply=
groupby((Customer/Country),aggregate(Amount))
/filter(Amount gt 1000)
/aggregate(Amount)
Example 122: 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 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.
GET /service/Sales?$apply=
concat(addnested(Product,compute(0.1 as Discount) as AugmentedProduct),
addnested(Product,compute(0.2 as Discount) as AugmentedProduct))
@@ -4181,25 +4470,25 @@
-Example 123: The nest
transformation can be used inside groupby
to produce one or more collection-valued properties per group.
+Example 130: 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" } } ] }
+ ]
+ }
Conforming clients MUST be prepared to consume a model that uses any or all of the constructs defined in this specification, including custom aggregation methods defined by the service, and MUST ignore any constructs not defined in this version of the specification.
This appendix contains the normative and informative references that are used in this document.
+This appendix contains the normative references that are used in this document.
While any hyperlinks included in this appendix were valid at the time of publication, OASIS cannot guarantee their long-term validity.
The following documents are referenced in such a way that some or all of their content constitutes requirements of this document.
@@ -4348,7 +4637,7 @@