diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 9dd0e974d1..6677352c6c 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -26,32 +26,36 @@ sealed class QueryCriteria { @CordaSerializable data class TimeCondition(val type: TimeInstantType, val predicate: ColumnPredicate) - /** - * VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] - */ - data class VaultQueryCriteria @JvmOverloads constructor ( - val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - val contractStateTypes: Set>? = null, - val stateRefs: List? = null, - val notaryName: List? = null, - val includeSoftlockedStates: Boolean = true, - val timeCondition: TimeCondition? = null) : QueryCriteria() { - + abstract class CommonQueryCriteria : QueryCriteria() { + abstract val status: Vault.StateStatus override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseCriteria(this) } } + /** + * VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] + */ + data class VaultQueryCriteria @JvmOverloads constructor (override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + val contractStateTypes: Set>? = null, + val stateRefs: List? = null, + val notaryName: List? = null, + val includeSoftlockedStates: Boolean = true, + val timeCondition: TimeCondition? = null) : CommonQueryCriteria() { + override fun visit(parser: IQueryCriteriaParser): Collection { + return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) + } + } + /** * LinearStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultLinearState] */ - data class LinearStateQueryCriteria @JvmOverloads constructor( - val participants: List? = null, - val linearId: List? = null, - val dealRef: List? = null) : QueryCriteria() { - + data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List? = null, + val linearId: List? = null, + val dealRef: List? = null, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseCriteria(this) + return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) } } @@ -62,15 +66,14 @@ sealed class QueryCriteria { * [Currency] as used in [Cash] contract state * [Commodity] as used in [CommodityContract] state */ - data class FungibleAssetQueryCriteria @JvmOverloads constructor( - val participants: List? = null, - val owner: List? = null, - val quantity: ColumnPredicate? = null, - val issuerPartyName: List? = null, - val issuerRef: List? = null) : QueryCriteria() { - + data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List? = null, + val owner: List? = null, + val quantity: ColumnPredicate? = null, + val issuerPartyName: List? = null, + val issuerRef: List? = null, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseCriteria(this) + return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) } } @@ -84,9 +87,11 @@ sealed class QueryCriteria { * * Refer to [CommercialPaper.State] for a concrete example. */ - data class VaultCustomQueryCriteria(val expression: CriteriaExpression) : QueryCriteria() { + data class VaultCustomQueryCriteria @JvmOverloads constructor + (val expression: CriteriaExpression, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { - return parser.parseCriteria(this) + return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this)) } } @@ -112,6 +117,7 @@ sealed class QueryCriteria { } interface IQueryCriteriaParser { + fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection fun parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria): Collection diff --git a/docs/source/api-vault-query.rst b/docs/source/api-vault-query.rst index 3651f06e44..817318380f 100644 --- a/docs/source/api-vault-query.rst +++ b/docs/source/api-vault-query.rst @@ -78,6 +78,8 @@ There are four implementations of this interface which can be chained together t :end-before: DOCEND VaultQueryExample20 All ``QueryCriteria`` implementations are composable using ``and`` and ``or`` operators, as also illustrated above. + +All ``QueryCriteria`` implementations provide an explicitly specifiable ``StateStatus`` attribute which defaults to filtering on UNCONSUMED states. .. note:: Custom contract states that implement the ``Queryable`` interface may now extend common schemas types ``FungiblePersistentState`` or, ``LinearPersistentState``. Previously, all custom contracts extended the root ``PersistentState`` class and defined repeated mappings of ``FungibleAsset`` and ``LinearState`` attributes. See ``SampleCashSchemaV2`` and ``DummyLinearStateSchemaV2`` as examples. diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 9698ed58ad..959eeb73b7 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -7,6 +7,7 @@ import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.vault.* +import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.OpaqueBytes @@ -41,13 +42,6 @@ class HibernateQueryCriteriaParser(val contractType: Class, log.trace { "Parsing VaultQueryCriteria: $criteria" } val predicateSet = mutableSetOf() - // state status - stateTypes = criteria.status - if (criteria.status == Vault.StateStatus.ALL) - predicateSet.add(vaultStates.get("stateStatus").`in`(setOf(Vault.StateStatus.UNCONSUMED, Vault.StateStatus.CONSUMED))) - else - predicateSet.add(criteriaBuilder.equal(vaultStates.get("stateStatus"), criteria.status)) - // contract State Types val combinedContractTypeTypes = criteria.contractStateTypes?.plus(contractType) ?: setOf(contractType) combinedContractTypeTypes.filter { it.name != ContractState::class.java.name }.let { @@ -217,6 +211,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) rootEntities.putIfAbsent(VaultSchemaV1.VaultLinearStates::class.java, vaultLinearStates) + val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) joinPredicates.add(joinPredicate) @@ -255,6 +250,7 @@ class HibernateQueryCriteriaParser(val contractType: Class, try { val entityRoot = criteriaQuery.from(entityClass) rootEntities.putIfAbsent(entityClass, entityRoot) + val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), entityRoot.get("stateRef")) joinPredicates.add(joinPredicate) @@ -315,6 +311,18 @@ class HibernateQueryCriteriaParser(val contractType: Class, return predicateSet } + override fun parseCriteria(criteria: CommonQueryCriteria): Collection { + log.trace { "Parsing CommonQueryCriteria: $criteria" } + val predicateSet = mutableSetOf() + + // state status + stateTypes = criteria.status + if (criteria.status != Vault.StateStatus.ALL) + predicateSet.add(criteriaBuilder.equal(vaultStates.get("stateStatus"), criteria.status)) + + return predicateSet + } + private fun parse(sorting: Sort) { log.trace { "Parsing sorting specification: $sorting" } diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index 305a14407a..ec918900cd 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -279,12 +279,8 @@ public class VaultQueryJavaTests { List linearIds = Arrays.asList(uid); List dealParty = Arrays.asList(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); PageSpecification pageSpec = new PageSpecification(0, getMAX_PAGE_SIZE()); @@ -296,7 +292,7 @@ public class VaultQueryJavaTests { Observable updates = results.getFuture(); // DOCEND VaultJavaQueryExample5 - assertThat(snapshot.getStates()).hasSize(4); + assertThat(snapshot.getStates()).hasSize(13); return tx; }); diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 3183a5546a..68589a78da 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -753,6 +753,20 @@ class VaultQueryTests { } } + @Test + fun `unconsumed cash fungible assets after spending`() { + database.transaction { + + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L)) + services.consumeCash(50.DOLLARS) + // should now have x2 CONSUMED + x2 UNCONSUMED (one spent + one change) + + val results = vaultQuerySvc.queryBy(FungibleAssetQueryCriteria()) + assertThat(results.statesMetadata).hasSize(2) + assertThat(results.states).hasSize(2) + } + } + @Test fun `consumed cash fungible assets`() { database.transaction { @@ -845,7 +859,7 @@ class VaultQueryTests { // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" // DOCSTART VaultQueryExample9 - val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId)) + val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.ALL) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria)) // DOCEND VaultQueryExample9 @@ -864,7 +878,7 @@ class VaultQueryTests { services.evolveLinearStates(linearStates) // consume current and produce new state reference // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" - val linearStateCriteria = LinearStateQueryCriteria(linearId = linearStates.map { it.state.data.linearId }) + val linearStateCriteria = LinearStateQueryCriteria(linearId = linearStates.map { it.state.data.linearId }, status = Vault.StateStatus.ALL) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.ALL) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) @@ -899,18 +913,15 @@ class VaultQueryTests { val uid = linearStates.states.first().state.data.linearId services.fillWithSomeTestDeals(listOf("123", "456", "789")) - val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.UNCONSUMED) val linearStateCriteria = LinearStateQueryCriteria(linearId = listOf(uid)) val dealStateCriteria = LinearStateQueryCriteria(dealRef = listOf("123", "456", "789")) - val compositeCriteria = vaultCriteria.and(linearStateCriteria).or(dealStateCriteria) + val compositeCriteria = linearStateCriteria or dealStateCriteria val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.DEAL_REFERENCE), Sort.Direction.DESC))) val results = vaultQuerySvc.queryBy(compositeCriteria, sorting = sorting) - results.states.forEach { - if (it.state.data is DummyDealContract.State) - println("${(it.state.data as DealState).ref}, ${it.state.data.linearId}") } - assertThat(results.states).hasSize(4) + assertThat(results.statesMetadata).hasSize(13) + assertThat(results.states).hasSize(13) } } @@ -942,7 +953,7 @@ class VaultQueryTests { services.evolveLinearState(linearState3) // consume current and produce new state reference // should now have 1 UNCONSUMED & 3 CONSUMED state refs for Linear State with "TEST" - val linearStateCriteria = LinearStateQueryCriteria(linearId = txns.states.map { it.state.data.linearId }) + val linearStateCriteria = LinearStateQueryCriteria(linearId = txns.states.map { it.state.data.linearId }, status = Vault.StateStatus.CONSUMED) val vaultCriteria = VaultQueryCriteria(status = Vault.StateStatus.CONSUMED) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.LinearStateAttribute.UUID), Sort.Direction.DESC))) val results = vaultQuerySvc.queryBy(linearStateCriteria.and(vaultCriteria), sorting = sorting) @@ -966,8 +977,12 @@ class VaultQueryTests { // DOCSTART VaultDeprecatedQueryExample1 val states = vaultSvc.linearHeadsOfType().filter { it.key == linearId } // DOCEND VaultDeprecatedQueryExample1 - assertThat(states).hasSize(1) + + // validate against new query api + val results = vaultQuerySvc.queryBy(LinearStateQueryCriteria(linearId = listOf(linearId))) + assertThat(results.statesMetadata).hasSize(1) + assertThat(results.states).hasSize(1) } } @@ -987,8 +1002,12 @@ class VaultQueryTests { // DOCSTART VaultDeprecatedQueryExample2 val states = vaultSvc.consumedStates().filter { it.state.data.linearId == linearId } // DOCEND VaultDeprecatedQueryExample2 - assertThat(states).hasSize(3) + + // validate against new query api + val results = vaultQuerySvc.queryBy(LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.CONSUMED)) + assertThat(results.statesMetadata).hasSize(3) + assertThat(results.states).hasSize(3) } } @@ -1009,8 +1028,12 @@ class VaultQueryTests { val states = vaultSvc.states(setOf(DummyLinearContract.State::class.java), EnumSet.of(Vault.StateStatus.CONSUMED, Vault.StateStatus.UNCONSUMED)).filter { it.state.data.linearId == linearId } // DOCEND VaultDeprecatedQueryExample3 - assertThat(states).hasSize(4) + + // validate against new query api + val results = vaultQuerySvc.queryBy(LinearStateQueryCriteria(linearId = listOf(linearId), status = Vault.StateStatus.ALL)) + assertThat(results.statesMetadata).hasSize(4) + assertThat(results.states).hasSize(4) } } @@ -1420,7 +1443,7 @@ class VaultQueryTests { services.fillWithSomeTestLinearStates(1, "TEST2") val uuid = services.fillWithSomeTestLinearStates(1, "TEST3").states.first().state.data.linearId.id - // 2 unconsumed states with same external ID + // 2 unconsumed states with same external ID, 1 with different external ID val results = builder { val externalIdCondition = VaultSchemaV1.VaultLinearStates::externalId.equal("TEST2") @@ -1429,10 +1452,11 @@ class VaultQueryTests { val uuidCondition = VaultSchemaV1.VaultLinearStates::uuid.equal(uuid) val uuidCustomCriteria = VaultCustomQueryCriteria(uuidCondition) - val criteria = externalIdCustomCriteria.or(uuidCustomCriteria) + val criteria = externalIdCustomCriteria or uuidCustomCriteria vaultQuerySvc.queryBy(criteria) } - assertThat(results.states).hasSize(2) + assertThat(results.statesMetadata).hasSize(3) + assertThat(results.states).hasSize(3) } }