Support for case insensitive vault queries (#3853)

* Make the criteria builder functions case insensitive

Add IGNORE_CASE versions of the comparison operator enums
Add exactMatch argument to criteria builder functions where strings can be passed in and set its default value to true
Use JvmOverrides to provide the default true version to java without needing to specify a value manually
If exactMatch is true then the original enums will be used, if false the IGNORE_CASE enums will be used instead
HibernateQueryCriteriaParser.columnPredicateToPredicate now takes into account the IGNORE_CASE versions of the enums

* Tidy up QueryCriteriaUtils and HibernateQueryCriteriaParser

Split HibernateQueryCriteriaParser.columnPredicateToPredicate into smaller functions
Reduce duplicated code in QueryCriteriaUtils

* Tidy up QueryCriteriaUtils and HibernateQueryCriteriaParser

Split HibernateQueryCriteriaParser.columnPredicateToPredicate into smaller functions
Reduce duplicated code in QueryCriteriaUtils (missed some code here)

* update changelog and api-vault-query docs with new API functions

* reorder Operator enums so that the ignore case enums are at the end

In case anyone is depending on the order of the enums, to keep compatibility with existing CorDapps the enums should be added at the end to prevent ordinals from breaking.
This commit is contained in:
Dan Newton
2018-09-07 17:20:21 +08:00
committed by josecoll
parent c6400cf344
commit db6c7f38a5
5 changed files with 361 additions and 80 deletions

View File

@ -7,6 +7,13 @@ import net.corda.core.internal.uncheckedCast
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.BinaryComparisonOperator.*
import net.corda.core.node.services.vault.CollectionOperator.*
import net.corda.core.node.services.vault.ColumnPredicate.*
import net.corda.core.node.services.vault.EqualityComparisonOperator.*
import net.corda.core.node.services.vault.LikenessOperator.*
import net.corda.core.node.services.vault.NullOperator.IS_NULL
import net.corda.core.node.services.vault.NullOperator.NOT_NULL
import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.PersistentStateRef
@ -54,54 +61,93 @@ abstract class AbstractQueryCriteriaParser<Q : GenericQueryCriteria<Q,P>, in P:
protected fun columnPredicateToPredicate(column: Path<out Any?>, columnPredicate: ColumnPredicate<*>): Predicate {
return when (columnPredicate) {
is ColumnPredicate.EqualityComparison -> {
val literal = columnPredicate.rightLiteral
when (columnPredicate.operator) {
EqualityComparisonOperator.EQUAL -> criteriaBuilder.equal(column, literal)
EqualityComparisonOperator.NOT_EQUAL -> criteriaBuilder.notEqual(column, literal)
}
}
is ColumnPredicate.BinaryComparison -> {
val literal: Comparable<Any?>? = uncheckedCast(columnPredicate.rightLiteral)
@Suppress("UNCHECKED_CAST")
column as Path<Comparable<Any?>?>
when (columnPredicate.operator) {
BinaryComparisonOperator.GREATER_THAN -> criteriaBuilder.greaterThan(column, literal)
BinaryComparisonOperator.GREATER_THAN_OR_EQUAL -> criteriaBuilder.greaterThanOrEqualTo(column, literal)
BinaryComparisonOperator.LESS_THAN -> criteriaBuilder.lessThan(column, literal)
BinaryComparisonOperator.LESS_THAN_OR_EQUAL -> criteriaBuilder.lessThanOrEqualTo(column, literal)
}
}
is ColumnPredicate.Likeness -> {
@Suppress("UNCHECKED_CAST")
column as Path<String?>
when (columnPredicate.operator) {
LikenessOperator.LIKE -> criteriaBuilder.like(column, columnPredicate.rightLiteral)
LikenessOperator.NOT_LIKE -> criteriaBuilder.notLike(column, columnPredicate.rightLiteral)
}
}
is ColumnPredicate.CollectionExpression -> {
when (columnPredicate.operator) {
CollectionOperator.IN -> column.`in`(columnPredicate.rightLiteral)
CollectionOperator.NOT_IN -> criteriaBuilder.not(column.`in`(columnPredicate.rightLiteral))
}
}
is ColumnPredicate.Between -> {
@Suppress("UNCHECKED_CAST")
column as Path<Comparable<Any?>?>
val fromLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightFromLiteral)
val toLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightToLiteral)
criteriaBuilder.between(column, fromLiteral, toLiteral)
}
is ColumnPredicate.NullExpression -> {
when (columnPredicate.operator) {
NullOperator.IS_NULL -> criteriaBuilder.isNull(column)
NullOperator.NOT_NULL -> criteriaBuilder.isNotNull(column)
}
}
is EqualityComparison -> equalityComparisonToPredicate(column, columnPredicate)
is BinaryComparison -> binaryComparisonToPredicate(column, columnPredicate)
is Likeness -> likeComparisonToPredicate(column, columnPredicate)
is CollectionExpression -> collectionComparisonToPredicate(column, columnPredicate)
is Between -> betweenComparisonToPredicate(column, columnPredicate)
is NullExpression -> nullComparisonToPredicate(column, columnPredicate)
else -> throw VaultQueryException("Not expecting $columnPredicate")
}
}
private fun equalityComparisonToPredicate(column: Path<out Any?>, columnPredicate: EqualityComparison<*>): Predicate {
val literal = columnPredicate.rightLiteral
return if (literal is String) {
@Suppress("UNCHECKED_CAST")
column as Path<String?>
when (columnPredicate.operator) {
EQUAL -> criteriaBuilder.equal(column, literal)
EQUAL_IGNORE_CASE -> criteriaBuilder.equal(criteriaBuilder.upper(column), literal.toUpperCase())
NOT_EQUAL -> criteriaBuilder.notEqual(column, literal)
NOT_EQUAL_IGNORE_CASE -> criteriaBuilder.notEqual(criteriaBuilder.upper(column), literal.toUpperCase())
}
} else {
when (columnPredicate.operator) {
EQUAL, EQUAL_IGNORE_CASE -> criteriaBuilder.equal(column, literal)
NOT_EQUAL, NOT_EQUAL_IGNORE_CASE -> criteriaBuilder.notEqual(column, literal)
}
}
}
private fun binaryComparisonToPredicate(column: Path<out Any?>, columnPredicate: BinaryComparison<*>): Predicate {
val literal: Comparable<Any?>? = uncheckedCast(columnPredicate.rightLiteral)
@Suppress("UNCHECKED_CAST")
column as Path<Comparable<Any?>?>
return when (columnPredicate.operator) {
GREATER_THAN -> criteriaBuilder.greaterThan(column, literal)
GREATER_THAN_OR_EQUAL -> criteriaBuilder.greaterThanOrEqualTo(column, literal)
LESS_THAN -> criteriaBuilder.lessThan(column, literal)
LESS_THAN_OR_EQUAL -> criteriaBuilder.lessThanOrEqualTo(column, literal)
}
}
private fun likeComparisonToPredicate(column: Path<out Any?>, columnPredicate: Likeness): Predicate {
@Suppress("UNCHECKED_CAST")
column as Path<String?>
return when (columnPredicate.operator) {
LIKE -> criteriaBuilder.like(column, columnPredicate.rightLiteral)
LIKE_IGNORE_CASE -> criteriaBuilder.like(criteriaBuilder.upper(column), columnPredicate.rightLiteral.toUpperCase())
NOT_LIKE -> criteriaBuilder.notLike(column, columnPredicate.rightLiteral)
NOT_LIKE_IGNORE_CASE -> criteriaBuilder.notLike(criteriaBuilder.upper(column), columnPredicate.rightLiteral.toUpperCase())
}
}
private fun collectionComparisonToPredicate(column: Path<out Any?>, columnPredicate: CollectionExpression<*>): Predicate {
val literal = columnPredicate.rightLiteral
return if (literal.any { it is String }) {
@Suppress("UNCHECKED_CAST")
column as Path<String?>
@Suppress("UNCHECKED_CAST")
literal as Collection<String>
when (columnPredicate.operator) {
IN -> column.`in`(literal)
IN_IGNORE_CASE -> criteriaBuilder.upper(column).`in`(literal.map { it.toUpperCase() })
NOT_IN -> criteriaBuilder.not(column.`in`(literal))
NOT_IN_IGNORE_CASE -> criteriaBuilder.not(criteriaBuilder.upper(column).`in`(literal.map { it.toUpperCase() }))
}
} else {
when (columnPredicate.operator) {
IN, IN_IGNORE_CASE -> column.`in`(literal)
NOT_IN, NOT_IN_IGNORE_CASE -> criteriaBuilder.not(column.`in`(literal))
}
}
}
private fun betweenComparisonToPredicate(column: Path<out Any?>, columnPredicate: Between<*>): Predicate {
@Suppress("UNCHECKED_CAST")
column as Path<Comparable<Any?>?>
val fromLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightFromLiteral)
val toLiteral: Comparable<Any?>? = uncheckedCast(columnPredicate.rightToLiteral)
return criteriaBuilder.between(column, fromLiteral, toLiteral)
}
private fun nullComparisonToPredicate(column: Path<out Any?>, columnPredicate: NullExpression<*>): Predicate {
return when (columnPredicate.operator) {
IS_NULL -> criteriaBuilder.isNull(column)
NOT_NULL -> criteriaBuilder.isNotNull(column)
}
}
}
class HibernateAttachmentQueryCriteriaParser(override val criteriaBuilder: CriteriaBuilder,
@ -474,7 +520,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
// state status
stateTypes = criteria.status
if (criteria.status != Vault.StateStatus.ALL) {
val predicateID = Pair(VaultSchemaV1.VaultStates::stateStatus.name, EqualityComparisonOperator.EQUAL)
val predicateID = Pair(VaultSchemaV1.VaultStates::stateStatus.name, EQUAL)
if (commonPredicates.containsKey(predicateID)) {
val existingStatus = ((commonPredicates[predicateID] as ComparisonPredicate).rightHandOperand as LiteralExpression).literal
if (existingStatus != criteria.status) {
@ -488,7 +534,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
// state relevance.
if (criteria.isRelevant != Vault.RelevancyStatus.ALL) {
val predicateID = Pair(VaultSchemaV1.VaultStates::isRelevant.name, EqualityComparisonOperator.EQUAL)
val predicateID = Pair(VaultSchemaV1.VaultStates::isRelevant.name, EQUAL)
if (commonPredicates.containsKey(predicateID)) {
val existingStatus = ((commonPredicates[predicateID] as ComparisonPredicate).rightHandOperand as LiteralExpression).literal
if (existingStatus != criteria.isRelevant) {
@ -503,7 +549,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
// contract state types
val contractStateTypes = deriveContractStateTypes(criteria.contractStateTypes)
if (contractStateTypes.isNotEmpty()) {
val predicateID = Pair(VaultSchemaV1.VaultStates::contractStateClassName.name, CollectionOperator.IN)
val predicateID = Pair(VaultSchemaV1.VaultStates::contractStateClassName.name, IN)
if (commonPredicates.containsKey(predicateID)) {
val existingTypes = (commonPredicates[predicateID]!!.expressions[0] as InPredicate<*>).values.map { (it as LiteralExpression).literal }.toSet()
if (existingTypes != contractStateTypes) {

View File

@ -800,6 +800,139 @@ abstract class VaultQueryTestsBase : VaultQueryParties {
}
}
@Test
fun `logical operator case insensitive EQUAL`() {
database.transaction {
listOf(USD, GBP, CHF).forEach {
vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER)
}
val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal("gBp", false) }
val criteria = VaultCustomQueryCriteria(logicalExpression)
val results = vaultService.queryBy<Cash.State>(criteria)
assertThat(results.states).hasSize(1)
}
}
@Test
fun `logical operator case insensitive EQUAL does not affect numbers`() {
database.transaction {
listOf(USD, GBP, CHF).forEach {
vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER)
}
val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.equal(10000, false) }
val criteria = VaultCustomQueryCriteria(logicalExpression)
val results = vaultService.queryBy<Cash.State>(criteria)
assertThat(results.states).hasSize(3)
}
}
@Test
fun `logical operator case insensitive NOT_EQUAL does not return results containing the same characters as the case insensitive string`() {
database.transaction {
listOf(USD, GBP, CHF).forEach {
vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER)
}
val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notEqual("gBp", false) }
val criteria = VaultCustomQueryCriteria(logicalExpression)
val results = vaultService.queryBy<Cash.State>(criteria)
assertThat(results.states).hasSize(2)
}
}
@Test
fun `logical operator case insensitive NOT_EQUAL does not affect numbers`() {
database.transaction {
listOf(USD, CHF).forEach {
vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER)
}
vaultFiller.fillWithSomeTestCash(AMOUNT(50, GBP), notaryServices, 1, DUMMY_CASH_ISSUER)
val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.notEqual(10000, false) }
val criteria = VaultCustomQueryCriteria(logicalExpression)
val results = vaultService.queryBy<Cash.State>(criteria)
assertThat(results.states).hasSize(1)
}
}
@Test
fun `logical operator case insensitive IN`() {
database.transaction {
listOf(USD, GBP, CHF).forEach {
vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER)
}
val currencies = listOf("cHf", "gBp")
val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.`in`(currencies, false) }
val criteria = VaultCustomQueryCriteria(logicalExpression)
val results = vaultService.queryBy<Cash.State>(criteria)
assertThat(results.states).hasSize(2)
}
}
@Test
fun `logical operator case insensitive IN does not affect numbers`() {
database.transaction {
vaultFiller.fillWithSomeTestCash(AMOUNT(100, USD), notaryServices, 1, DUMMY_CASH_ISSUER)
vaultFiller.fillWithSomeTestCash(AMOUNT(200, CHF), notaryServices, 1, DUMMY_CASH_ISSUER)
vaultFiller.fillWithSomeTestCash(AMOUNT(50, GBP), notaryServices, 1, DUMMY_CASH_ISSUER)
val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.`in`(listOf(10000L, 20000L), false) }
val criteria = VaultCustomQueryCriteria(logicalExpression)
val results = vaultService.queryBy<Cash.State>(criteria)
assertThat(results.states).hasSize(2)
}
}
@Test
fun `logical operator case insensitive NOT IN does not return results containing the same characters as the case insensitive strings`() {
database.transaction {
listOf(USD, GBP, CHF).forEach {
vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER)
}
val currencies = listOf("cHf", "gBp")
val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notIn(currencies, false) }
val criteria = VaultCustomQueryCriteria(logicalExpression)
val results = vaultService.queryBy<Cash.State>(criteria)
assertThat(results.states).hasSize(1)
}
}
@Test
fun `logical operator case insensitive NOT_IN does not affect numbers`() {
database.transaction {
vaultFiller.fillWithSomeTestCash(AMOUNT(100, USD), notaryServices, 1, DUMMY_CASH_ISSUER)
vaultFiller.fillWithSomeTestCash(AMOUNT(200, CHF), notaryServices, 1, DUMMY_CASH_ISSUER)
vaultFiller.fillWithSomeTestCash(AMOUNT(50, GBP), notaryServices, 1, DUMMY_CASH_ISSUER)
val logicalExpression = builder { CashSchemaV1.PersistentCashState::pennies.notIn(listOf(10000L, 20000L), false) }
val criteria = VaultCustomQueryCriteria(logicalExpression)
val results = vaultService.queryBy<Cash.State>(criteria)
assertThat(results.states).hasSize(1)
}
}
@Test
fun `logical operator case insensitive LIKE`() {
database.transaction {
listOf(USD, GBP, CHF).forEach {
vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER)
}
val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.like("%bP", false) } // GPB
val criteria = VaultCustomQueryCriteria(logicalExpression)
val results = vaultService.queryBy<Cash.State>(criteria)
assertThat(results.states).hasSize(1)
}
}
@Test
fun `logical operator NOT LIKE does not return results containing the same characters as the case insensitive string`() {
database.transaction {
listOf(USD, GBP, CHF).forEach {
vaultFiller.fillWithSomeTestCash(AMOUNT(100, it), notaryServices, 1, DUMMY_CASH_ISSUER)
}
val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.notLike("%bP", false) } // GPB
val criteria = VaultCustomQueryCriteria(logicalExpression)
val results = vaultService.queryBy<Cash.State>(criteria)
assertThat(results.states).hasSize(2)
}
}
@Test
fun `aggregate functions without group clause`() {
database.transaction {