From 80c6c46d6fcea94cb92f2f5e995bc634375aefa1 Mon Sep 17 00:00:00 2001 From: josecoll Date: Thu, 30 Aug 2018 17:00:50 +0200 Subject: [PATCH] CORDA-1888 - Fix Vault Query composite queries (#3775) (#3869) * CORDA-1888 Fix Vault Query composite queries (#3775) * Reproduce composite query failures. * Fixes to OR querying and composite queries that use the same QueryCriteria (Linear, Fungible, Custom) more than once. * Revert debug logging for Hibernate SQL. * Cleanup and remove redundant joinPredicates global var. * Fix failing Java Unit test. * Fix Java compilation error in example-code section of docs. * Include copy() function for original constructor to maintain backwards API compatibility. * Fixed compilation errors post cherry-pick. --- .../vault/HibernateQueryCriteriaParser.kt | 55 ++++++++++------ .../services/vault/VaultQueryJavaTests.java | 17 ++--- .../persistence/HibernateConfigurationTest.kt | 47 ++++++++++++++ .../node/services/vault/VaultQueryTests.kt | 64 ++++++++++++++++++- .../net/corda/testing/contracts/DummyState.kt | 8 ++- .../testing/internal/vault/VaultFiller.kt | 22 ++++++- 6 files changed, 178 insertions(+), 35 deletions(-) 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 94ffb00d90..49a121cd04 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 @@ -33,7 +33,9 @@ abstract class AbstractQueryCriteriaParser, in P: val leftPredicates = parse(left) val rightPredicates = parse(right) - val orPredicate = criteriaBuilder.or(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()) + val leftAnd = criteriaBuilder.and(*leftPredicates.toTypedArray()) + val rightAnd = criteriaBuilder.and(*rightPredicates.toTypedArray()) + val orPredicate = criteriaBuilder.or(leftAnd,rightAnd) predicateSet.add(orPredicate) return predicateSet @@ -173,8 +175,6 @@ class HibernateQueryCriteriaParser(val contractStateType: Class() // incrementally build list of root entities (for later use in Sort parsing) private val rootEntities = mutableMapOf, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates)) private val aggregateExpressions = mutableListOf>() @@ -323,8 +323,14 @@ class HibernateQueryCriteriaParser(val contractStateType: Class() - val vaultFungibleStates = criteriaQuery.from(VaultSchemaV1.VaultFungibleStates::class.java) - rootEntities.putIfAbsent(VaultSchemaV1.VaultFungibleStates::class.java, vaultFungibleStates) + // ensure we re-use any existing instance of the same root entity + val entityStateClass = VaultSchemaV1.VaultFungibleStates::class.java + val vaultFungibleStates = + rootEntities.getOrElse(entityStateClass) { + val entityRoot = criteriaQuery.from(entityStateClass) + rootEntities[entityStateClass] = entityRoot + entityRoot + } val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultFungibleStates.get("stateRef")) predicateSet.add(joinPredicate) @@ -355,7 +361,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class - val joinLinearStateToParty = vaultFungibleStates.joinSet("participants") + val joinLinearStateToParty = vaultFungibleStates.joinSet("participants") predicateSet.add(criteriaBuilder.and(joinLinearStateToParty.`in`(participants))) criteriaQuery.distinct(true) } @@ -367,11 +373,17 @@ class HibernateQueryCriteriaParser(val contractStateType: Class() - val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) - rootEntities.putIfAbsent(VaultSchemaV1.VaultLinearStates::class.java, vaultLinearStates) + // ensure we re-use any existing instance of the same root entity + val entityStateClass = VaultSchemaV1.VaultLinearStates::class.java + val vaultLinearStates = + rootEntities.getOrElse(entityStateClass) { + val entityRoot = criteriaQuery.from(entityStateClass) + rootEntities[entityStateClass] = entityRoot + entityRoot + } val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) - joinPredicates.add(joinPredicate) + predicateSet.add(joinPredicate) // linear ids UUID criteria.uuid?.let { @@ -400,22 +412,28 @@ class HibernateQueryCriteriaParser(val contractStateType: Class() - val entityClass = resolveEnclosingObjectFromExpression(criteria.expression) + val entityStateClass = resolveEnclosingObjectFromExpression(criteria.expression) try { - val entityRoot = criteriaQuery.from(entityClass) - rootEntities.putIfAbsent(entityClass, entityRoot) + // ensure we re-use any existing instance of the same root entity + val entityRoot = + rootEntities.getOrElse(entityStateClass) { + val entityRoot = criteriaQuery.from(entityStateClass) + rootEntities[entityStateClass] = entityRoot + entityRoot + } val joinPredicate = criteriaBuilder.equal(vaultStates.get("stateRef"), entityRoot.get("stateRef")) - joinPredicates.add(joinPredicate) + predicateSet.add(joinPredicate) // resolve general criteria expressions - parseExpression(entityRoot, criteria.expression, predicateSet) + @Suppress("UNCHECKED_CAST") + parseExpression(entityRoot as Root, criteria.expression, predicateSet) } catch (e: Exception) { e.message?.let { message -> if (message.contains("Not an entity")) throw VaultQueryException(""" - Please register the entity '${entityClass.name}' + Please register the entity '${entityStateClass.name}' See https://docs.corda.net/api-persistence.html#custom-schema-registration for more information""") } throw VaultQueryException("Parsing error: ${e.message}") @@ -437,7 +455,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class("stateRef"), entityRoot.get("stateRef")) - joinPredicates.add(joinPredicate) + rootEntities[entityStateClass] = entityRoot entityRoot } when (direction) { @@ -515,7 +531,6 @@ class HibernateQueryCriteriaParser(val contractStateType: Class dealIds = asList("123", "456", "789"); - UniqueIdentifier uid = - database.transaction(tx -> { - Vault states = vaultFiller.fillWithSomeTestLinearStates(10, null); - UniqueIdentifier _uid = states.getStates().iterator().next().component1().getData().getLinearId(); - vaultFiller.fillWithSomeTestDeals(dealIds); - return _uid; - }); + UniqueIdentifier uid = new UniqueIdentifier("999", UUID.randomUUID()); + database.transaction(tx -> { + vaultFiller.fillWithSomeTestLinearStates(10, null); + vaultFiller.fillWithSomeTestLinearStates(1, null, emptyList(), uid); + vaultFiller.fillWithSomeTestDeals(dealIds); + return tx; + }); database.transaction(tx -> { // DOCSTART VaultJavaQueryExample5 @SuppressWarnings("unchecked") @@ -344,7 +345,7 @@ public class VaultQueryJavaTests { Vault.Page snapshot = results.getSnapshot(); // DOCEND VaultJavaQueryExample5 - assertThat(snapshot.getStates()).hasSize(13); + assertThat(snapshot.getStates()).hasSize(4); return tx; }); diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt index d6ed3fb51d..6434194ba5 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/HibernateConfigurationTest.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionState +import net.corda.core.contracts.UniqueIdentifier import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty @@ -622,6 +623,52 @@ class HibernateConfigurationTest { assertThat(queryResults).hasSize(10) } + /** + * Composite OR query + */ + @Test + fun `composite or query across VaultStates, VaultLinearStates and DummyLinearStates`() { + val uniqueID456 = UniqueIdentifier("456") + database.transaction { + vaultFiller.fillWithSomeTestLinearStates(1, externalId = "123", linearString = "123", linearNumber = 123, linearBoolean = true) + vaultFiller.fillWithSomeTestLinearStates(1, uniqueIdentifier = uniqueID456) + vaultFiller.fillWithSomeTestLinearStates(1, externalId = "789") + } + val sessionFactory = sessionFactoryForSchemas(VaultSchemaV1, DummyLinearStateSchemaV1) + val criteriaBuilder = sessionFactory.criteriaBuilder + val entityManager = sessionFactory.createEntityManager() + + // structure query + val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java) + val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + val vaultLinearStates = criteriaQuery.from(VaultSchemaV1.VaultLinearStates::class.java) + val dummyLinearStates = criteriaQuery.from(DummyLinearStateSchemaV1.PersistentDummyLinearState::class.java) + + criteriaQuery.select(vaultStates) + val joinPredicate1 = criteriaBuilder.equal(vaultStates.get("stateRef"), vaultLinearStates.get("stateRef")) + val joinPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(vaultStates.get("stateRef"), dummyLinearStates.get("stateRef"))) + + // and predicates on VaultLinearStates + val andLinearStatesPredicate1 = criteriaBuilder.and(criteriaBuilder.equal(vaultLinearStates.get("externalId"), uniqueID456.externalId)) + val andLinearStatesPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(vaultLinearStates.get("uuid"), uniqueID456.id)) + val andLinearStatesPredicate = criteriaBuilder.and(andLinearStatesPredicate1, andLinearStatesPredicate2) + + // and predicates on PersistentDummyLinearState + val andDummyLinearStatesPredicate1 = criteriaBuilder.and(criteriaBuilder.equal(dummyLinearStates.get("linearString"), "123")) + val andDummyLinearStatesPredicate2 = criteriaBuilder.and(criteriaBuilder.equal(dummyLinearStates.get("linearNumber"), 123L)) + val andDummyLinearStatesPredicate3 = criteriaBuilder.and(criteriaBuilder.equal(dummyLinearStates.get("linearBoolean"), true)) + val andDummyLinearStatesPredicate = criteriaBuilder.and(andDummyLinearStatesPredicate1, criteriaBuilder.and(andDummyLinearStatesPredicate2, andDummyLinearStatesPredicate3)) + + // or predicates + val orPredicates = criteriaBuilder.or(andLinearStatesPredicate, andDummyLinearStatesPredicate) + + criteriaQuery.where(joinPredicate1, joinPredicate2, orPredicates) + + // execute query + val queryResults = entityManager.createQuery(criteriaQuery).resultList + assertThat(queryResults).hasSize(2) + } + /** * Test a OneToOne table mapping */ 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 93998232f3..11988f5524 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 @@ -1276,11 +1276,12 @@ class VaultQueryTests { @Test fun `unconsumed deal states sorted`() { database.transaction { - val linearStates = vaultFiller.fillWithSomeTestLinearStates(10) + vaultFiller.fillWithSomeTestLinearStates(10) + val uid = UniqueIdentifier("999") + vaultFiller.fillWithSomeTestLinearStates(1, uniqueIdentifier = uid) vaultFiller.fillWithSomeTestDeals(listOf("123", "456", "789")) - val uid = linearStates.states.first().state.data.linearId.id - val linearStateCriteria = LinearStateQueryCriteria(uuid = listOf(uid)) + val linearStateCriteria = LinearStateQueryCriteria(uuid = listOf(uid.id)) val dealStateCriteria = LinearStateQueryCriteria(externalId = listOf("123", "456", "789")) val compositeCriteria = linearStateCriteria or dealStateCriteria @@ -1810,6 +1811,63 @@ class VaultQueryTests { } } + @Test + fun `composite query for fungible and linear states`() { + database.transaction { + vaultFiller.fillWithSomeTestLinearStates(1, "TEST1") + vaultFiller.fillWithSomeTestDeals(listOf("123")) + vaultFiller.fillWithSomeTestCash(100.DOLLARS, notaryServices, 1, DUMMY_CASH_ISSUER, services.myInfo.singleIdentity()) + vaultFiller.fillWithSomeTestCommodity(Amount(100, Commodity.getInstance("FCOJ")!!), notaryServices, DUMMY_OBLIGATION_ISSUER.ref(1)) + vaultFiller.fillWithDummyState() + // all contract states query + val results = vaultService.queryBy() + assertThat(results.states).hasSize(5) + // linear states only query + val linearStateCriteria = LinearStateQueryCriteria() + val resultsLSC = vaultService.queryBy(linearStateCriteria) + assertThat(resultsLSC.states).hasSize(2) + // fungible asset states only query + val fungibleAssetStateCriteria = FungibleAssetQueryCriteria() + val resultsFASC = vaultService.queryBy(fungibleAssetStateCriteria) + assertThat(resultsFASC.states).hasSize(2) + // composite OR query for both linear and fungible asset states (eg. all states in either Fungible and Linear states tables) + val resultsCompositeOr = vaultService.queryBy(fungibleAssetStateCriteria.or(linearStateCriteria)) + assertThat(resultsCompositeOr.states).hasSize(4) + // composite AND query for both linear and fungible asset states (eg. all states in both Fungible and Linear states tables) + val resultsCompositeAnd = vaultService.queryBy(fungibleAssetStateCriteria.and(linearStateCriteria)) + assertThat(resultsCompositeAnd.states).hasSize(0) + } + } + + @Test + fun `composite query for fungible and linear states for multiple participants`() { + database.transaction { + identitySvc.verifyAndRegisterIdentity(ALICE_IDENTITY) + identitySvc.verifyAndRegisterIdentity(BOB_IDENTITY) + identitySvc.verifyAndRegisterIdentity(CHARLIE_IDENTITY) + vaultFiller.fillWithSomeTestLinearStates(1, "TEST1", listOf(ALICE)) + vaultFiller.fillWithSomeTestLinearStates(1, "TEST2", listOf(BOB)) + vaultFiller.fillWithSomeTestLinearStates(1, "TEST3", listOf(CHARLIE)) + vaultFiller.fillWithSomeTestCash(100.DOLLARS, notaryServices, 1, DUMMY_CASH_ISSUER) + vaultFiller.fillWithSomeTestCommodity(Amount(100, Commodity.getInstance("FCOJ")!!), notaryServices, DUMMY_OBLIGATION_ISSUER.ref(1)) + vaultFiller.fillWithDummyState() + // all contract states query + val results = vaultService.queryBy() + assertThat(results.states).hasSize(6) + // linear states by participants only query + val linearStateCriteria = LinearStateQueryCriteria(participants = listOf(ALICE,BOB)) + val resultsLSC = vaultService.queryBy(linearStateCriteria) + assertThat(resultsLSC.states).hasSize(2) + // fungible asset states by participants only query + val fungibleAssetStateCriteria = FungibleAssetQueryCriteria(participants = listOf(services.myInfo.singleIdentity())) + val resultsFASC = vaultService.queryBy(fungibleAssetStateCriteria) + assertThat(resultsFASC.states).hasSize(2) + // composite query for both linear and fungible asset states by participants + val resultsComposite = vaultService.queryBy(linearStateCriteria.or(fungibleAssetStateCriteria)) + assertThat(resultsComposite.states).hasSize(4) + } + } + @Test fun `unconsumed linear heads where external id is null`() { database.transaction { diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt index 8502739712..a358427b08 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/contracts/DummyState.kt @@ -6,8 +6,10 @@ import net.corda.core.identity.AbstractParty /** * Dummy state for use in testing. Not part of any contract, not even the [DummyContract]. */ -data class DummyState( +data class DummyState @JvmOverloads constructor ( /** Some information that the state represents for test purposes. **/ - val magicNumber: Int = 0) : ContractState { - override val participants: List get() = emptyList() + val magicNumber: Int = 0, + override val participants: List = listOf()) : ContractState { + + fun copy(magicNumber: Int = this.magicNumber) = DummyState(magicNumber) } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt index 6308223bbb..3017cc129f 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/vault/VaultFiller.kt @@ -17,6 +17,8 @@ import net.corda.finance.contracts.DealState import net.corda.finance.contracts.asset.Cash import net.corda.finance.contracts.asset.Obligation import net.corda.finance.contracts.asset.OnLedgerAsset +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyState import net.corda.testing.core.* import net.corda.testing.internal.chooseIdentity import net.corda.testing.internal.chooseIdentityAndCert @@ -96,6 +98,7 @@ class VaultFiller @JvmOverloads constructor( fun fillWithSomeTestLinearStates(numberToCreate: Int, externalId: String? = null, participants: List = emptyList(), + uniqueIdentifier: UniqueIdentifier? = null, linearString: String = "", linearNumber: Long = 0L, linearBoolean: Boolean = false, @@ -108,7 +111,7 @@ class VaultFiller @JvmOverloads constructor( // Issue a Linear state val dummyIssue = TransactionBuilder(notary = defaultNotary.party).apply { addOutputState(DummyLinearContract.State( - linearId = UniqueIdentifier(externalId), + linearId = uniqueIdentifier ?: UniqueIdentifier(externalId), participants = participants.plus(me), linearString = linearString, linearNumber = linearNumber, @@ -168,6 +171,23 @@ class VaultFiller @JvmOverloads constructor( } + /** + * Records a dummy state in the Vault (useful for creating random states when testing vault queries) + */ + fun fillWithDummyState() : Vault { + val outputState = TransactionState( + data = DummyState(Random().nextInt(), participants = listOf(services.myInfo.singleIdentity())), + contract = DummyContract.PROGRAM_ID, + notary = defaultNotary.party + ) + val builder = TransactionBuilder() + .addOutputState(outputState) + .addCommand(DummyCommandData, defaultNotary.party.owningKey) + val stxn = services.signInitialTransaction(builder) + services.recordTransactions(stxn) + return Vault(setOf(stxn.tx.outRef(0))) + } + /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */