ENT-1428 Vault queries - change SQL generation for aggregate function in 'group by' clause. (#426)

Vault queries: 'order by' function aggregation e.g. 'order by sum(col)' is replaced by the position of the selected column (e.g. 'order by 1').
Rationale: NonStop database doesn't support aggregation function in order by clause e.g. 'order by (sum col_x)'. I couldn't find a way to force Hibernate to produce alias e.g. ' select sum(col_x) as alias_x... order by also_x ...',   and the only solution supported by all db an HPE NonStop is to use positional parameter e.g. 'select sum(col_x) as alias_x... order by 1 ...'
This commit is contained in:
szymonsztuka 2018-04-30 09:45:15 +01:00 committed by GitHub
parent 8ce718f4bf
commit 416d4ecaeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 82 additions and 9 deletions

View File

@ -299,29 +299,36 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
} }
//TODO investigate possibility to avoid producing redundant joins in SQL for multiple aggregate functions against the same table //TODO investigate possibility to avoid producing redundant joins in SQL for multiple aggregate functions against the same table
aggregateExpressions.add(aggregateExpression) aggregateExpressions.add(aggregateExpression)
// optionally order by this aggregate function // Some databases may not support aggregate expression in 'group by' clause e.g. 'group by sum(col)',
expression.orderBy?.let { // Hibernate Criteria Builder can't produce alias 'group by col_alias', and the only solution is to use a positional parameter 'group by 1'
val orderCriteria = val orderByColumnPosition = aggregateExpressions.size
when (expression.orderBy!!) { var shiftLeft = 0
Sort.Direction.ASC -> criteriaBuilder.asc(aggregateExpression)
Sort.Direction.DESC -> criteriaBuilder.desc(aggregateExpression)
}
criteriaQuery.orderBy(orderCriteria)
}
// add optional group by clauses // add optional group by clauses
expression.groupByColumns?.let { columns -> expression.groupByColumns?.let { columns ->
val groupByExpressions = val groupByExpressions =
columns.map { _column -> columns.map { _column ->
val path = root.get<Any?>(getColumnName(_column)) val path = root.get<Any?>(getColumnName(_column))
val columnNumberBeforeRemoval = aggregateExpressions.size
if (path is SingularAttributePath) //remove the same columns from different joins to match the single column in 'group by' only (from the last join) if (path is SingularAttributePath) //remove the same columns from different joins to match the single column in 'group by' only (from the last join)
aggregateExpressions.removeAll { aggregateExpressions.removeAll {
elem -> if (elem is SingularAttributePath) elem.attribute.javaMember == path.attribute.javaMember else false elem -> if (elem is SingularAttributePath) elem.attribute.javaMember == path.attribute.javaMember else false
} }
shiftLeft += columnNumberBeforeRemoval - aggregateExpressions.size //record how many times a duplicated column was removed (from the previous 'parseAggregateFunction' run)
aggregateExpressions.add(path) aggregateExpressions.add(path)
path path
} }
criteriaQuery.groupBy(groupByExpressions) criteriaQuery.groupBy(groupByExpressions)
} }
// optionally order by this aggregate function
expression.orderBy?.let {
val orderCriteria =
when (expression.orderBy!!) {
// when adding column position of 'group by' shift in case columns were removed
Sort.Direction.ASC -> criteriaBuilder.asc(criteriaBuilder.literal<Int>(orderByColumnPosition - shiftLeft))
Sort.Direction.DESC -> criteriaBuilder.desc(criteriaBuilder.literal<Int>(orderByColumnPosition - shiftLeft))
}
criteriaQuery.orderBy(orderCriteria)
}
return aggregateExpression return aggregateExpression
} }
else -> throw VaultQueryException("Not expecting $columnPredicate") else -> throw VaultQueryException("Not expecting $columnPredicate")

View File

@ -767,6 +767,72 @@ open class VaultQueryTests {
} }
} }
@Test
fun `aggregate functions with single group clause desc first column`() {
database.transaction {
listOf(100.DOLLARS, 200.DOLLARS, 300.DOLLARS, 400.POUNDS, 500.SWISS_FRANCS).zip(1..5).forEach { (howMuch, states) ->
vaultFiller.fillWithSomeTestCash(howMuch, notaryServices, states, DUMMY_CASH_ISSUER)
}
val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency), orderBy = Sort.Direction.DESC) }
val max = builder { CashSchemaV1.PersistentCashState::pennies.max(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val min = builder { CashSchemaV1.PersistentCashState::pennies.min(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val results = vaultService.queryBy<FungibleAsset<*>>(VaultCustomQueryCriteria(sum)
.and(VaultCustomQueryCriteria(max))
.and(VaultCustomQueryCriteria(min)))
assertThat(results.otherResults).hasSize(12)
assertThat(results.otherResults.subList(0,4)).isEqualTo(listOf(60000L, 11298L, 8702L, "USD"))
assertThat(results.otherResults.subList(4,8)).isEqualTo(listOf(50000L, 10274L, 9481L, "CHF"))
assertThat(results.otherResults.subList(8,12)).isEqualTo(listOf(40000L, 10343L, 9351L, "GBP"))
}
}
@Test
fun `aggregate functions with single group clause desc mid column`() {
database.transaction {
listOf(100.DOLLARS, 200.DOLLARS, 300.DOLLARS, 400.POUNDS, 500.SWISS_FRANCS).zip(1..5).forEach { (howMuch, states) ->
vaultFiller.fillWithSomeTestCash(howMuch, notaryServices, states, DUMMY_CASH_ISSUER)
}
val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val max = builder { CashSchemaV1.PersistentCashState::pennies.max(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency), orderBy = Sort.Direction.DESC) }
val min = builder { CashSchemaV1.PersistentCashState::pennies.min(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val results = vaultService.queryBy<FungibleAsset<*>>(VaultCustomQueryCriteria(sum)
.and(VaultCustomQueryCriteria(max))
.and(VaultCustomQueryCriteria(min)))
assertThat(results.otherResults).hasSize(12)
assertThat(results.otherResults.subList(0,4)).isEqualTo(listOf(60000L, 11298L, 8702L, "USD"))
assertThat(results.otherResults.subList(4,8)).isEqualTo(listOf(40000L, 10343L, 9351L, "GBP"))
assertThat(results.otherResults.subList(8,12)).isEqualTo(listOf(50000L, 10274L, 9481L, "CHF"))
}
}
@Test
fun `aggregate functions with single group clause desc last column`() {
database.transaction {
listOf(100.DOLLARS, 200.DOLLARS, 300.DOLLARS, 400.POUNDS, 500.SWISS_FRANCS).zip(1..5).forEach { (howMuch, states) ->
vaultFiller.fillWithSomeTestCash(howMuch, notaryServices, states, DUMMY_CASH_ISSUER)
}
val sum = builder { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val max = builder { CashSchemaV1.PersistentCashState::pennies.max(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency)) }
val min = builder { CashSchemaV1.PersistentCashState::pennies.min(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency), orderBy = Sort.Direction.DESC) }
val results = vaultService.queryBy<FungibleAsset<*>>(VaultCustomQueryCriteria(sum)
.and(VaultCustomQueryCriteria(max))
.and(VaultCustomQueryCriteria(min)))
assertThat(results.otherResults).hasSize(12)
assertThat(results.otherResults.subList(0,4)).isEqualTo(listOf(50000L, 10274L, 9481L, "CHF"))
assertThat(results.otherResults.subList(4,8)).isEqualTo(listOf(40000L, 10343L, 9351L, "GBP"))
assertThat(results.otherResults.subList(8,12)).isEqualTo(listOf(60000L, 11298L, 8702L, "USD"))
}
}
@Test @Test
fun `aggregate functions sum by issuer and currency and sort by aggregate sum`() { fun `aggregate functions sum by issuer and currency and sort by aggregate sum`() {
database.transaction { database.transaction {