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

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