Vault Query Sort by StateRef (or constituents: txId, index) (#990)

* Provide sorting by state reference (and individual constituents of: txId, index)

* Fixed formatting.

* Updated import following rebase from master.

* Updated import following rebase from master.
This commit is contained in:
josecoll 2017-07-10 12:49:00 +01:00 committed by GitHub
parent f718acb939
commit c81ef7eb93
7 changed files with 200 additions and 29 deletions

View File

@ -150,7 +150,13 @@ data class Sort(val columns: Collection<SortColumn>) {
@CordaSerializable
interface Attribute
enum class VaultStateAttribute(val columnName: String) : Attribute {
enum class CommonStateAttribute(val attributeParent: String, val attributeChild: String?) : Attribute {
STATE_REF("stateRef", null),
STATE_REF_TXN_ID("stateRef", "txId"),
STATE_REF_INDEX("stateRef", "index")
}
enum class VaultStateAttribute(val attributeName: String) : Attribute {
/** Vault States */
NOTARY_NAME("notaryName"),
CONTRACT_TYPE("contractStateClassName"),
@ -160,14 +166,14 @@ data class Sort(val columns: Collection<SortColumn>) {
LOCK_ID("lockId")
}
enum class LinearStateAttribute(val columnName: String) : Attribute {
enum class LinearStateAttribute(val attributeName: String) : Attribute {
/** Vault Linear States */
UUID("uuid"),
EXTERNAL_ID("externalId"),
DEAL_REFERENCE("dealReference")
}
enum class FungibleStateAttribute(val columnName: String) : Attribute {
enum class FungibleStateAttribute(val attributeName: String) : Attribute {
/** Vault Fungible States */
QUANTITY("quantity"),
ISSUER_REF("issuerRef")

View File

@ -28,6 +28,9 @@ UNRELEASED
* ``random63BitValue()`` has moved to ``CryptoUtils``
* Added additional common Sort attributes (see ``Sort.CommandStateAttribute``) for use in Vault Query criteria
to include STATE_REF, STATE_REF_TXN_ID, STATE_REF_INDEX
Milestone 13
------------

View File

@ -334,7 +334,7 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
val leftPredicates = parse(left)
val rightPredicates = parse(right)
val andPredicate = criteriaBuilder.and(criteriaBuilder.and(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray()))
val andPredicate = criteriaBuilder.and(*leftPredicates.toTypedArray(), *rightPredicates.toTypedArray())
predicateSet.add(andPredicate)
return predicateSet
@ -378,11 +378,11 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
var orderCriteria = mutableListOf<Order>()
sorting.columns.map { (sortAttribute, direction) ->
val (entityStateClass, entityStateColumnName) =
when(sortAttribute) {
is SortAttribute.Standard -> parse(sortAttribute.attribute)
is SortAttribute.Custom -> Pair(sortAttribute.entityStateClass, sortAttribute.entityStateColumnName)
}
val (entityStateClass, entityStateAttributeParent, entityStateAttributeChild) =
when(sortAttribute) {
is SortAttribute.Standard -> parse(sortAttribute.attribute)
is SortAttribute.Custom -> Triple(sortAttribute.entityStateClass, sortAttribute.entityStateColumnName, null)
}
val sortEntityRoot =
rootEntities.getOrElse(entityStateClass) {
// scenario where sorting on attributes not parsed as criteria
@ -394,10 +394,16 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
}
when (direction) {
Sort.Direction.ASC -> {
orderCriteria.add(criteriaBuilder.asc(sortEntityRoot.get<String>(entityStateColumnName)))
if (entityStateAttributeChild != null)
orderCriteria.add(criteriaBuilder.asc(sortEntityRoot.get<String>(entityStateAttributeParent).get<String>(entityStateAttributeChild)))
else
orderCriteria.add(criteriaBuilder.asc(sortEntityRoot.get<String>(entityStateAttributeParent)))
}
Sort.Direction.DESC ->
orderCriteria.add(criteriaBuilder.desc(sortEntityRoot.get<String>(entityStateColumnName)))
if (entityStateAttributeChild != null)
orderCriteria.add(criteriaBuilder.desc(sortEntityRoot.get<String>(entityStateAttributeParent).get<String>(entityStateAttributeChild)))
else
orderCriteria.add(criteriaBuilder.desc(sortEntityRoot.get<String>(entityStateAttributeParent)))
}
}
if (orderCriteria.isNotEmpty()) {
@ -406,20 +412,23 @@ class HibernateQueryCriteriaParser(val contractType: Class<out ContractState>,
}
}
private fun parse(sortAttribute: Sort.Attribute): Pair<Class<out PersistentState>, String> {
val entityClassAndColumnName : Pair<Class<out PersistentState>, String> =
when(sortAttribute) {
is Sort.VaultStateAttribute -> {
Pair(VaultSchemaV1.VaultStates::class.java, sortAttribute.columnName)
private fun parse(sortAttribute: Sort.Attribute): Triple<Class<out PersistentState>, String, String?> {
val entityClassAndColumnName : Triple<Class<out PersistentState>, String, String?> =
when(sortAttribute) {
is Sort.CommonStateAttribute -> {
Triple(VaultSchemaV1.VaultStates::class.java, sortAttribute.attributeParent, sortAttribute.attributeChild)
}
is Sort.VaultStateAttribute -> {
Triple(VaultSchemaV1.VaultStates::class.java, sortAttribute.attributeName, null)
}
is Sort.LinearStateAttribute -> {
Triple(VaultSchemaV1.VaultLinearStates::class.java, sortAttribute.attributeName, null)
}
is Sort.FungibleStateAttribute -> {
Triple(VaultSchemaV1.VaultFungibleStates::class.java, sortAttribute.attributeName, null)
}
else -> throw VaultQueryException("Invalid sort attribute: $sortAttribute")
}
is Sort.LinearStateAttribute -> {
Pair(VaultSchemaV1.VaultLinearStates::class.java, sortAttribute.columnName)
}
is Sort.FungibleStateAttribute -> {
Pair(VaultSchemaV1.VaultFungibleStates::class.java, sortAttribute.columnName)
}
else -> throw VaultQueryException("Invalid sort attribute: $sortAttribute")
}
return entityClassAndColumnName
}
}

View File

@ -55,6 +55,7 @@ import static net.corda.node.utilities.DatabaseSupportKt.transaction;
import static net.corda.testing.CoreTestUtils.getMEGA_CORP;
import static net.corda.testing.CoreTestUtils.getMEGA_CORP_KEY;
import static net.corda.testing.node.MockServicesKt.makeTestDataSourceProperties;
import static net.corda.core.utilities.ByteArrays.toHexString;
import static org.assertj.core.api.Assertions.assertThat;
public class VaultQueryJavaTests {
@ -134,6 +135,31 @@ public class VaultQueryJavaTests {
});
}
@Test
public void unconsumedStatesForStateRefsSortedByTxnId() {
transaction(database, tx -> {
VaultFiller.fillWithSomeTestLinearStates(services, 8);
Vault<LinearState> issuedStates = VaultFiller.fillWithSomeTestLinearStates(services, 2);
Stream<StateRef> stateRefsStream = StreamSupport.stream(issuedStates.getStates().spliterator(), false).map(StateAndRef::getRef);
List<StateRef> stateRefs = stateRefsStream.collect(Collectors.toList());
SortAttribute.Standard sortAttribute = new SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID);
Sort sorting = new Sort(Arrays.asList(new Sort.SortColumn(sortAttribute, Sort.Direction.ASC)));
VaultQueryCriteria criteria = new VaultQueryCriteria(Vault.StateStatus.UNCONSUMED, null, stateRefs);
Vault.Page<DummyLinearContract.State> results = vaultQuerySvc.queryBy(DummyLinearContract.State.class, criteria, sorting);
assertThat(results.getStates()).hasSize(2);
stateRefs.sort(Comparator.comparing(stateRef -> toHexString(stateRef.getTxhash().getBytes())));
assertThat(results.getStates().get(0).getRef()).isEqualTo(stateRefs.get(0));
assertThat(results.getStates().get(1).getRef()).isEqualTo(stateRefs.get(1));
return tx;
});
}
@Test
public void consumedCashStates() {
transaction(database, tx -> {

View File

@ -202,6 +202,67 @@ class HibernateConfigurationTest {
queryResultsAsc.map { println(it.recordedTime) }
}
@Test
fun `with sorting by state ref desc and asc`() {
// generate additional state ref indexes
database.transaction {
services.consumeCash(1.DOLLARS)
services.consumeCash(2.DOLLARS)
services.consumeCash(3.DOLLARS)
services.consumeCash(4.DOLLARS)
services.consumeCash(5.DOLLARS)
}
// structure query
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
val sortByStateRef = vaultStates.get<PersistentStateRef>("stateRef")
// order by DESC
criteriaQuery.orderBy(criteriaBuilder.desc(sortByStateRef))
val queryResults = entityManager.createQuery(criteriaQuery).resultList
println("DESC by stateRef")
queryResults.map { println(it.stateRef) }
// order by ASC
criteriaQuery.orderBy(criteriaBuilder.asc(sortByStateRef))
val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList
println("ASC by stateRef")
queryResultsAsc.map { println(it.stateRef) }
}
@Test
fun `with sorting by state ref index and txId desc and asc`() {
// generate additional state ref indexes
database.transaction {
services.consumeCash(1.DOLLARS)
services.consumeCash(2.DOLLARS)
services.consumeCash(3.DOLLARS)
services.consumeCash(4.DOLLARS)
services.consumeCash(5.DOLLARS)
}
// structure query
val criteriaQuery = criteriaBuilder.createQuery(VaultSchemaV1.VaultStates::class.java)
val vaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java)
val sortByIndex = vaultStates.get<PersistentStateRef>("stateRef").get<String>("index")
val sortByTxId = vaultStates.get<PersistentStateRef>("stateRef").get<String>("txId")
// order by DESC
criteriaQuery.orderBy(criteriaBuilder.desc(sortByIndex), criteriaBuilder.desc(sortByTxId))
val queryResults = entityManager.createQuery(criteriaQuery).resultList
println("DESC by index txId")
queryResults.map { println(it.stateRef) }
// order by ASC
criteriaQuery.orderBy(criteriaBuilder.asc(sortByIndex), criteriaBuilder.asc(sortByTxId))
val queryResultsAsc = entityManager.createQuery(criteriaQuery).resultList
println("ASC by index txId")
queryResultsAsc.map { println(it.stateRef) }
}
@Test
fun `with pagination`() {
// add 100 additional cash entries

View File

@ -18,6 +18,7 @@ import net.corda.core.schemas.testing.DummyLinearStateSchemaV1
import net.corda.core.seconds
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.toHexString
import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.vault.schemas.jpa.VaultSchemaV1
@ -220,21 +221,80 @@ class VaultQueryTests {
}
@Test
fun `unconsumed states for state refs`() {
fun `unconsumed cash states sorted by state ref`() {
database.transaction {
var stateRefs : MutableList<StateRef> = mutableListOf()
val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
val issuedStateRefs = issuedStates.states.map { it.ref }.toList()
stateRefs.addAll(issuedStateRefs)
val spentStates = services.consumeCash(25.DOLLARS)
var spentStateRefs = spentStates.states.map { it.ref }.toList()
stateRefs.addAll(spentStateRefs)
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
val criteria = VaultQueryCriteria()
val results = vaultQuerySvc.queryBy<Cash.State>(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))))
// default StateRef sort is by index then txnId:
// order by
// vaultschem1_.output_index,
// vaultschem1_.transaction_id asc
assertThat(results.states).hasSize(8) // -3 CONSUMED + 1 NEW UNCONSUMED (change)
val sortedStateRefs = stateRefs.sortedBy { it.index }
assertThat(results.states.first().ref.index).isEqualTo(sortedStateRefs.first().index) // 0
assertThat(results.states.last().ref.index).isEqualTo(sortedStateRefs.last().index) // 1
}
}
@Test
fun `unconsumed cash states sorted by state ref txnId and index`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
services.consumeCash(10.DOLLARS)
services.consumeCash(10.DOLLARS)
val sortAttributeTxnId = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID)
val sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX)
val sortBy = Sort(setOf(Sort.SortColumn(sortAttributeTxnId, Sort.Direction.ASC),
Sort.SortColumn(sortAttributeIndex, Sort.Direction.ASC)))
val criteria = VaultQueryCriteria()
val results = vaultQuerySvc.queryBy<Cash.State>(criteria, sortBy)
results.statesMetadata.forEach {
println(" ${it.ref}")
}
// explicit sort order asc by txnId and then index:
// order by
// vaultschem1_.transaction_id asc,
// vaultschem1_.output_index asc
assertThat(results.states).hasSize(9) // -2 CONSUMED + 1 NEW UNCONSUMED (change)
}
}
@Test
fun `unconsumed states for state refs`() {
database.transaction {
services.fillWithSomeTestLinearStates(8)
val issuedStates = services.fillWithSomeTestLinearStates(2)
val stateRefs = issuedStates.states.map { it.ref }.toList()
// DOCSTART VaultQueryExample2
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID)
val criteria = VaultQueryCriteria(stateRefs = listOf(stateRefs.first(), stateRefs.last()))
val results = vaultQuerySvc.queryBy<DummyLinearContract.State>(criteria)
val results = vaultQuerySvc.queryBy<DummyLinearContract.State>(criteria, Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))))
// DOCEND VaultQueryExample2
assertThat(results.states).hasSize(2)
assertThat(results.states.first().ref).isEqualTo(issuedStates.states.first().ref)
assertThat(results.states.last().ref).isEqualTo(issuedStates.states.last().ref)
val sortedStateRefs = stateRefs.sortedBy { it.txhash.bytes.toHexString() }
assertThat(results.states.first().ref).isEqualTo(sortedStateRefs.first())
assertThat(results.states.last().ref).isEqualTo(sortedStateRefs.last())
}
}

View File

@ -223,7 +223,7 @@ fun ServiceHub.evolveLinearStates(linearStates: List<StateAndRef<LinearState>>)
fun ServiceHub.evolveLinearState(linearState: StateAndRef<LinearState>) : StateAndRef<LinearState> = consumeAndProduce(linearState)
@JvmOverloads
fun ServiceHub.consumeCash(amount: Amount<Currency>, to: Party = CHARLIE) {
fun ServiceHub.consumeCash(amount: Amount<Currency>, to: Party = CHARLIE): Vault<Cash.State> {
// A tx that spends our money.
val spendTX = TransactionType.General.Builder(DUMMY_NOTARY).apply {
vaultService.generateSpend(this, amount, to)
@ -231,4 +231,10 @@ fun ServiceHub.consumeCash(amount: Amount<Currency>, to: Party = CHARLIE) {
}.toSignedTransaction(checkSufficientSignatures = false)
recordTransactions(spendTX)
// Get all the StateRefs of all the generated transactions.
val states = spendTX.tx.outputs.indices.map { i -> spendTX.tx.outRef<Cash.State>(i) }
return Vault(states)
}