mirror of
https://github.com/corda/corda.git
synced 2025-01-20 03:36:29 +00:00
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:
parent
baaef30d5b
commit
44f57639d2
@ -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].
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user