Vault Query Aggregate Function support (#950)

* Partial (ie. incomplete) implementation of Aggregate Functions.

* Completed implementation of Aggregate Functions (sum, count, max, min, avg) with optional grouping.

* Completed Java DSL and associated JUnit tests.

* Added optional sorting by aggregate function.

* Added Jvm filename annotation on QueryCriteriaUtils.

* Added documentation (API and RST with code samples).

* Incorporating feedback from MH - improved readability in structuring Java and/or queries.

* Remove redundant import.

* Removed redundant commas.

* Streamlined expression parsing (in doing so, remove the ugly try-catch raised by RP in PR review comments.)

* Added JvmStatic and JvmOverloads to Java DSL; removed duplicate Kotlin DSL functions using default params; changed varargs to lists due to ambiguity

* Fix missing imports after rebase from master.

* Fix errors following rebase from master.

* Updates on expression handling following feedback from RP.
This commit is contained in:
josecoll 2017-07-06 10:57:59 +01:00 committed by GitHub
parent baaef30d5b
commit 44f57639d2
11 changed files with 695 additions and 71 deletions

View File

@ -80,6 +80,8 @@ interface CordaRPCOps : RPCOps {
* 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table.
* 3. the [PageSpecification] used in the query
* 4. a total number of results available (for subsequent paging if necessary)
* 5. status types used in this query: UNCONSUMED, CONSUMED, ALL
* 6. other results (aggregate functions with/without using value groups)
*
* Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries.
* It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification].

View File

@ -122,13 +122,19 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
* 4) a total number of states that met the given [QueryCriteria]
* Note that this may be more than the specified [PageSpecification.pageSize], and should be used to perform
* further pagination (by issuing new queries).
* 5) Status types used in this query: UNCONSUMED, CONSUMED, ALL
* 6) Other results as a [List] of any type (eg. aggregate function results with/without group by)
*
* Note: currently otherResults are used only for Aggregate Functions (in which case, the states and statesMetadata
* results will be empty)
*/
@CordaSerializable
data class Page<out T : ContractState>(val states: List<StateAndRef<T>>,
val statesMetadata: List<StateMetadata>,
val pageable: PageSpecification,
val totalStatesAvailable: Int,
val stateTypes: StateStatus)
val stateTypes: StateStatus,
val otherResults: List<Any>)
@CordaSerializable
data class StateMetadata(val ref: StateRef,
@ -349,6 +355,8 @@ interface VaultQueryService {
* 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table.
* 3. the [PageSpecification] used in the query
* 4. a total number of results available (for subsequent paging if necessary)
* 5. status types used in this query: UNCONSUMED, CONSUMED, ALL
* 6. other results (aggregate functions with/without using value groups)
*
* @throws VaultQueryException if the query cannot be executed for any reason
* (missing criteria or parsing error, invalid operator, unsupported query, underlying database error)

View File

@ -1,3 +1,5 @@
@file:JvmName("QueryCriteria")
package net.corda.core.node.services.vault
import net.corda.core.contracts.ContractState
@ -5,8 +7,6 @@ import net.corda.core.contracts.StateRef
import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.identity.AbstractParty
import net.corda.core.node.services.Vault
import net.corda.core.node.services.vault.QueryCriteria.AndComposition
import net.corda.core.node.services.vault.QueryCriteria.OrComposition
import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
@ -114,6 +114,9 @@ sealed class QueryCriteria {
RECORDED,
CONSUMED
}
infix fun and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria)
infix fun or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria)
}
interface IQueryCriteriaParser {
@ -125,7 +128,4 @@ interface IQueryCriteriaParser {
fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection<Predicate>
fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection<Predicate>
fun parse(criteria: QueryCriteria, sorting: Sort? = null) : Collection<Predicate>
}
infix fun QueryCriteria.and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria)
infix fun QueryCriteria.or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria)
}

View File

@ -1,3 +1,5 @@
@file:JvmName("QueryCriteriaUtils")
package net.corda.core.node.services.vault
import net.corda.core.schemas.PersistentState
@ -44,11 +46,23 @@ enum class CollectionOperator {
NOT_IN
}
@CordaSerializable
enum class AggregateFunctionType {
COUNT,
AVG,
MIN,
MAX,
SUM,
}
@CordaSerializable
sealed class CriteriaExpression<O, out T> {
data class BinaryLogical<O>(val left: CriteriaExpression<O, Boolean>, val right: CriteriaExpression<O, Boolean>, val operator: BinaryLogicalOperator) : CriteriaExpression<O, Boolean>()
data class Not<O>(val expression: CriteriaExpression<O, Boolean>) : CriteriaExpression<O, Boolean>()
data class ColumnPredicateExpression<O, C>(val column: Column<O, C>, val predicate: ColumnPredicate<C>) : CriteriaExpression<O, Boolean>()
data class AggregateFunctionExpression<O, C>(val column: Column<O, C>, val predicate: ColumnPredicate<C>,
val groupByColumns: List<Column<O, C>>?,
val orderBy: Sort.Direction?) : CriteriaExpression<O, Boolean>()
}
@CordaSerializable
@ -65,6 +79,7 @@ sealed class ColumnPredicate<C> {
data class CollectionExpression<C>(val operator: CollectionOperator, val rightLiteral: Collection<C>) : ColumnPredicate<C>()
data class Between<C : Comparable<C>>(val rightFromLiteral: C, val rightToLiteral: C) : ColumnPredicate<C>()
data class NullExpression<C>(val operator: NullOperator) : ColumnPredicate<C>()
data class AggregateFunction<C>(val type: AggregateFunctionType) : ColumnPredicate<C>()
}
fun <O, R> resolveEnclosingObjectFromExpression(expression: CriteriaExpression<O, R>): Class<O> {
@ -72,6 +87,7 @@ fun <O, R> resolveEnclosingObjectFromExpression(expression: CriteriaExpression<O
is CriteriaExpression.BinaryLogical -> resolveEnclosingObjectFromExpression(expression.left)
is CriteriaExpression.Not -> resolveEnclosingObjectFromExpression(expression.expression)
is CriteriaExpression.ColumnPredicateExpression<O, *> -> resolveEnclosingObjectFromColumn(expression.column)
is CriteriaExpression.AggregateFunctionExpression<O, *> -> resolveEnclosingObjectFromColumn(expression.column)
}
}
@ -140,14 +156,14 @@ data class Sort(val columns: Collection<SortColumn>) {
STATE_STATUS("stateStatus"),
RECORDED_TIME("recordedTime"),
CONSUMED_TIME("consumedTime"),
LOCK_ID("lockId"),
LOCK_ID("lockId")
}
enum class LinearStateAttribute(val columnName: String) : Attribute {
/** Vault Linear States */
UUID("uuid"),
EXTERNAL_ID("externalId"),
DEAL_REFERENCE("dealReference"),
DEAL_REFERENCE("dealReference")
}
enum class FungibleStateAttribute(val columnName: String) : Attribute {
@ -183,10 +199,15 @@ sealed class SortAttribute {
object Builder {
fun <R : Comparable<R>> compare(operator: BinaryComparisonOperator, value: R) = ColumnPredicate.BinaryComparison(operator, value)
fun <O, R> KProperty1<O, R?>.predicate(predicate: ColumnPredicate<R>) = CriteriaExpression.ColumnPredicateExpression(Column.Kotlin(this), predicate)
fun <R> Field.predicate(predicate: ColumnPredicate<R>) = CriteriaExpression.ColumnPredicateExpression(Column.Java<Any, R>(this), predicate)
fun <O, R> KProperty1<O, R?>.functionPredicate(predicate: ColumnPredicate<R>, groupByColumns: List<Column.Kotlin<O, R>>? = null, orderBy: Sort.Direction? = null)
= CriteriaExpression.AggregateFunctionExpression(Column.Kotlin(this), predicate, groupByColumns, orderBy)
fun <R> Field.functionPredicate(predicate: ColumnPredicate<R>, groupByColumns: List<Column.Java<Any, R>>? = null, orderBy: Sort.Direction? = null)
= CriteriaExpression.AggregateFunctionExpression(Column.Java<Any, R>(this), predicate, groupByColumns, orderBy)
fun <O, R : Comparable<R>> KProperty1<O, R?>.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value))
fun <R : Comparable<R>> Field.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value))
@ -200,15 +221,15 @@ object Builder {
fun <O, R : Comparable<R>> KProperty1<O, R?>.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection))
fun <O, R : Comparable<R>> KProperty1<O, R?>.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection))
fun <R> Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value))
fun <R> Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value))
fun <R : Comparable<R>> Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value)
fun <R : Comparable<R>> Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value)
fun <R : Comparable<R>> Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value)
fun <R : Comparable<R>> Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value)
fun <R : Comparable<R>> Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to))
fun <R : Comparable<R>> Field.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection))
fun <R : Comparable<R>> Field.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection))
@JvmStatic fun <R> Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value))
@JvmStatic fun <R> Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value))
@JvmStatic fun <R : Comparable<R>> Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value)
@JvmStatic fun <R : Comparable<R>> Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value)
@JvmStatic fun <R : Comparable<R>> Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value)
@JvmStatic fun <R : Comparable<R>> Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value)
@JvmStatic fun <R : Comparable<R>> Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to))
@JvmStatic fun <R : Comparable<R>> Field.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection))
@JvmStatic fun <R : Comparable<R>> Field.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection))
fun <R> equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)
fun <R> notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)
@ -221,14 +242,45 @@ object Builder {
fun <R : Comparable<R>> notIn(collection: Collection<R>) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)
fun <O> KProperty1<O, String?>.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string))
fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string))
@JvmStatic fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string))
fun <O> KProperty1<O, String?>.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string))
fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string))
@JvmStatic fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string))
fun <O, R> KProperty1<O, R?>.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL))
fun Field.isNull() = predicate(ColumnPredicate.NullExpression<Any>(NullOperator.IS_NULL))
@JvmStatic fun Field.isNull() = predicate(ColumnPredicate.NullExpression<Any>(NullOperator.IS_NULL))
fun <O, R> KProperty1<O, R?>.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL))
fun Field.notNull() = predicate(ColumnPredicate.NullExpression<Any>(NullOperator.NOT_NULL))
@JvmStatic fun Field.notNull() = predicate(ColumnPredicate.NullExpression<Any>(NullOperator.NOT_NULL))
/** aggregate functions */
fun <O, R> KProperty1<O, R?>.sum(groupByColumns: List<KProperty1<O, R>>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column.Kotlin(it) }, orderBy)
@JvmStatic @JvmOverloads
fun <R> Field.sum(groupByColumns: List<Field>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction<R>(AggregateFunctionType.SUM), groupByColumns?.map { Column.Java<Any,R>(it) }, orderBy)
fun <O, R> KProperty1<O, R?>.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT))
@JvmStatic fun Field.count() = functionPredicate(ColumnPredicate.AggregateFunction<Any>(AggregateFunctionType.COUNT))
fun <O, R> KProperty1<O, R?>.avg(groupByColumns: List<KProperty1<O, R>>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column.Kotlin(it) }, orderBy)
@JvmStatic
@JvmOverloads
fun <R> Field.avg(groupByColumns: List<Field>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction<R>(AggregateFunctionType.AVG), groupByColumns?.map { Column.Java<Any,R>(it) }, orderBy)
fun <O, R> KProperty1<O, R?>.min(groupByColumns: List<KProperty1<O, R>>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column.Kotlin(it) }, orderBy)
@JvmStatic
@JvmOverloads
fun <R> Field.min(groupByColumns: List<Field>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction<R>(AggregateFunctionType.MIN), groupByColumns?.map { Column.Java<Any,R>(it) }, orderBy)
fun <O, R> KProperty1<O, R?>.max(groupByColumns: List<KProperty1<O, R>>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column.Kotlin(it) }, orderBy)
@JvmStatic
@JvmOverloads
fun <R> Field.max(groupByColumns: List<Field>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction<R>(AggregateFunctionType.MAX), groupByColumns?.map { Column.Java<Any,R>(it) }, orderBy)
}
inline fun <A> builder(block: Builder.() -> A) = block(Builder)

View File

@ -50,27 +50,27 @@ The API provides both static (snapshot) and dynamic (snapshot with streaming upd
Simple pagination (page number and size) and sorting (directional ordering using standard or custom property attributes) is also specifiable.
Defaults are defined for Paging (pageNumber = 0, pageSize = 200) and Sorting (direction = ASC).
The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN).
The ``QueryCriteria`` interface provides a flexible mechanism for specifying different filtering criteria, including and/or composition and a rich set of operators to include: binary logical (AND, OR), comparison (LESS_THAN, LESS_THAN_OR_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL), equality (EQUAL, NOT_EQUAL), likeness (LIKE, NOT_LIKE), nullability (IS_NULL, NOT_NULL), and collection based (IN, NOT_IN). Standard SQL-92 aggregate functions (SUM, AVG, MIN, MAX, COUNT) are also supported.
There are four implementations of this interface which can be chained together to define advanced filters.
1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED).
1. ``VaultQueryCriteria`` provides filterable criteria on attributes within the Vault states table: status (UNCONSUMED, CONSUMED), state reference(s), contract state type(s), notaries, soft locked states, timestamps (RECORDED, CONSUMED).
.. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, includeSoftlockedStates = true).
.. note:: Sensible defaults are defined for frequently used attributes (status = UNCONSUMED, includeSoftlockedStates = true).
2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s).
2. ``FungibleAssetQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``FungibleAsset`` contract state interface, used to represent assets that are fungible, countable and issued by a specific party (eg. ``Cash.State`` and ``CommodityContract.State`` in the Corda finance module). Filterable attributes include: participants(s), owner(s), quantity, issuer party(s) and issuer reference(s).
.. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table.
.. note:: All contract states that extend the ``FungibleAsset`` now automatically persist that interfaces common state attributes to the **vault_fungible_states** table.
3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), dealRef(s).
3. ``LinearStateQueryCriteria`` provides filterable criteria on attributes defined in the Corda Core ``LinearState`` and ``DealState`` contract state interfaces, used to represent entities that continuously supercede themselves, all of which share the same *linearId* (eg. trade entity states such as the ``IRSState`` defined in the SIMM valuation demo). Filterable attributes include: participant(s), linearId(s), dealRef(s).
.. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table.
.. note:: All contract states that extend ``LinearState`` or ``DealState`` now automatically persist those interfaces common state attributes to the **vault_linear_states** table.
4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the :doc:`Persistence </api-persistence>` documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression. The ColumnPredicateExpression allows for specification arbitrary criteria using the previously enumerated operator types. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``.
4. ``VaultCustomQueryCriteria`` provides the means to specify one or many arbitrary expressions on attributes defined by a custom contract state that implements its own schema as described in the :doc:`Persistence </api-persistence>` documentation and associated examples. Custom criteria expressions are expressed using one of several type-safe ``CriteriaExpression``: BinaryLogical, Not, ColumnPredicateExpression, AggregateFunctionExpression. The ``ColumnPredicateExpression`` allows for specification arbitrary criteria using the previously enumerated operator types. The ``AggregateFunctionExpression`` allows for the specification of an aggregate function type (sum, avg, max, min, count) with optional grouping and sorting. Furthermore, a rich DSL is provided to enable simple construction of custom criteria using any combination of ``ColumnPredicate``. See the ``Builder`` object in ``QueryCriteriaUtils`` for a complete specification of the DSL.
.. note:: It is a requirement to register any custom contract schemas to be used in Vault Custom queries in the associated `CordaPluginRegistry` configuration for the respective CorDapp using the ``requiredSchemas`` configuration field (which specifies a set of `MappedSchema`)
An example is illustrated here:
An example of a custom query is illustrated here:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
:language: kotlin
@ -236,6 +236,37 @@ Query for fungible assets for a specifc issuer party:
:start-after: DOCSTART VaultQueryExample14
:end-before: DOCEND VaultQueryExample14
**Aggregate Function queries using** ``VaultCustomQueryCriteria``
.. note:: Query results for aggregate functions are contained in the `otherResults` attribute of a results Page.
Aggregations on cash using various functions:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
:language: kotlin
:start-after: DOCSTART VaultQueryExample21
:end-before: DOCEND VaultQueryExample21
.. note:: `otherResults` will contain 5 items, one per calculated aggregate function.
Aggregations on cash grouped by currency for various functions:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
:language: kotlin
:start-after: DOCSTART VaultQueryExample22
:end-before: DOCEND VaultQueryExample22
.. note:: `otherResults` will contain 24 items, one result per calculated aggregate function per currency (the grouping attribute - currency in this case - is returned per aggregate result).
Sum aggregation on cash grouped by issuer party and currency and sorted by sum:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt
:language: kotlin
:start-after: DOCSTART VaultQueryExample23
:end-before: DOCEND VaultQueryExample23
.. note:: `otherResults` will contain 12 items sorted from largest summed cash amount to smallest, one result per calculated aggregate function per issuer party and currency (grouping attributes are returned per aggregate result).
**Dynamic queries** (also using ``VaultQueryCriteria``) are an extension to the snapshot queries by returning an additional ``QueryResults`` return type in the form of an ``Observable<Vault.Update>``. Refer to `ReactiveX Observable <http://reactivex.io/documentation/observable.html>`_ for a detailed understanding and usage of this type.
Track unconsumed cash states:
@ -301,6 +332,29 @@ Query for consumed deal states or linear ids, specify a paging specification and
:start-after: DOCSTART VaultJavaQueryExample2
:end-before: DOCEND VaultJavaQueryExample2
**Aggregate Function queries using** ``VaultCustomQueryCriteria``
Aggregations on cash using various functions:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt
:language: kotlin
:start-after: DOCSTART VaultJavaQueryExample21
:end-before: DOCEND VaultJavaQueryExample21
Aggregations on cash grouped by currency for various functions:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt
:language: kotlin
:start-after: DOCSTART VaultJavaQueryExample22
:end-before: DOCEND VaultJavaQueryExample22
Sum aggregation on cash grouped by issuer party and currency and sorted by sum:
.. literalinclude:: ../../node/src/test/kotlin/net/corda/node/services/vault/VaultQueryJavaTests.kt
:language: kotlin
:start-after: DOCSTART VaultJavaQueryExample23
:end-before: DOCEND VaultJavaQueryExample23
Track unconsumed cash states:
.. literalinclude:: ../../node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java

View File

@ -9,7 +9,6 @@ import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.Vault
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.and
import net.corda.core.toFuture
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.DUMMY_NOTARY_KEY

View File

@ -35,6 +35,7 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
private val joinPredicates = mutableListOf<Predicate>()
// incrementally build list of root entities (for later use in Sort parsing)
private val rootEntities = mutableMapOf<Class<out PersistentState>, Root<*>>()
private val aggregateExpressions = mutableListOf<Expression<*>>()
var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED
@ -78,7 +79,7 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
QueryCriteria.TimeInstantType.CONSUMED -> Column.Kotlin(VaultSchemaV1.VaultStates::consumedTime)
}
val expression = CriteriaExpression.ColumnPredicateExpression(timeColumn, timeCondition.predicate)
predicateSet.add(expressionToPredicate(vaultStates, expression))
predicateSet.add(parseExpression(vaultStates, expression) as Predicate)
}
return predicateSet
}
@ -127,32 +128,75 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
NullOperator.NOT_NULL -> criteriaBuilder.isNotNull(column)
}
}
else -> throw VaultQueryException("Not expecting $columnPredicate")
}
}
/**
* @return : Expression<Boolean> -> : Predicate
*/
private fun <O, R> expressionToExpression(root: Root<O>, expression: CriteriaExpression<O, R>): Expression<R> {
private fun <O> parseExpression(entityRoot: Root<O>, expression: CriteriaExpression<O, Boolean>, predicateSet: MutableSet<Predicate>) {
if (expression is CriteriaExpression.AggregateFunctionExpression<O,*>) {
parseAggregateFunction(entityRoot, expression)
} else {
predicateSet.add(parseExpression(entityRoot, expression) as Predicate)
}
}
private fun <O, R> parseExpression(root: Root<O>, expression: CriteriaExpression<O, R>): Expression<Boolean> {
return when (expression) {
is CriteriaExpression.BinaryLogical -> {
val leftPredicate = expressionToExpression(root, expression.left)
val rightPredicate = expressionToExpression(root, expression.right)
val leftPredicate = parseExpression(root, expression.left)
val rightPredicate = parseExpression(root, expression.right)
when (expression.operator) {
BinaryLogicalOperator.AND -> criteriaBuilder.and(leftPredicate, rightPredicate) as Expression<R>
BinaryLogicalOperator.OR -> criteriaBuilder.or(leftPredicate, rightPredicate) as Expression<R>
BinaryLogicalOperator.AND -> criteriaBuilder.and(leftPredicate, rightPredicate)
BinaryLogicalOperator.OR -> criteriaBuilder.or(leftPredicate, rightPredicate)
}
}
is CriteriaExpression.Not -> criteriaBuilder.not(expressionToExpression(root, expression.expression)) as Expression<R>
is CriteriaExpression.Not -> criteriaBuilder.not(parseExpression(root, expression.expression))
is CriteriaExpression.ColumnPredicateExpression<O, *> -> {
val column = root.get<Any?>(getColumnName(expression.column))
columnPredicateToPredicate(column, expression.predicate) as Expression<R>
columnPredicateToPredicate(column, expression.predicate)
}
else -> throw VaultQueryException("Unexpected expression: $expression")
}
}
private fun <O> expressionToPredicate(root: Root<O>, expression: CriteriaExpression<O, Boolean>): Predicate {
return expressionToExpression(root, expression) as Predicate
private fun <O, R> parseAggregateFunction(root: Root<O>, expression: CriteriaExpression.AggregateFunctionExpression<O, R>): Expression<out Any?>? {
val column = root.get<Any?>(getColumnName(expression.column))
val columnPredicate = expression.predicate
when (columnPredicate) {
is ColumnPredicate.AggregateFunction -> {
column as Path<Long?>?
val aggregateExpression =
when (columnPredicate.type) {
AggregateFunctionType.SUM -> criteriaBuilder.sum(column)
AggregateFunctionType.AVG -> criteriaBuilder.avg(column)
AggregateFunctionType.COUNT -> criteriaBuilder.count(column)
AggregateFunctionType.MAX -> criteriaBuilder.max(column)
AggregateFunctionType.MIN -> criteriaBuilder.min(column)
}
aggregateExpressions.add(aggregateExpression)
// optionally order by this aggregate function
expression.orderBy?.let {
val orderCriteria =
when (expression.orderBy!!) {
Sort.Direction.ASC -> criteriaBuilder.asc(aggregateExpression)
Sort.Direction.DESC -> criteriaBuilder.desc(aggregateExpression)
}
criteriaQuery.orderBy(orderCriteria)
}
// add optional group by clauses
expression.groupByColumns?.let { columns ->
val groupByExpressions =
columns.map { column ->
val path = root.get<Any?>(getColumnName(column))
aggregateExpressions.add(path)
path
}
criteriaQuery.groupBy(groupByExpressions)
}
return aggregateExpression
}
else -> throw VaultQueryException("Not expecting $columnPredicate")
}
}
override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria) : Collection<Predicate> {
@ -254,7 +298,8 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
val joinPredicate = criteriaBuilder.equal(vaultStates.get<PersistentStateRef>("stateRef"), entityRoot.get<PersistentStateRef>("stateRef"))
joinPredicates.add(joinPredicate)
predicateSet.add(expressionToPredicate(entityRoot, criteria.expression))
// resolve general criteria expressions
parseExpression(entityRoot, criteria.expression, predicateSet)
}
catch (e: Exception) {
e.message?.let { message ->
@ -303,7 +348,11 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
parse(sorting)
}
val selections = listOf(vaultStates).plus(rootEntities.map { it.value })
val selections =
if (aggregateExpressions.isEmpty())
listOf(vaultStates).plus(rootEntities.map { it.value })
else
aggregateExpressions
criteriaQuery.multiselect(selections)
val combinedPredicates = joinPredicates.plus(predicateSet)
criteriaQuery.where(*combinedPredicates.toTypedArray())

View File

@ -18,19 +18,20 @@ import net.corda.core.node.services.vault.Sort
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.storageKryo
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1
import org.jetbrains.exposed.sql.transactions.TransactionManager
import rx.subjects.PublishSubject
import java.lang.Exception
import java.util.*
import javax.persistence.EntityManager
import javax.persistence.Tuple
class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration,
val updatesPublisher: PublishSubject<Vault.Update>) : SingletonSerializeAsToken(), VaultQueryService {
companion object {
val log = loggerFor<HibernateVaultQueryImpl>()
}
@ -80,17 +81,24 @@ class HibernateVaultQueryImpl(hibernateConfig: HibernateConfiguration,
val results = query.resultList
val statesAndRefs: MutableList<StateAndRef<*>> = mutableListOf()
val statesMeta: MutableList<Vault.StateMetadata> = mutableListOf()
val otherResults: MutableList<Any> = mutableListOf()
results.asSequence()
.forEach { it ->
val it = it[0] as VaultSchemaV1.VaultStates
val stateRef = StateRef(SecureHash.parse(it.stateRef!!.txId!!), it.stateRef!!.index!!)
val state = it.contractState.deserialize<TransactionState<T>>(storageKryo())
statesMeta.add(Vault.StateMetadata(stateRef, it.contractStateClassName, it.recordedTime, it.consumedTime, it.stateStatus, it.notaryName, it.notaryKey, it.lockId, it.lockUpdateTime))
statesAndRefs.add(StateAndRef(state, stateRef))
if (it[0] is VaultSchemaV1.VaultStates) {
val it = it[0] as VaultSchemaV1.VaultStates
val stateRef = StateRef(SecureHash.parse(it.stateRef!!.txId!!), it.stateRef!!.index!!)
val state = it.contractState.deserialize<TransactionState<T>>(storageKryo())
statesMeta.add(Vault.StateMetadata(stateRef, it.contractStateClassName, it.recordedTime, it.consumedTime, it.stateStatus, it.notaryName, it.notaryKey, it.lockId, it.lockUpdateTime))
statesAndRefs.add(StateAndRef(state, stateRef))
}
else {
log.debug { "OtherResults: ${Arrays.toString(it.toArray())}" }
otherResults.addAll(it.toArray().asList())
}
}
return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, pageable = paging, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates) as Vault.Page<T>
return Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, pageable = paging, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) as Vault.Page<T>
} catch (e: Exception) {
log.error(e.message)

View File

@ -6,7 +6,7 @@ import net.corda.contracts.DealState;
import net.corda.contracts.asset.Cash;
import net.corda.core.contracts.*;
import net.corda.core.contracts.testing.DummyLinearContract;
import net.corda.core.crypto.SecureHash;
import net.corda.core.crypto.*;
import net.corda.core.identity.AbstractParty;
import net.corda.core.messaging.DataFeed;
import net.corda.core.node.services.Vault;
@ -45,10 +45,11 @@ import java.util.stream.StreamSupport;
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER;
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY;
import static net.corda.testing.CoreTestUtils.getBOC;
import static net.corda.testing.CoreTestUtils.getBOC_KEY;
import static net.corda.testing.CoreTestUtils.getBOC_PUBKEY;
import static net.corda.core.contracts.ContractsDSL.USD;
import static net.corda.core.node.services.vault.QueryCriteriaKt.and;
import static net.corda.core.node.services.vault.QueryCriteriaKt.or;
import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.getMAX_PAGE_SIZE;
import static net.corda.core.node.services.vault.QueryCriteriaUtils.getMAX_PAGE_SIZE;
import static net.corda.node.utilities.DatabaseSupportKt.configureDatabase;
import static net.corda.node.utilities.DatabaseSupportKt.transaction;
import static net.corda.testing.CoreTestUtils.getMEGA_CORP;
@ -188,8 +189,8 @@ public class VaultQueryJavaTests {
QueryCriteria linearCriteriaAll = new LinearStateQueryCriteria(null, linearIds);
QueryCriteria dealCriteriaAll = new LinearStateQueryCriteria(null, null, dealIds);
QueryCriteria compositeCriteria1 = or(dealCriteriaAll, linearCriteriaAll);
QueryCriteria compositeCriteria2 = and(vaultCriteria, compositeCriteria1);
QueryCriteria compositeCriteria1 = dealCriteriaAll.or(linearCriteriaAll);
QueryCriteria compositeCriteria2 = vaultCriteria.and(compositeCriteria1);
PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE());
Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC);
@ -224,14 +225,14 @@ public class VaultQueryJavaTests {
Field attributeCurrency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency");
Field attributeQuantity = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies");
CriteriaExpression currencyIndex = Builder.INSTANCE.equal(attributeCurrency, "USD");
CriteriaExpression quantityIndex = Builder.INSTANCE.greaterThanOrEqual(attributeQuantity, 10L);
CriteriaExpression currencyIndex = Builder.equal(attributeCurrency, "USD");
CriteriaExpression quantityIndex = Builder.greaterThanOrEqual(attributeQuantity, 10L);
QueryCriteria customCriteria2 = new VaultCustomQueryCriteria(quantityIndex);
QueryCriteria customCriteria1 = new VaultCustomQueryCriteria(currencyIndex);
QueryCriteria criteria = QueryCriteriaKt.and(QueryCriteriaKt.and(generalCriteria, customCriteria1), customCriteria2);
QueryCriteria criteria = generalCriteria.and(customCriteria1).and(customCriteria2);
Vault.Page<ContractState> results = vaultQuerySvc.queryBy(Cash.State.class, criteria);
// DOCEND VaultJavaQueryExample3
@ -297,8 +298,8 @@ public class VaultQueryJavaTests {
List<AbstractParty> dealParty = Collections.singletonList(getMEGA_CORP());
QueryCriteria dealCriteria = new LinearStateQueryCriteria(dealParty, null, dealIds);
QueryCriteria linearCriteria = new LinearStateQueryCriteria(dealParty, linearIds, null);
QueryCriteria dealOrLinearIdCriteria = or(dealCriteria, linearCriteria);
QueryCriteria compositeCriteria = and(dealOrLinearIdCriteria, vaultCriteria);
QueryCriteria dealOrLinearIdCriteria = dealCriteria.or(linearCriteria);
QueryCriteria compositeCriteria = dealOrLinearIdCriteria.and(vaultCriteria);
PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE());
Sort.SortColumn sortByUid = new Sort.SortColumn(new SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC);
@ -374,4 +375,169 @@ public class VaultQueryJavaTests {
return tx;
});
}
/**
* Aggregation Functions
*/
@Test
public void aggregateFunctionsWithoutGroupClause() {
transaction(database, tx -> {
Amount<Currency> dollars100 = new Amount<>(100, Currency.getInstance("USD"));
Amount<Currency> dollars200 = new Amount<>(200, Currency.getInstance("USD"));
Amount<Currency> dollars300 = new Amount<>(300, Currency.getInstance("USD"));
Amount<Currency> pounds = new Amount<>(400, Currency.getInstance("GBP"));
Amount<Currency> swissfrancs = new Amount<>(500, Currency.getInstance("CHF"));
VaultFiller.fillWithSomeTestCash(services, dollars100, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
VaultFiller.fillWithSomeTestCash(services, dollars200, TestConstants.getDUMMY_NOTARY(), 2, 2, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
VaultFiller.fillWithSomeTestCash(services, dollars300, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
VaultFiller.fillWithSomeTestCash(services, pounds, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
VaultFiller.fillWithSomeTestCash(services, swissfrancs, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
try {
// DOCSTART VaultJavaQueryExample21
Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies");
QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies));
QueryCriteria countCriteria = new VaultCustomQueryCriteria(Builder.count(pennies));
QueryCriteria maxCriteria = new VaultCustomQueryCriteria(Builder.max(pennies));
QueryCriteria minCriteria = new VaultCustomQueryCriteria(Builder.min(pennies));
QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies));
QueryCriteria criteria = sumCriteria.and(countCriteria).and(maxCriteria).and(minCriteria).and(avgCriteria);
Vault.Page<Cash.State> results = vaultQuerySvc.queryBy(Cash.State.class, criteria);
// DOCEND VaultJavaQueryExample21
assertThat(results.getOtherResults()).hasSize(5);
assertThat(results.getOtherResults().get(0)).isEqualTo(1500L);
assertThat(results.getOtherResults().get(1)).isEqualTo(15L);
assertThat(results.getOtherResults().get(2)).isEqualTo(113L);
assertThat(results.getOtherResults().get(3)).isEqualTo(87L);
assertThat(results.getOtherResults().get(4)).isEqualTo(100.0);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return tx;
});
}
@Test
public void aggregateFunctionsWithSingleGroupClause() {
transaction(database, tx -> {
Amount<Currency> dollars100 = new Amount<>(100, Currency.getInstance("USD"));
Amount<Currency> dollars200 = new Amount<>(200, Currency.getInstance("USD"));
Amount<Currency> dollars300 = new Amount<>(300, Currency.getInstance("USD"));
Amount<Currency> pounds = new Amount<>(400, Currency.getInstance("GBP"));
Amount<Currency> swissfrancs = new Amount<>(500, Currency.getInstance("CHF"));
VaultFiller.fillWithSomeTestCash(services, dollars100, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
VaultFiller.fillWithSomeTestCash(services, dollars200, TestConstants.getDUMMY_NOTARY(), 2, 2, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
VaultFiller.fillWithSomeTestCash(services, dollars300, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
VaultFiller.fillWithSomeTestCash(services, pounds, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
VaultFiller.fillWithSomeTestCash(services, swissfrancs, TestConstants.getDUMMY_NOTARY(), 5, 5, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
try {
// DOCSTART VaultJavaQueryExample22
Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies");
Field currency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency");
QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Arrays.asList(currency)));
QueryCriteria countCriteria = new VaultCustomQueryCriteria(Builder.count(pennies));
QueryCriteria maxCriteria = new VaultCustomQueryCriteria(Builder.max(pennies, Arrays.asList(currency)));
QueryCriteria minCriteria = new VaultCustomQueryCriteria(Builder.min(pennies, Arrays.asList(currency)));
QueryCriteria avgCriteria = new VaultCustomQueryCriteria(Builder.avg(pennies, Arrays.asList(currency)));
QueryCriteria criteria = sumCriteria.and(countCriteria).and(maxCriteria).and(minCriteria).and(avgCriteria);
Vault.Page<Cash.State> results = vaultQuerySvc.queryBy(Cash.State.class, criteria);
// DOCEND VaultJavaQueryExample22
assertThat(results.getOtherResults()).hasSize(27);
/** CHF */
assertThat(results.getOtherResults().get(0)).isEqualTo(500L);
assertThat(results.getOtherResults().get(1)).isEqualTo("CHF");
assertThat(results.getOtherResults().get(2)).isEqualTo(5L);
assertThat(results.getOtherResults().get(3)).isEqualTo(102L);
assertThat(results.getOtherResults().get(4)).isEqualTo("CHF");
assertThat(results.getOtherResults().get(5)).isEqualTo(94L);
assertThat(results.getOtherResults().get(6)).isEqualTo("CHF");
assertThat(results.getOtherResults().get(7)).isEqualTo(100.00);
assertThat(results.getOtherResults().get(8)).isEqualTo("CHF");
/** GBP */
assertThat(results.getOtherResults().get(9)).isEqualTo(400L);
assertThat(results.getOtherResults().get(10)).isEqualTo("GBP");
assertThat(results.getOtherResults().get(11)).isEqualTo(4L);
assertThat(results.getOtherResults().get(12)).isEqualTo(103L);
assertThat(results.getOtherResults().get(13)).isEqualTo("GBP");
assertThat(results.getOtherResults().get(14)).isEqualTo(93L);
assertThat(results.getOtherResults().get(15)).isEqualTo("GBP");
assertThat(results.getOtherResults().get(16)).isEqualTo(100.0);
assertThat(results.getOtherResults().get(17)).isEqualTo("GBP");
/** USD */
assertThat(results.getOtherResults().get(18)).isEqualTo(600L);
assertThat(results.getOtherResults().get(19)).isEqualTo("USD");
assertThat(results.getOtherResults().get(20)).isEqualTo(6L);
assertThat(results.getOtherResults().get(21)).isEqualTo(113L);
assertThat(results.getOtherResults().get(22)).isEqualTo("USD");
assertThat(results.getOtherResults().get(23)).isEqualTo(87L);
assertThat(results.getOtherResults().get(24)).isEqualTo("USD");
assertThat(results.getOtherResults().get(25)).isEqualTo(100.0);
assertThat(results.getOtherResults().get(26)).isEqualTo("USD");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return tx;
});
}
@Test
public void aggregateFunctionsSumByIssuerAndCurrencyAndSortByAggregateSum() {
transaction(database, tx -> {
Amount<Currency> dollars100 = new Amount<>(100, Currency.getInstance("USD"));
Amount<Currency> dollars200 = new Amount<>(200, Currency.getInstance("USD"));
Amount<Currency> pounds300 = new Amount<>(300, Currency.getInstance("GBP"));
Amount<Currency> pounds400 = new Amount<>(400, Currency.getInstance("GBP"));
VaultFiller.fillWithSomeTestCash(services, dollars100, TestConstants.getDUMMY_NOTARY(), 1, 1, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
VaultFiller.fillWithSomeTestCash(services, dollars200, TestConstants.getDUMMY_NOTARY(), 2, 2, new Random(0L), new OpaqueBytes("1".getBytes()), null, getBOC().ref(new OpaqueBytes("1".getBytes())), getBOC_KEY());
VaultFiller.fillWithSomeTestCash(services, pounds300, TestConstants.getDUMMY_NOTARY(), 3, 3, new Random(0L), new OpaqueBytes("1".getBytes()), null, getDUMMY_CASH_ISSUER(), getDUMMY_CASH_ISSUER_KEY());
VaultFiller.fillWithSomeTestCash(services, pounds400, TestConstants.getDUMMY_NOTARY(), 4, 4, new Random(0L), new OpaqueBytes("1".getBytes()), null, getBOC().ref(new OpaqueBytes("1".getBytes())), getBOC_KEY());
try {
// DOCSTART VaultJavaQueryExample23
Field pennies = CashSchemaV1.PersistentCashState.class.getDeclaredField("pennies");
Field currency = CashSchemaV1.PersistentCashState.class.getDeclaredField("currency");
Field issuerParty = CashSchemaV1.PersistentCashState.class.getDeclaredField("issuerParty");
QueryCriteria sumCriteria = new VaultCustomQueryCriteria(Builder.sum(pennies, Arrays.asList(issuerParty, currency), Sort.Direction.DESC));
Vault.Page<Cash.State> results = vaultQuerySvc.queryBy(Cash.State.class, sumCriteria);
// DOCEND VaultJavaQueryExample23
assertThat(results.getOtherResults()).hasSize(12);
assertThat(results.getOtherResults().get(0)).isEqualTo(400L);
assertThat(results.getOtherResults().get(1)).isEqualTo(EncodingUtils.toBase58String(getBOC_PUBKEY()));
assertThat(results.getOtherResults().get(2)).isEqualTo("GBP");
assertThat(results.getOtherResults().get(3)).isEqualTo(300L);
assertThat(results.getOtherResults().get(4)).isEqualTo(EncodingUtils.toBase58String(getDUMMY_CASH_ISSUER().getParty().getOwningKey()));
assertThat(results.getOtherResults().get(5)).isEqualTo("GBP");
assertThat(results.getOtherResults().get(6)).isEqualTo(200L);
assertThat(results.getOtherResults().get(7)).isEqualTo(EncodingUtils.toBase58String(getBOC_PUBKEY()));
assertThat(results.getOtherResults().get(8)).isEqualTo("USD");
assertThat(results.getOtherResults().get(9)).isEqualTo(100L);
assertThat(results.getOtherResults().get(10)).isEqualTo(EncodingUtils.toBase58String(getDUMMY_CASH_ISSUER().getParty().getOwningKey()));
assertThat(results.getOtherResults().get(11)).isEqualTo("USD");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return tx;
});
}
}

View File

@ -1,6 +1,7 @@
package net.corda.node.services.database
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.contracts.asset.DummyFungibleContract
import net.corda.testing.contracts.consumeCash
import net.corda.testing.contracts.fillWithSomeTestCash
@ -31,6 +32,8 @@ import net.corda.schemas.CashSchemaV1
import net.corda.schemas.SampleCashSchemaV2
import net.corda.schemas.SampleCashSchemaV3
import net.corda.testing.BOB_PUBKEY
import net.corda.testing.BOC
import net.corda.testing.BOC_KEY
import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestDataSourceProperties
import org.assertj.core.api.Assertions
@ -301,6 +304,109 @@ class HibernateConfigurationTest {
}
}
@Test
fun `calculate cash balances`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L)) // +$100 = $200
services.fillWithSomeTestCash(50.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // £50 = £50
services.fillWithSomeTestCash(25.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // +£25 = £175
services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L)) // CHF500 = CHF500
services.fillWithSomeTestCash(250.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L)) // +CHF250 = CHF750
}
// structure query
val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java)
val cashStates = criteriaQuery.from(CashSchemaV1.PersistentCashState::class.java)
// aggregate function
criteriaQuery.multiselect(cashStates.get<String>("currency"),
criteriaBuilder.sum(cashStates.get<Long>("pennies")))
// group by
criteriaQuery.groupBy(cashStates.get<String>("currency"))
// execute query
val queryResults = entityManager.createQuery(criteriaQuery).resultList
queryResults.forEach { tuple -> println("${tuple.get(0)} = ${tuple.get(1)}") }
assertThat(queryResults[0].get(0)).isEqualTo("CHF")
assertThat(queryResults[0].get(1)).isEqualTo(75000L)
assertThat(queryResults[1].get(0)).isEqualTo("GBP")
assertThat(queryResults[1].get(1)).isEqualTo(7500L)
assertThat(queryResults[2].get(0)).isEqualTo("USD")
assertThat(queryResults[2].get(1)).isEqualTo(20000L)
}
@Test
fun `calculate cash balance for single currency`() {
database.transaction {
services.fillWithSomeTestCash(50.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // £50 = £50
services.fillWithSomeTestCash(25.POUNDS, DUMMY_NOTARY, 5, 5, Random(0L)) // +£25 = £175
}
// structure query
val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java)
val cashStates = criteriaQuery.from(CashSchemaV1.PersistentCashState::class.java)
// aggregate function
criteriaQuery.multiselect(cashStates.get<String>("currency"),
criteriaBuilder.sum(cashStates.get<Long>("pennies")))
// where
criteriaQuery.where(criteriaBuilder.equal(cashStates.get<String>("currency"), "GBP"))
// group by
criteriaQuery.groupBy(cashStates.get<String>("currency"))
// execute query
val queryResults = entityManager.createQuery(criteriaQuery).resultList
queryResults.forEach { tuple -> println("${tuple.get(0)} = ${tuple.get(1)}") }
assertThat(queryResults[0].get(0)).isEqualTo("GBP")
assertThat(queryResults[0].get(1)).isEqualTo(7500L)
}
@Test
fun `calculate and order by cash balance for owner and currency`() {
database.transaction {
services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = BOC.ref(1), issuerKey = BOC_KEY)
services.fillWithSomeTestCash(300.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER)
services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L), issuedBy = BOC.ref(2), issuerKey = BOC_KEY)
}
// structure query
val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java)
val cashStates = criteriaQuery.from(CashSchemaV1.PersistentCashState::class.java)
// aggregate function
criteriaQuery.multiselect(cashStates.get<String>("currency"),
criteriaBuilder.sum(cashStates.get<Long>("pennies")))
// group by
criteriaQuery.groupBy(cashStates.get<String>("issuerParty"), cashStates.get<String>("currency"))
// order by
criteriaQuery.orderBy(criteriaBuilder.desc(criteriaBuilder.sum(cashStates.get<Long>("pennies"))))
// execute query
val queryResults = entityManager.createQuery(criteriaQuery).resultList
queryResults.forEach { tuple -> println("${tuple.get(0)} = ${tuple.get(1)}") }
assertThat(queryResults).hasSize(4)
assertThat(queryResults[0].get(0)).isEqualTo("GBP")
assertThat(queryResults[0].get(1)).isEqualTo(40000L)
assertThat(queryResults[1].get(0)).isEqualTo("GBP")
assertThat(queryResults[1].get(1)).isEqualTo(30000L)
assertThat(queryResults[2].get(0)).isEqualTo("USD")
assertThat(queryResults[2].get(1)).isEqualTo(20000L)
assertThat(queryResults[3].get(0)).isEqualTo("USD")
assertThat(queryResults[3].get(1)).isEqualTo(10000L)
}
/**
* CashSchemaV2 = optimised Cash schema (extending FungibleState)
*/

View File

@ -3,12 +3,12 @@ package net.corda.node.services.vault
import net.corda.contracts.CommercialPaper
import net.corda.contracts.Commodity
import net.corda.contracts.DealState
import net.corda.contracts.DummyDealContract
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.core.contracts.*
import net.corda.core.contracts.testing.DummyLinearContract
import net.corda.core.crypto.entropyToKeyPair
import net.corda.core.crypto.toBase58String
import net.corda.core.days
import net.corda.core.identity.Party
import net.corda.core.node.services.*
@ -18,9 +18,6 @@ import net.corda.core.schemas.testing.DummyLinearStateSchemaV1
import net.corda.core.seconds
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.testing.DUMMY_NOTARY
import net.corda.testing.DUMMY_NOTARY_KEY
import net.corda.testing.TEST_TX_TIME
import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1
@ -556,6 +553,143 @@ class VaultQueryTests {
}
}
@Test
fun `aggregate functions without group clause`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L))
services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L))
services.fillWithSomeTestCash(300.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L))
services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L))
services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L))
// DOCSTART VaultQueryExample21
val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum() }
val sumCriteria = VaultCustomQueryCriteria(sum)
val count = builder { CashSchemaV1.PersistentCashState::pennies.count() }
val countCriteria = VaultCustomQueryCriteria(count)
val max = builder { CashSchemaV1.PersistentCashState::pennies.max() }
val maxCriteria = VaultCustomQueryCriteria(max)
val min = builder { CashSchemaV1.PersistentCashState::pennies.min() }
val minCriteria = VaultCustomQueryCriteria(min)
val avg = builder { CashSchemaV1.PersistentCashState::pennies.avg() }
val avgCriteria = VaultCustomQueryCriteria(avg)
val results = vaultQuerySvc.queryBy<FungibleAsset<*>>(sumCriteria
.and(countCriteria)
.and(maxCriteria)
.and(minCriteria)
.and(avgCriteria))
// DOCEND VaultQueryExample21
assertThat(results.otherResults).hasSize(5)
assertThat(results.otherResults[0]).isEqualTo(150000L)
assertThat(results.otherResults[1]).isEqualTo(15L)
assertThat(results.otherResults[2]).isEqualTo(11298L)
assertThat(results.otherResults[3]).isEqualTo(8702L)
assertThat(results.otherResults[4]).isEqualTo(10000.0)
}
}
@Test
fun `aggregate functions with single group clause`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L))
services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L))
services.fillWithSomeTestCash(300.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L))
services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L))
services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L))
// DOCSTART VaultQueryExample22
val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val sumCriteria = VaultCustomQueryCriteria(sum)
val max = builder { CashSchemaV1.PersistentCashState::pennies.max(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val maxCriteria = VaultCustomQueryCriteria(max)
val min = builder { CashSchemaV1.PersistentCashState::pennies.min(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val minCriteria = VaultCustomQueryCriteria(min)
val avg = builder { CashSchemaV1.PersistentCashState::pennies.avg(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val avgCriteria = VaultCustomQueryCriteria(avg)
val results = vaultQuerySvc.queryBy<FungibleAsset<*>>(sumCriteria
.and(maxCriteria)
.and(minCriteria)
.and(avgCriteria))
// DOCEND VaultQueryExample22
assertThat(results.otherResults).hasSize(24)
/** CHF */
assertThat(results.otherResults[0]).isEqualTo(50000L)
assertThat(results.otherResults[1]).isEqualTo("CHF")
assertThat(results.otherResults[2]).isEqualTo(10274L)
assertThat(results.otherResults[3]).isEqualTo("CHF")
assertThat(results.otherResults[4]).isEqualTo(9481L)
assertThat(results.otherResults[5]).isEqualTo("CHF")
assertThat(results.otherResults[6]).isEqualTo(10000.0)
assertThat(results.otherResults[7]).isEqualTo("CHF")
/** GBP */
assertThat(results.otherResults[8]).isEqualTo(40000L)
assertThat(results.otherResults[9]).isEqualTo("GBP")
assertThat(results.otherResults[10]).isEqualTo(10343L)
assertThat(results.otherResults[11]).isEqualTo("GBP")
assertThat(results.otherResults[12]).isEqualTo(9351L)
assertThat(results.otherResults[13]).isEqualTo("GBP")
assertThat(results.otherResults[14]).isEqualTo(10000.0)
assertThat(results.otherResults[15]).isEqualTo("GBP")
/** USD */
assertThat(results.otherResults[16]).isEqualTo(60000L)
assertThat(results.otherResults[17]).isEqualTo("USD")
assertThat(results.otherResults[18]).isEqualTo(11298L)
assertThat(results.otherResults[19]).isEqualTo("USD")
assertThat(results.otherResults[20]).isEqualTo(8702L)
assertThat(results.otherResults[21]).isEqualTo("USD")
assertThat(results.otherResults[22]).isEqualTo(10000.0)
assertThat(results.otherResults[23]).isEqualTo("USD")
}
}
@Test
fun `aggregate functions sum by issuer and currency and sort by aggregate sum`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = DUMMY_CASH_ISSUER)
services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L), issuedBy = BOC.ref(1), issuerKey = BOC_KEY)
services.fillWithSomeTestCash(300.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L), issuedBy = DUMMY_CASH_ISSUER)
services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L), issuedBy = BOC.ref(2), issuerKey = BOC_KEY)
// DOCSTART VaultQueryExample23
val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::issuerParty,
CashSchemaV1.PersistentCashState::currency),
orderBy = Sort.Direction.DESC)
}
val results = vaultQuerySvc.queryBy<FungibleAsset<*>>(VaultCustomQueryCriteria(sum))
// DOCEND VaultQueryExample23
assertThat(results.otherResults).hasSize(12)
assertThat(results.otherResults[0]).isEqualTo(40000L)
assertThat(results.otherResults[1]).isEqualTo(BOC_PUBKEY.toBase58String())
assertThat(results.otherResults[2]).isEqualTo("GBP")
assertThat(results.otherResults[3]).isEqualTo(30000L)
assertThat(results.otherResults[4]).isEqualTo(DUMMY_CASH_ISSUER.party.owningKey.toBase58String())
assertThat(results.otherResults[5]).isEqualTo("GBP")
assertThat(results.otherResults[6]).isEqualTo(20000L)
assertThat(results.otherResults[7]).isEqualTo(BOC_PUBKEY.toBase58String())
assertThat(results.otherResults[8]).isEqualTo("USD")
assertThat(results.otherResults[9]).isEqualTo(10000L)
assertThat(results.otherResults[10]).isEqualTo(DUMMY_CASH_ISSUER.party.owningKey.toBase58String())
assertThat(results.otherResults[11]).isEqualTo("USD")
}
}
private val TODAY = LocalDate.now().atStartOfDay().toInstant(ZoneOffset.UTC)
@Test
@ -862,7 +996,7 @@ class VaultQueryTests {
// DOCSTART VaultQueryExample9
val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.ALL)
val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL)
val results = vaultQuerySvc.queryBy<LinearState>(linearStateCriteria.and(vaultCriteria))
val results = vaultQuerySvc.queryBy<LinearState>(linearStateCriteria and vaultCriteria)
// DOCEND VaultQueryExample9
assertThat(results.states).hasSize(4)
}
@ -1176,6 +1310,52 @@ class VaultQueryTests {
}
}
@Test
fun `unconsumed cash balance for single currency`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L))
services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L))
val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val sumCriteria = VaultCustomQueryCriteria(sum)
val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(USD.currencyCode) }
val ccyCriteria = VaultCustomQueryCriteria(ccyIndex)
val results = vaultQuerySvc.queryBy<FungibleAsset<*>>(sumCriteria.and(ccyCriteria))
assertThat(results.otherResults).hasSize(2)
assertThat(results.otherResults[0]).isEqualTo(30000L)
assertThat(results.otherResults[1]).isEqualTo("USD")
}
}
@Test
fun `unconsumed cash balances for all currencies`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L))
services.fillWithSomeTestCash(200.DOLLARS, DUMMY_NOTARY, 2, 2, Random(0L))
services.fillWithSomeTestCash(300.POUNDS, DUMMY_NOTARY, 3, 3, Random(0L))
services.fillWithSomeTestCash(400.POUNDS, DUMMY_NOTARY, 4, 4, Random(0L))
services.fillWithSomeTestCash(500.SWISS_FRANCS, DUMMY_NOTARY, 5, 5, Random(0L))
services.fillWithSomeTestCash(600.SWISS_FRANCS, DUMMY_NOTARY, 6, 6, Random(0L))
val ccyIndex = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val criteria = VaultCustomQueryCriteria(ccyIndex)
val results = vaultQuerySvc.queryBy<FungibleAsset<*>>(criteria)
assertThat(results.otherResults).hasSize(6)
assertThat(results.otherResults[0]).isEqualTo(110000L)
assertThat(results.otherResults[1]).isEqualTo("CHF")
assertThat(results.otherResults[2]).isEqualTo(70000L)
assertThat(results.otherResults[3]).isEqualTo("GBP")
assertThat(results.otherResults[4]).isEqualTo(30000L)
assertThat(results.otherResults[5]).isEqualTo("USD")
}
}
@Test
fun `unconsumed fungible assets for quantity greater than`() {
database.transaction {