diff --git a/community/common/src/test/java/org/neo4j/test/rule/RandomRule.java b/community/common/src/test/java/org/neo4j/test/rule/RandomRule.java index 36638d1617008..d9a56cb6c64a5 100644 --- a/community/common/src/test/java/org/neo4j/test/rule/RandomRule.java +++ b/community/common/src/test/java/org/neo4j/test/rule/RandomRule.java @@ -44,6 +44,7 @@ public class RandomRule implements TestRule { private long seed; + private boolean hasGlobalSeed; private Random random; private Randoms randoms; private Configuration config = Randoms.DEFAULT; @@ -54,6 +55,13 @@ public RandomRule withConfiguration( Randoms.Configuration config ) return this; } + public RandomRule withSeedForAllTests( long seed ) + { + hasGlobalSeed = true; + this.seed = seed; + return this; + } + @Override public Statement apply( final Statement base, Description description ) { @@ -62,14 +70,17 @@ public Statement apply( final Statement base, Description description ) @Override public void evaluate() throws Throwable { - Seed methodSeed = description.getAnnotation( Seed.class ); - if ( methodSeed != null ) - { - seed = methodSeed.value(); - } - else + if ( !hasGlobalSeed ) { - seed = currentTimeMillis(); + Seed methodSeed = description.getAnnotation( Seed.class ); + if ( methodSeed != null ) + { + seed = methodSeed.value(); + } + else + { + seed = currentTimeMillis(); + } } reset(); try diff --git a/community/kernel/src/main/java/org/neo4j/kernel/api/index/IndexEntryUpdate.java b/community/kernel/src/main/java/org/neo4j/kernel/api/index/IndexEntryUpdate.java index 4c212fe1408cb..a5f2fdb091aa0 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/api/index/IndexEntryUpdate.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/api/index/IndexEntryUpdate.java @@ -129,8 +129,9 @@ public int hashCode() @Override public String toString() { - return format( "IndexEntryUpdate[id=%d, mode=%s, %s, values=%s]", entityId, updateMode, indexKey().schema() - .userDescription( SchemaUtil.idTokenNameLookup ), Arrays.toString(values) ); + return format( "IndexEntryUpdate[id=%d, mode=%s, %s, beforeValues=%s, values=%s]", entityId, updateMode, + indexKey().schema().userDescription( SchemaUtil.idTokenNameLookup ), + Arrays.toString( before ), Arrays.toString( values ) ); } public static IndexEntryUpdate add( diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NativeSchemaNumberIndexAccessor.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NativeSchemaNumberIndexAccessor.java index f8905bb80bbbd..37882d495ff03 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NativeSchemaNumberIndexAccessor.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NativeSchemaNumberIndexAccessor.java @@ -22,7 +22,6 @@ import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; - import org.neo4j.collection.primitive.PrimitiveLongIterator; import org.neo4j.cursor.RawCursor; import org.neo4j.graphdb.ResourceIterator; @@ -38,6 +37,8 @@ import org.neo4j.kernel.api.index.IndexUpdater; import org.neo4j.kernel.api.index.PropertyAccessor; import org.neo4j.kernel.api.schema.IndexQuery; +import org.neo4j.kernel.api.schema.IndexQuery.ExactPredicate; +import org.neo4j.kernel.api.schema.IndexQuery.NumberRangePredicate; import org.neo4j.kernel.impl.api.index.IndexUpdateMode; import org.neo4j.storageengine.api.schema.IndexReader; import org.neo4j.storageengine.api.schema.IndexSampler; @@ -123,11 +124,12 @@ private class NativeSchemaNumberIndexReader implements IndexReader { private final KEY treeKeyFrom = layout.newKey(); private final KEY treeKeyTo = layout.newKey(); + private RawCursor,IOException> openSeeker; @Override public void close() { - throw new UnsupportedOperationException( "Implement me" ); + ensureOpenSeekerClosed(); } @Override @@ -162,7 +164,66 @@ public IndexSampler createSampler() @Override public PrimitiveLongIterator query( IndexQuery... predicates ) throws IndexNotApplicableKernelException { - throw new UnsupportedOperationException( "Implement me" ); + if ( predicates.length != 1 ) + { + throw new UnsupportedOperationException(); + } + + ensureOpenSeekerClosed(); + IndexQuery predicate = predicates[0]; + switch ( predicate.type() ) + { + case exists: + treeKeyFrom.initAsLowest(); + treeKeyTo.initAsHighest(); + return startSeekForInitializedRange(); + case exact: + ExactPredicate exactPredicate = (ExactPredicate) predicate; + Object[] values = new Object[] {exactPredicate.value()}; + treeKeyFrom.from( Long.MIN_VALUE, values ); + treeKeyTo.from( Long.MAX_VALUE, values ); + return startSeekForInitializedRange(); + case rangeNumeric: + NumberRangePredicate rangePredicate = (NumberRangePredicate) predicate; + treeKeyFrom.from( rangePredicate.fromInclusive() ? Long.MIN_VALUE : Long.MAX_VALUE, + new Object[] {rangePredicate.from()} ); + treeKeyFrom.entityIdIsSpecialTieBreaker = true; + treeKeyTo.from( rangePredicate.toInclusive() ? Long.MAX_VALUE : Long.MIN_VALUE, + new Object[] {rangePredicate.to()} ); + treeKeyTo.entityIdIsSpecialTieBreaker = true; + return startSeekForInitializedRange(); + default: + throw new IllegalArgumentException( "IndexQuery of type " + predicate.type() + " is not supported." ); + } + } + + private PrimitiveLongIterator startSeekForInitializedRange() + { + try + { + openSeeker = tree.seek( treeKeyFrom, treeKeyTo ); + return new NumberHitIterator<>( openSeeker ); + } + catch ( IOException e ) + { + throw new UncheckedIOException( e ); + } + } + + private void ensureOpenSeekerClosed() + { + if ( openSeeker != null ) + { + try + { + openSeeker.close(); + } + catch ( IOException e ) + { + throw new UncheckedIOException( e ); + } + openSeeker = null; + } } } } diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NumberHitIterator.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NumberHitIterator.java new file mode 100644 index 0000000000000..642df38c415b5 --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NumberHitIterator.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.com] + * + * This file is part of Neo4j. + * + * Neo4j is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.neo4j.kernel.impl.index.schema; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import org.neo4j.collection.primitive.PrimitiveLongCollections; +import org.neo4j.collection.primitive.PrimitiveLongIterator; +import org.neo4j.cursor.RawCursor; +import org.neo4j.index.internal.gbptree.Hit; + +/** + * Wraps number key/value results in a {@link PrimitiveLongIterator}. + * The {@link RawCursor seeker} which gets passed in will have to be closed somewhere else because + * the {@link PrimitiveLongIterator} is just a plain iterator, no resource. + * + * @param type of {@link NumberKey}. + * @param type of {@link NumberValue}. + */ +public class NumberHitIterator + extends PrimitiveLongCollections.PrimitiveLongBaseIterator +{ + private final RawCursor,IOException> seeker; + + public NumberHitIterator( RawCursor,IOException> seeker ) + { + this.seeker = seeker; + } + + @Override + protected boolean fetchNext() + { + try + { + return seeker.next() ? next( seeker.get().key().entityId ) : false; + } + catch ( IOException e ) + { + throw new UncheckedIOException( e ); + } + } +} diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NumberKey.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NumberKey.java index 8f701dc91dc9b..867cf322a2eec 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NumberKey.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NumberKey.java @@ -40,20 +40,19 @@ class NumberKey long entityId; /** - * Explicitly defines this key as higher than any other if {@code true}. - * The {@link GBPTree} only supports exclusive end of range for range scans which means highest possible - * double value can not be found in the tree because it will always fall outside of range. - * But if {@code isHighest} is used as end of range, it will be effectively unbound and thus include all values. + * Marks that comparisons with this key requires also comparing entityId, this allows functionality + * of inclusive/exclusive bounds of range queries. + * This is because {@link GBPTree} only support from inclusive and to exclusive. *

- * Note that {@code isHighest} is only an in memory state. + * Note that {@code entityIdIsSpecialTieBreaker} is only an in memory state. */ - boolean isHighest; + boolean entityIdIsSpecialTieBreaker; public void from( long entityId, Value[] values ) { this.value = assertValidSingleNumber( values ).doubleValue(); this.entityId = entityId; - this.isHighest = false; + entityIdIsSpecialTieBreaker = false; } String propertiesAsString() @@ -65,14 +64,14 @@ void initAsLowest() { value = Double.NEGATIVE_INFINITY; entityId = Long.MIN_VALUE; - isHighest = false; + entityIdIsSpecialTieBreaker = true; } void initAsHighest() { value = Double.POSITIVE_INFINITY; entityId = Long.MAX_VALUE; - isHighest = true; + entityIdIsSpecialTieBreaker = true; } @Override diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NumberLayout.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NumberLayout.java index 929debaa8e0a0..0965ac571a6d8 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NumberLayout.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/NumberLayout.java @@ -39,7 +39,7 @@ public NumberKey copyKey( NumberKey key, { into.value = key.value; into.entityId = key.entityId; - into.isHighest = key.isHighest; + into.entityIdIsSpecialTieBreaker = key.entityIdIsSpecialTieBreaker; return into; } diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/UniqueNumberLayout.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/UniqueNumberLayout.java index 8331e599c6cfc..a08ff4edd2a0e 100644 --- a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/UniqueNumberLayout.java +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/UniqueNumberLayout.java @@ -41,15 +41,10 @@ public int compare( NumberKey o1, NumberKey o2 ) int comparison = Double.compare( o1.value, o2.value ); if ( comparison == 0 ) { - // This is a special case where we need to check if o2 is the unbounded max key. - // This needs to be checked to be able to support storing Double.MAX_VALUE and retrieve it later. - if ( o2.isHighest ) + // This is a special case where we need also compare entityId support inclusive/exclusive + if ( o1.entityIdIsSpecialTieBreaker || o2.entityIdIsSpecialTieBreaker ) { - return -1; - } - if ( o1.isHighest ) - { - return 1; + return Long.compare( o1.entityId, o2.entityId ); } } return comparison; diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/LayoutTestUtil.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/LayoutTestUtil.java index e70201533fcf2..1d2546e96015a 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/LayoutTestUtil.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/LayoutTestUtil.java @@ -49,7 +49,7 @@ abstract class LayoutTestUtil abstract Layout createLayout(); - abstract IndexEntryUpdate[] someUpdates(); + abstract IndexEntryUpdate[] someUpdates(); protected abstract double fractionDuplicates(); @@ -90,7 +90,7 @@ int compareIndexedPropertyValue( NumberValue value1, NumberValue value2 ) return typeCompare; } - Iterator> randomUniqueUpdateGenerator( RandomRule random ) + Iterator> randomUpdateGenerator( RandomRule random ) { double fractionDuplicates = fractionDuplicates(); return new PrefetchingIterator>() @@ -137,21 +137,20 @@ private Number existingNonUniqueValue( RandomRule randomRule ) }; } - @SuppressWarnings( "rawtypes" ) - IndexEntryUpdate[] someUpdatesNoDuplicateValues() + IndexEntryUpdate[] someUpdatesNoDuplicateValues() { return generateAddUpdatesFor( ALL_EXTREME_VALUES ); } - @SuppressWarnings( "rawtypes" ) - IndexEntryUpdate[] someUpdatesWithDuplicateValues() + IndexEntryUpdate[] someUpdatesWithDuplicateValues() { return generateAddUpdatesFor( ArrayUtils.addAll( ALL_EXTREME_VALUES, ALL_EXTREME_VALUES ) ); } - private IndexEntryUpdate[] generateAddUpdatesFor( Number[] values ) + private IndexEntryUpdate[] generateAddUpdatesFor( Number[] values ) { - IndexEntryUpdate[] indexEntryUpdates = new IndexEntryUpdate[values.length]; + @SuppressWarnings( "unchecked" ) + IndexEntryUpdate[] indexEntryUpdates = new IndexEntryUpdate[values.length]; for ( int i = 0; i < indexEntryUpdates.length; i++ ) { indexEntryUpdates[i] = add( i, values[i] ); diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexPopulatorTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexPopulatorTest.java index 7fb06cbf9e8aa..834baddb2119a 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexPopulatorTest.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexPopulatorTest.java @@ -370,7 +370,7 @@ public void shouldApplyLargeAmountOfInterleavedRandomUpdates() throws Exception populator.create(); random.reset(); Random updaterRandom = new Random( random.seed() ); - Iterator> updates = layoutUtil.randomUniqueUpdateGenerator( random ); + Iterator> updates = layoutUtil.randomUpdateGenerator( random ); // when int count = interleaveLargeAmountOfUpdates( updaterRandom, updates ); @@ -378,7 +378,7 @@ public void shouldApplyLargeAmountOfInterleavedRandomUpdates() throws Exception // then populator.close( true ); random.reset(); - verifyUpdates( layoutUtil.randomUniqueUpdateGenerator( random ), count ); + verifyUpdates( layoutUtil.randomUpdateGenerator( random ), count ); } @Test diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaNumberIndexAccessorTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaNumberIndexAccessorTest.java index 9363fb181af06..f40ad425116e9 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaNumberIndexAccessorTest.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaNumberIndexAccessorTest.java @@ -28,17 +28,27 @@ import java.util.HashSet; import java.util.Iterator; import java.util.Set; +import java.util.function.Predicate; import org.neo4j.collection.primitive.Primitive; +import org.neo4j.collection.primitive.PrimitiveLongCollections; +import org.neo4j.collection.primitive.PrimitiveLongIterator; import org.neo4j.kernel.api.exceptions.index.IndexEntryConflictException; import org.neo4j.kernel.api.index.IndexEntryUpdate; import org.neo4j.kernel.api.index.IndexUpdater; +import org.neo4j.kernel.api.schema.IndexQuery; import org.neo4j.kernel.api.schema.index.IndexDescriptor; import org.neo4j.kernel.api.schema.index.IndexDescriptorFactory; import org.neo4j.storageengine.api.schema.IndexReader; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; + +import static org.neo4j.collection.primitive.PrimitiveLongCollections.EMPTY_LONG_ARRAY; +import static org.neo4j.function.Predicates.all; +import static org.neo4j.function.Predicates.alwaysTrue; +import static org.neo4j.function.Predicates.in; import static org.neo4j.index.internal.gbptree.RecoveryCleanupWorkCollector.IMMEDIATE; import static org.neo4j.kernel.api.index.IndexEntryUpdate.change; import static org.neo4j.kernel.api.index.IndexEntryUpdate.remove; @@ -127,7 +137,6 @@ public void removeMustThrowAfterClose() throws Exception public void shouldIndexAdd() throws Exception { // given - @SuppressWarnings( "unchecked" ) IndexEntryUpdate[] updates = layoutUtil.someUpdates(); try ( IndexUpdater updater = accessor.newUpdater( ONLINE ) ) { @@ -144,7 +153,6 @@ public void shouldIndexAdd() throws Exception public void shouldIndexChange() throws Exception { // given - @SuppressWarnings( "unchecked" ) IndexEntryUpdate[] updates = layoutUtil.someUpdates(); processAll( updates ); @@ -181,7 +189,6 @@ public void shouldIndexChange() throws Exception public void shouldIndexRemove() throws Exception { // given - @SuppressWarnings( "unchecked" ) IndexEntryUpdate[] updates = layoutUtil.someUpdates(); processAll( updates ); @@ -205,7 +212,7 @@ public void shouldHandleRandomUpdates() throws Exception { // given Set> expectedData = new HashSet<>(); - Iterator> newDataGenerator = layoutUtil.randomUniqueUpdateGenerator( random ); + Iterator> newDataGenerator = layoutUtil.randomUpdateGenerator( random ); // when int rounds = 50; @@ -231,50 +238,53 @@ public void shouldHandleRandomUpdates() throws Exception public void shouldReturnZeroCountForEmptyIndex() throws Exception { // given - IndexReader reader = accessor.newReader(); - - // when - long count = reader.countIndexedNodes( 123, 456 ); + try ( IndexReader reader = accessor.newReader() ) + { + // when + long count = reader.countIndexedNodes( 123, 456 ); - // then - assertEquals( 0, count ); + // then + assertEquals( 0, count ); + } } @Test public void shouldReturnCountOneForExistingData() throws Exception { // given - IndexEntryUpdate[] updates = layoutUtil.someUpdates(); + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); processAll( updates ); // when - IndexReader reader = accessor.newReader(); - for ( IndexEntryUpdate update : updates ) + try ( IndexReader reader = accessor.newReader() ) { - long count = reader.countIndexedNodes( update.getEntityId(), update.values() ); + for ( IndexEntryUpdate update : updates ) + { + long count = reader.countIndexedNodes( update.getEntityId(), update.values() ); - // then - assertEquals( 1, count ); - } + // then + assertEquals( 1, count ); + } - // and when - long count = reader.countIndexedNodes( 123, 456 ); + // and when + long count = reader.countIndexedNodes( 123, 456 ); - // then - assertEquals( 0, count ); + // then + assertEquals( 0, count ); + } } @Test public void shouldReturnCountZeroForMismatchingData() throws Exception { // given - IndexEntryUpdate[] updates = layoutUtil.someUpdates(); + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); processAll( updates ); // when IndexReader reader = accessor.newReader(); - for ( IndexEntryUpdate update : updates ) + for ( IndexEntryUpdate update : updates ) { long countWithMismatchingData = reader.countIndexedNodes( update.getEntityId() + 1, update.values() ); long countWithNonExistentEntityId = reader.countIndexedNodes( NON_EXISTENT_ENTITY_ID, update.values() ); @@ -287,6 +297,168 @@ public void shouldReturnCountZeroForMismatchingData() throws Exception } } + @Test + public void shouldReturnAllEntriesForExistsPredicate() throws Exception + { + // given + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); + processAll( updates ); + + // when + IndexReader reader = accessor.newReader(); + PrimitiveLongIterator result = reader.query( IndexQuery.exists( 0 ) ); + + // then + assertEntityIdHits( extractEntityIds( updates, alwaysTrue() ), result ); + } + + @Test + public void shouldReturnNoEntriesForExistsPredicateForEmptyIndex() throws Exception + { + // when + IndexReader reader = accessor.newReader(); + PrimitiveLongIterator result = reader.query( IndexQuery.exists( 0 ) ); + + // then + long[] actual = PrimitiveLongCollections.asArray( result ); + assertEquals( 0, actual.length ); + } + + @Test + public void shouldReturnMatchingEntriesForExactPredicate() throws Exception + { + // given + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); + processAll( updates ); + + // when + IndexReader reader = accessor.newReader(); + for ( IndexEntryUpdate update : updates ) + { + Object value = update.values()[0]; + PrimitiveLongIterator result = reader.query( IndexQuery.exact( 0, value ) ); + assertEntityIdHits( extractEntityIds( updates, in( value ) ), result ); + } + } + + @Test + public void shouldReturnNoEntriesForMismatchingExactPredicate() throws Exception + { + // given + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); + processAll( updates ); + + // when + IndexReader reader = accessor.newReader(); + Object value = NON_EXISTENT_VALUE; + PrimitiveLongIterator result = reader.query( IndexQuery.exact( 0, value ) ); + assertEntityIdHits( EMPTY_LONG_ARRAY, result ); + } + + @Test + public void shouldReturnMatchingEntriesForRangePredicateWithInclusiveStartAndExclusiveEnd() throws Exception + { + // given + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); + processAll( updates ); + + // when + IndexReader reader = accessor.newReader(); + PrimitiveLongIterator result = reader.query( + IndexQuery.range( 0, Double.NEGATIVE_INFINITY, true, Double.POSITIVE_INFINITY, false ) ); + assertEntityIdHits( extractEntityIds( updates, lessThan( Double.POSITIVE_INFINITY ) ), result ); + } + + @Test + public void shouldReturnMatchingEntriesForRangePredicateWithInclusiveStartAndInclusiveEnd() throws Exception + { + // given + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); + processAll( updates ); + + // when + IndexReader reader = accessor.newReader(); + PrimitiveLongIterator result = reader.query( + IndexQuery.range( 0, Double.NEGATIVE_INFINITY, true, Double.POSITIVE_INFINITY, true ) ); + assertEntityIdHits( extractEntityIds( updates, alwaysTrue() ), result ); + } + + @Test + public void shouldReturnMatchingEntriesForRangePredicateWithExclusiveStartAndExclusiveEnd() throws Exception + { + // given + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); + processAll( updates ); + + // when + IndexReader reader = accessor.newReader(); + PrimitiveLongIterator result = reader.query( + IndexQuery.range( 0, Double.NEGATIVE_INFINITY, false, Double.POSITIVE_INFINITY, false ) ); + assertEntityIdHits( extractEntityIds( updates, + all( greaterThan( Double.NEGATIVE_INFINITY ), lessThan( Double.POSITIVE_INFINITY ) ) ), result ); + } + + @Test + public void shouldReturnMatchingEntriesForRangePredicateWithExclusiveStartAndInclusiveEnd() throws Exception + { + // given + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); + processAll( updates ); + + // when + IndexReader reader = accessor.newReader(); + PrimitiveLongIterator result = reader.query( + IndexQuery.range( 0, Double.NEGATIVE_INFINITY, false, Double.POSITIVE_INFINITY, true ) ); + assertEntityIdHits( extractEntityIds( updates, + all( greaterThan( Double.NEGATIVE_INFINITY ), greaterThan( Double.NEGATIVE_INFINITY ) ) ), result ); + } + + @Test + public void shouldReturnNoEntriesForRangePredicateOutsideAnyMatch() throws Exception + { + // given + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); + processAll( updates ); + + // when + IndexReader reader = accessor.newReader(); + PrimitiveLongIterator result = reader.query( + IndexQuery.range( 0, NON_EXISTENT_VALUE, true, NON_EXISTENT_VALUE + 10, true ) ); + assertEntityIdHits( EMPTY_LONG_ARRAY, result ); + } + + private static Predicate lessThan( Double value ) + { + return t -> ((Number)t).doubleValue() < value; + } + + private static Predicate greaterThan( Double value ) + { + return t -> ((Number)t).doubleValue() > value; + } + + private void assertEntityIdHits( long[] expected, PrimitiveLongIterator result ) + { + long[] actual = PrimitiveLongCollections.asArray( result ); + Arrays.sort( actual ); + Arrays.sort( expected ); + assertArrayEquals( expected, actual ); + } + + private long[] extractEntityIds( IndexEntryUpdate[] updates, Predicate valueFilter ) + { + long[] entityIds = new long[updates.length]; + int cursor = 0; + for ( int i = 0; i < updates.length; i++ ) + { + if ( valueFilter.test( updates[i].values()[0] ) ) + { + entityIds[cursor++] = updates[i].getEntityId(); + } + } + return Arrays.copyOf( entityIds, cursor ); + } + private void applyUpdatesToExpectedData( Set> expectedData, IndexEntryUpdate[] batch ) { @@ -310,18 +482,19 @@ private void applyUpdatesToExpectedData( Set> throw new IllegalArgumentException( update.updateMode().name() ); } - if ( addition != null ) - { - expectedData.add( addition ); - } if ( removal != null ) { expectedData.remove( removal ); } + if ( addition != null ) + { + expectedData.add( addition ); + } } } - private IndexEntryUpdate[] generateRandomUpdates( Set> expectedData, + private IndexEntryUpdate[] generateRandomUpdates( + Set> expectedData, Iterator> newDataGenerator, int count, float removeFactor ) { @SuppressWarnings( "unchecked" ) @@ -396,21 +569,25 @@ private IndexEntryUpdate simpleUpdate() // shouldReturnCountOneForExistingData // shouldReturnCountZeroForMismatchingData - // TODO: shouldReturnAllEntriesForExistsPredicate - // TODO: shouldReturnNoEntriesForExistsPredicateForEmptyIndex - // TODO: shouldReturnMatchingEntriesForExactPredicate - // TODO: shouldReturnNoEntriesForMismatchingExactPredicate - // TODO: shouldReturnMatchingEntriesForRangePredicateWithInclusiveStartAndExclusiveEnd - // TODO: shouldReturnMatchingEntriesForRangePredicateWithInclusiveStartAndInclusiveEnd - // TODO: shouldReturnMatchingEntriesForRangePredicateWithExclusiveStartAndExclusiveEnd - // TODO: shouldReturnMatchingEntriesForRangePredicateWithExclusiveStartAndInclusiveEnd - // TODO: shouldReturnNoEntriesForRangePredicateOutsideAnyMatch + // shouldReturnAllEntriesForExistsPredicate + // shouldReturnNoEntriesForExistsPredicateForEmptyIndex + // shouldReturnMatchingEntriesForExactPredicate + // shouldReturnNoEntriesForMismatchingExactPredicate + // shouldReturnMatchingEntriesForRangePredicateWithInclusiveStartAndExclusiveEnd + // shouldReturnMatchingEntriesForRangePredicateWithInclusiveStartAndInclusiveEnd + // shouldReturnMatchingEntriesForRangePredicateWithExclusiveStartAndExclusiveEnd + // shouldReturnMatchingEntriesForRangePredicateWithExclusiveStartAndInclusiveEnd + // shouldReturnNoEntriesForRangePredicateOutsideAnyMatch + // TODO: multiple query predicates... actually Lucene SimpleIndexReader only supports single predicate + // so perhaps we should wait with this until we know exactly how this works and which combinations + // that should be optimized. // TODO: SAMPLER // long countIndexedNodes( long nodeId, Object... propertyValues ) // IndexSampler createSampler() // PrimitiveLongIterator query( IndexQuery... predicates ) +// close // ACCESSOR // todo shouldHandleMultipleConsecutiveUpdaters diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NonUniqueLayoutTestUtil.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NonUniqueLayoutTestUtil.java index fba00f80efdbd..1fe0906782309 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NonUniqueLayoutTestUtil.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NonUniqueLayoutTestUtil.java @@ -21,6 +21,7 @@ import org.neo4j.index.internal.gbptree.Layout; import org.neo4j.kernel.api.index.IndexEntryUpdate; +import org.neo4j.kernel.api.schema.index.IndexDescriptor; import org.neo4j.kernel.api.schema.index.IndexDescriptorFactory; class NonUniqueLayoutTestUtil extends LayoutTestUtil @@ -37,7 +38,7 @@ Layout createLayout() } @Override - IndexEntryUpdate[] someUpdates() + IndexEntryUpdate[] someUpdates() { return someUpdatesWithDuplicateValues(); }