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 4e4aa8fd12..ede0d9ba4b 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 @@ -302,7 +302,13 @@ sealed class QueryCriteria : GenericQueryCriteria>? = null, relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL - ) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes, relevancyStatus) + ) : this(participants, + linearId?.map { it.id }.takeIf { it != null && it.isNotEmpty() }, + linearId?.mapNotNull { it.externalId }.takeIf { it != null && it.isNotEmpty() }, + status, + contractStateTypes, + relevancyStatus + ) // V3 c'tor @DeprecatedConstructorForDeserialization(version = 1) 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 b2b3532346..a144727a23 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 @@ -153,6 +153,18 @@ abstract class AbstractQueryCriteriaParser, in P: NOT_NULL -> criteriaBuilder.isNotNull(column) } } + + /** + * Returns the given predicate if the provided `args` list is not empty + * If the list is empty it returns an always false predicate (1=0) + */ + protected fun checkIfListIsEmpty(args: List, criteriaBuilder: CriteriaBuilder, predicate: Predicate): Predicate { + return if (args.isEmpty()) { + criteriaBuilder.and(criteriaBuilder.equal(criteriaBuilder.literal(1), 0)) + } else { + predicate + } + } } class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: CriteriaBuilder, @@ -215,7 +227,14 @@ class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: (criteria.contractClassNamesCondition as EqualityComparison>).rightLiteral else emptyList() val joinDBAttachmentToContractClassNames = root.joinList("contractClassNames") - predicateSet.add(criteriaBuilder.and(joinDBAttachmentToContractClassNames.`in`(contractClassNames))) + + predicateSet.add( + checkIfListIsEmpty( + args = contractClassNames, + criteriaBuilder = criteriaBuilder, + predicate = criteriaBuilder.and(joinDBAttachmentToContractClassNames.`in`(contractClassNames)) + ) + ) } criteria.signersCondition?.let { @@ -224,7 +243,14 @@ class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: (criteria.signersCondition as EqualityComparison>).rightLiteral else emptyList() val joinDBAttachmentToSigners = root.joinList("signers") - predicateSet.add(criteriaBuilder.and(joinDBAttachmentToSigners.`in`(signers))) + + predicateSet.add( + checkIfListIsEmpty( + args = signers, + criteriaBuilder = criteriaBuilder, + predicate = criteriaBuilder.and(joinDBAttachmentToSigners.`in`(signers)) + ) + ) } criteria.isSignedCondition?.let { isSigned -> @@ -290,14 +316,27 @@ class HibernateQueryCriteriaParser(val contractStateType: Class("notary").`in`(criteria.notary))) + predicateSet.add( + checkIfListIsEmpty( + args = criteria.notary!!, + criteriaBuilder = criteriaBuilder, + predicate = criteriaBuilder.and(vaultStates.get("notary").`in`(criteria.notary)) + ) + ) } // state references criteria.stateRefs?.let { val persistentStateRefs = (criteria.stateRefs as List).map(::PersistentStateRef) val compositeKey = vaultStates.get("stateRef") - predicateSet.add(criteriaBuilder.and(compositeKey.`in`(persistentStateRefs))) + + predicateSet.add( + checkIfListIsEmpty( + args = persistentStateRefs, + criteriaBuilder = criteriaBuilder, + predicate = criteriaBuilder.and(compositeKey.`in`(persistentStateRefs)) + ) + ) } // time constraints (recorded, consumed) @@ -447,7 +486,14 @@ class HibernateQueryCriteriaParser(val contractStateType: Class - predicateSet.add(criteriaBuilder.and(vaultFungibleStatesRoot.get("owner").`in`(owners))) + + predicateSet.add( + checkIfListIsEmpty( + args = owners, + criteriaBuilder = criteriaBuilder, + predicate = criteriaBuilder.and(vaultFungibleStatesRoot.get("owner").`in`(owners)) + ) + ) } // quantity @@ -458,13 +504,27 @@ class HibernateQueryCriteriaParser(val contractStateType: Class - predicateSet.add(criteriaBuilder.and(vaultFungibleStatesRoot.get("issuer").`in`(issuerParties))) + + predicateSet.add( + checkIfListIsEmpty( + args = issuerParties, + criteriaBuilder = criteriaBuilder, + predicate = criteriaBuilder.and(vaultFungibleStatesRoot.get("issuer").`in`(issuerParties)) + ) + ) } // issuer reference criteria.issuerRef?.let { val issuerRefs = (criteria.issuerRef as List).map { it.bytes } - predicateSet.add(criteriaBuilder.and(vaultFungibleStatesRoot.get("issuerRef").`in`(issuerRefs))) + + predicateSet.add( + checkIfListIsEmpty( + args = issuerRefs, + criteriaBuilder = criteriaBuilder, + predicate = criteriaBuilder.and(vaultFungibleStatesRoot.get("issuerRef").`in`(issuerRefs)) + ) + ) } if (criteria.participants != null && criteria.exactParticipants != null) @@ -498,14 +558,27 @@ class HibernateQueryCriteriaParser(val contractStateType: Class - predicateSet.add(criteriaBuilder.and(vaultLinearStatesRoot.get("uuid").`in`(uuids))) + + predicateSet.add( + checkIfListIsEmpty( + args = uuids, + criteriaBuilder = criteriaBuilder, + predicate = criteriaBuilder.and(vaultLinearStatesRoot.get("uuid").`in`(uuids)) + ) + ) } // linear ids externalId criteria.externalId?.let { val externalIds = criteria.externalId as List - if (externalIds.isNotEmpty()) - predicateSet.add(criteriaBuilder.and(vaultLinearStatesRoot.get("externalId").`in`(externalIds))) + + predicateSet.add( + checkIfListIsEmpty( + args = externalIds, + criteriaBuilder = criteriaBuilder, + predicate = criteriaBuilder.and(vaultLinearStatesRoot.get("externalId").`in`(externalIds)) + ) + ) } if (criteria.participants != null && criteria.exactParticipants != null) @@ -694,8 +767,11 @@ class HibernateQueryCriteriaParser(val contractStateType: Class("x500Name").`in`(participants)) + commonPredicates[predicateID] = checkIfListIsEmpty( + args = participants, + criteriaBuilder = criteriaBuilder, + predicate = criteriaBuilder.and(getPersistentPartyRoot().get("x500Name").`in`(participants)) + ) } // Add the join for vault states to persistent entities (if this is not a Fungible nor Linear criteria query) diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index e7af37bb6f..801c917c35 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -258,6 +258,24 @@ class NodeAttachmentServiceTest { ) } + @Test(timeout=300_000) + fun `AttachmentsQueryCriteria returns empty resultset without errors if there is an empty list after the 'in' clause`() { + SelfCleaningDir().use { file -> + val contractJar = makeTestContractJar(file.path, "com.example.MyContract") + contractJar.read { storage.importAttachment(it, "uploaderB", "contract.jar") } + + assertEquals( + 1, + storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(listOf("com.example.MyContract")))).size + ) + + assertEquals( + 0, + storage.queryAttachments(AttachmentsQueryCriteria(contractClassNamesCondition = Builder.equal(emptyList()))).size + ) + } + } + @Test(timeout=300_000) fun `contract class, versioning and signing metadata can be used to search`() { SelfCleaningDir().use { file -> 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 ff362766b4..407b09be53 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 @@ -267,6 +267,31 @@ abstract class VaultQueryTestsBase : VaultQueryParties { } } + @Test(timeout=300_000) + fun `VaultQueryCriteria returns empty resultset without errors if there is an empty list after the 'in' clause`() { + database.transaction { + val states = vaultFiller.fillWithSomeTestLinearStates(1, "TEST") + val stateRefs = states.states.map { it.ref } + + val criteria = VaultQueryCriteria(notary = listOf(DUMMY_NOTARY)) + val results = vaultService.queryBy(criteria) + assertThat(results.states).hasSize(1) + + val emptyCriteria = VaultQueryCriteria(notary = emptyList()) + val emptyResults = vaultService.queryBy(emptyCriteria) + assertThat(emptyResults.states).hasSize(0) + + val stateCriteria = VaultQueryCriteria(stateRefs = stateRefs) + val stateResults = vaultService.queryBy(stateCriteria) + assertThat(stateResults.states).hasSize(1) + + val emptyStateCriteria = VaultQueryCriteria(stateRefs = emptyList()) + val emptyStateResults = vaultService.queryBy(emptyStateCriteria) + assertThat(emptyStateResults.states).hasSize(0) + + } + } + /** Generic Query tests (combining both FungibleState and LinearState contract types) */ @@ -1823,6 +1848,33 @@ abstract class VaultQueryTestsBase : VaultQueryParties { /** LinearState tests */ + @Test(timeout=300_000) + fun `LinearStateQueryCriteria returns empty resultset without errors if there is an empty list after the 'in' clause`() { + database.transaction { + val uid = UniqueIdentifier("999") + vaultFiller.fillWithSomeTestLinearStates(numberToCreate = 1, uniqueIdentifier = uid) + vaultFiller.fillWithSomeTestLinearStates(numberToCreate = 1, externalId = "1234") + + val uuidCriteria = LinearStateQueryCriteria(uuid = listOf(uid.id)) + val externalIdCriteria = LinearStateQueryCriteria(externalId = listOf("1234")) + + val uuidResults = vaultService.queryBy(uuidCriteria) + val externalIdResults = vaultService.queryBy(externalIdCriteria) + + assertThat(uuidResults.states).hasSize(1) + assertThat(externalIdResults.states).hasSize(1) + + val uuidCriteriaEmpty = LinearStateQueryCriteria(uuid = emptyList()) + val externalIdCriteriaEmpty = LinearStateQueryCriteria(externalId = emptyList()) + + val uuidResultsEmpty = vaultService.queryBy(uuidCriteriaEmpty) + val externalIdResultsEmpty = vaultService.queryBy(externalIdCriteriaEmpty) + + assertThat(uuidResultsEmpty.states).hasSize(0) + assertThat(externalIdResultsEmpty.states).hasSize(0) + } + } + @Test(timeout=300_000) fun `unconsumed linear heads for linearId without external Id`() { database.transaction { @@ -2030,6 +2082,46 @@ abstract class VaultQueryTestsBase : VaultQueryParties { /** FungibleAsset tests */ + @Test(timeout=300_000) + fun `FungibleAssetQueryCriteria returns empty resultset without errors if there is an empty list after the 'in' clause`() { + database.transaction { + vaultFiller.fillWithSomeTestCash(100.DOLLARS, notaryServices, 1, MEGA_CORP.ref(0)) + + val ownerCriteria = FungibleAssetQueryCriteria(owner = listOf(MEGA_CORP)) + val ownerResults = vaultService.queryBy>(ownerCriteria) + + assertThat(ownerResults.states).hasSize(1) + + val emptyOwnerCriteria = FungibleAssetQueryCriteria(owner = emptyList()) + val emptyOwnerResults = vaultService.queryBy>(emptyOwnerCriteria) + + assertThat(emptyOwnerResults.states).hasSize(0) + + // Issuer field checks + val issuerCriteria = FungibleAssetQueryCriteria(issuer = listOf(MEGA_CORP)) + val issuerResults = vaultService.queryBy>(issuerCriteria) + + assertThat(issuerResults.states).hasSize(1) + + val emptyIssuerCriteria = FungibleAssetQueryCriteria(issuer = emptyList()) + val emptyIssuerResults = vaultService.queryBy>(emptyIssuerCriteria) + + assertThat(emptyIssuerResults.states).hasSize(0) + + // Issuer Ref field checks + val issuerRefCriteria = FungibleAssetQueryCriteria(issuerRef = listOf(MINI_CORP.ref(0).reference)) + val issuerRefResults = vaultService.queryBy>(issuerRefCriteria) + + assertThat(issuerRefResults.states).hasSize(1) + + val emptyIssuerRefCriteria = FungibleAssetQueryCriteria(issuerRef = emptyList()) + val emptyIssuerRefResults = vaultService.queryBy>(emptyIssuerRefCriteria) + + assertThat(emptyIssuerRefResults.states).hasSize(0) + + } + } + @Test(timeout=300_000) fun `unconsumed fungible assets for specific issuer party and refs`() { database.transaction {