diff --git a/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/CompositeIndexAccessorCompatibility.java b/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/CompositeIndexAccessorCompatibility.java index ee16985464272..ff77dda94d5ff 100644 --- a/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/CompositeIndexAccessorCompatibility.java +++ b/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/CompositeIndexAccessorCompatibility.java @@ -30,10 +30,10 @@ import java.time.LocalTime; import java.time.OffsetTime; import java.time.ZoneId; -import java.util.ArrayList; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -632,7 +632,7 @@ public void testIndexSeekRangeWithExistsBySpatialArray() throws Exception public void testExactMatchOnRandomCompositeValues() throws Exception { // given - List types = testSuite.supportedValueTypes(); + RandomValues.Type[] types = randomSetOfSupportedTypes(); List> updates = new ArrayList<>(); Set duplicateChecker = new HashSet<>(); for ( long id = 0; id < 10_000; id++ ) @@ -641,8 +641,8 @@ public void testExactMatchOnRandomCompositeValues() throws Exception do { update = add( id, descriptor.schema(), - random.nextValue( random.among( types ) ), - random.nextValue( random.among( types ) ) ); + random.randomValues().nextValueOfTypes( types ), + random.randomValues().nextValueOfTypes( types ) ); } while ( !duplicateChecker.add( ValueTuple.of( update.values() ) ) ); updates.add( update ); diff --git a/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/CompositeRandomizedIndexAccessorCompatibility.java b/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/CompositeRandomizedIndexAccessorCompatibility.java index 7005d2147e89c..26b89e8e0cecf 100644 --- a/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/CompositeRandomizedIndexAccessorCompatibility.java +++ b/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/CompositeRandomizedIndexAccessorCompatibility.java @@ -19,19 +19,25 @@ */ package org.neo4j.kernel.api.index; +import org.junit.Assume; import org.junit.Ignore; import org.junit.Test; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; +import org.neo4j.internal.kernel.api.IndexQuery; import org.neo4j.internal.kernel.api.schema.SchemaDescriptor; import org.neo4j.kernel.api.schema.index.TestIndexDescriptorFactory; +import org.neo4j.storageengine.api.schema.IndexDescriptor; import org.neo4j.values.storable.RandomValues; +import org.neo4j.values.storable.Value; import org.neo4j.values.storable.ValueTuple; +import org.neo4j.values.storable.Values; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; @@ -47,48 +53,147 @@ " errors or warnings in some IDEs about test classes needing a public zero-arg constructor." ) public class CompositeRandomizedIndexAccessorCompatibility extends IndexAccessorCompatibility { - public CompositeRandomizedIndexAccessorCompatibility( IndexProviderCompatibilityTestSuite testSuite ) + public CompositeRandomizedIndexAccessorCompatibility( IndexProviderCompatibilityTestSuite testSuite, IndexDescriptor descriptor ) { - // composite index of 4 properties - super( testSuite, TestIndexDescriptorFactory.forLabel( 1000, 100, 101, 102, 103 ) ); + super( testSuite, descriptor ); } - @Test - public void testExactMatchOnRandomCompositeValues() throws Exception + @Ignore( "Not a test. This is a compatibility suite" ) + public static class Exact extends CompositeRandomizedIndexAccessorCompatibility { - // given - List types = testSuite.supportedValueTypes(); - Collections.shuffle( types, random.random() ); - types = types.subList( 0, random.nextInt( 2, types.size() ) ); - List> updates = new ArrayList<>(); - Set duplicateChecker = new HashSet<>(); - for ( long id = 0; id < 30_000; id++ ) + public Exact( IndexProviderCompatibilityTestSuite testSuite ) { - IndexEntryUpdate update; - do + // composite index of 4 properties + super( testSuite, TestIndexDescriptorFactory.forLabel( 1000, 100, 101, 102, 103 ) ); + } + + @Test + public void testExactMatchOnRandomCompositeValues() throws Exception + { + // given + RandomValues.Type[] types = randomSetOfSupportedTypes(); + List> updates = new ArrayList<>(); + Set duplicateChecker = new HashSet<>(); + for ( long id = 0; id < 30_000; id++ ) + { + IndexEntryUpdate update; + do + { + update = add( id, descriptor.schema(), + random.randomValues().nextValueOfTypes( types ), + random.randomValues().nextValueOfTypes( types ), + random.randomValues().nextValueOfTypes( types ), + random.randomValues().nextValueOfTypes( types ) ); + } + while ( !duplicateChecker.add( ValueTuple.of( update.values() ) ) ); + updates.add( update ); + } + updateAndCommit( updates ); + + // when + for ( IndexEntryUpdate update : updates ) + { + // then + List hits = query( + exact( 100, update.values()[0] ), + exact( 101, update.values()[1] ), + exact( 102, update.values()[2] ), + exact( 103, update.values()[3] ) ); + assertEquals( update + " " + hits.toString(), 1, hits.size() ); + assertThat( single( hits ), equalTo( update.getEntityId() ) ); + } + } + } + + @Ignore( "Not a test. This is a compatibility suite" ) + public static class Range extends CompositeRandomizedIndexAccessorCompatibility + { + public Range( IndexProviderCompatibilityTestSuite testSuite ) + { + // composite index of 2 properties + super( testSuite, TestIndexDescriptorFactory.forLabel( 1000, 100, 101 ) ); + } + + /** + * All entries in composite index look like (booleanValue, randomValue ). + * Range queries in composite only work if all predicates before it is exact. + * We use boolean values for exact part so that we get some real ranges to work + * on in second composite slot where the random values are. + */ + @Test + public void testRangeMatchOnRandomValues() throws Exception + { + Assume.assumeTrue( "Assume support for granular composite queries", testSuite.supportsGranularCompositeQueries() ); + // given + RandomValues.Type[] types = randomSetOfSupportedAndSortableTypes(); + List values = generateValuesFromType( types ); + List> updates = generateUpdatesFromValues( values ); + updateAndCommit( updates ); + TreeSet sortedValues = new TreeSet<>( ( u1, u2 ) -> ValueTuple.COMPARATOR.compare( + ValueTuple.of( u1.values()[0], u1.values()[1] ), + ValueTuple.of( u2.values()[0], u2.values()[1] ) ) ); + sortedValues.addAll( updates ); + + for ( int i = 0; i < 100; i++ ) { - update = add( id, descriptor.schema(), - random.nextValue( random.among( types ) ), - random.nextValue( random.among( types ) ), - random.nextValue( random.among( types ) ), - random.nextValue( random.among( types ) ) ); + Value booleanValue = random.randomValues().nextBooleanValue(); + RandomValues.Type type = random.among( types ); + Value from = random.randomValues().nextValueOfType( type ); + Value to = random.randomValues().nextValueOfType( type ); + if ( Values.COMPARATOR.compare( from, to ) > 0 ) + { + Value tmp = from; + from = to; + to = tmp; + } + boolean fromInclusive = random.nextBoolean(); + boolean toInclusive = random.nextBoolean(); + + // when + List expectedIds = sortedValues.subSet( + add( 0, descriptor.schema(), booleanValue, from ), fromInclusive, + add( 0, descriptor.schema(), booleanValue, to ), toInclusive ) + .stream() + .map( IndexEntryUpdate::getEntityId ) + .collect( Collectors.toList() ); + List actualIds = query( IndexQuery.exact( 100, booleanValue ), IndexQuery.range( 101, from, fromInclusive, to, toInclusive ) ); + expectedIds.sort( Long::compare ); + actualIds.sort( Long::compare ); + + // then + assertThat( actualIds, equalTo( expectedIds ) ); } - while ( !duplicateChecker.add( ValueTuple.of( update.values() ) ) ); - updates.add( update ); } - updateAndCommit( updates ); - // when - for ( IndexEntryUpdate update : updates ) + private List generateValuesFromType( RandomValues.Type[] types ) { - // then - List hits = query( - exact( 100, update.values()[0] ), - exact( 101, update.values()[1] ), - exact( 102, update.values()[2] ), - exact( 103, update.values()[3] ) ); - assertEquals( update + " " + hits.toString(), 1, hits.size() ); - assertThat( single( hits ), equalTo( update.getEntityId() ) ); + List values = new ArrayList<>(); + Set duplicateChecker = new HashSet<>(); + for ( long i = 0; i < 30_000; i++ ) + { + ValueTuple value; + do + { + value = ValueTuple.of( + // Use boolean for first slot in composite because we will use exact match on this part.x + random.randomValues().nextBooleanValue(), + random.randomValues().nextValueOfTypes( types ) ); + } + while ( !duplicateChecker.add( value ) ); + values.add( value ); + } + return values; + } + + private List> generateUpdatesFromValues( List values ) + { + List> updates = new ArrayList<>(); + int id = 0; + for ( ValueTuple value : values ) + { + updates.add( add( id++, descriptor.schema(), (Object[]) value.getValues() ) ); + } + return updates; } } } diff --git a/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/IndexAccessorCompatibility.java b/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/IndexAccessorCompatibility.java index 01e7a1fb2bdf1..d68d2c767a0c8 100644 --- a/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/IndexAccessorCompatibility.java +++ b/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/IndexAccessorCompatibility.java @@ -23,8 +23,8 @@ import org.junit.Assert; import org.junit.Before; -import java.util.Collection; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; @@ -40,6 +40,7 @@ import org.neo4j.storageengine.api.schema.IndexDescriptor; import org.neo4j.storageengine.api.schema.IndexReader; import org.neo4j.storageengine.api.schema.SimpleNodeValueClient; +import org.neo4j.values.storable.RandomValues; import org.neo4j.values.storable.Value; import org.neo4j.values.storable.ValueCategory; import org.neo4j.values.storable.ValueGroup; @@ -84,6 +85,27 @@ public void after() } } + RandomValues.Type[] randomSetOfSupportedTypes() + { + RandomValues.Type[] supportedTypes = testSuite.supportedValueTypes(); + return random.randomValues().selection( supportedTypes, 2, supportedTypes.length, false ); + } + + RandomValues.Type[] randomSetOfSupportedAndSortableTypes() + { + RandomValues.Type[] types = randomSetOfSupportedTypes(); + types = removeSpatialTypes( types ); // <- don't use spatial values + types = RandomValues.excluding( types, RandomValues.Type.STRING, RandomValues.Type.STRING_ARRAY ); // <- don't use strings outside of BMP + return types; + } + + private RandomValues.Type[] removeSpatialTypes( RandomValues.Type[] types ) + { + return Arrays.stream( types ) + .filter( t -> !t.name().contains( "POINT" ) ) + .toArray( RandomValues.Type[]::new ); + } + protected List query( IndexQuery... predicates ) throws Exception { try ( IndexReader reader = accessor.newReader(); ) diff --git a/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/IndexProviderCompatibilityTestSuite.java b/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/IndexProviderCompatibilityTestSuite.java index fd70ee40f9073..8e96e80791936 100644 --- a/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/IndexProviderCompatibilityTestSuite.java +++ b/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/IndexProviderCompatibilityTestSuite.java @@ -69,7 +69,8 @@ CompositeIndexAccessorCompatibility.Unique.class, UniqueConstraintCompatibility.class, SimpleRandomizedIndexAccessorCompatibility.class, - CompositeRandomizedIndexAccessorCompatibility.class + CompositeRandomizedIndexAccessorCompatibility.Exact.class, + CompositeRandomizedIndexAccessorCompatibility.Range.class } ) public abstract class IndexProviderCompatibilityTestSuite { @@ -98,17 +99,21 @@ public boolean supportFullValuePrecisionForNumbers() return true; } - public List supportedValueTypes() + public RandomValues.Type[] supportedValueTypes() { - List types = new ArrayList<>( Arrays.asList( RandomValues.Type.values() ) ); if ( !supportsSpatial() ) { - types.remove( RandomValues.Type.CARTESIAN_POINT ); - types.remove( RandomValues.Type.CARTESIAN_POINT_3D ); - types.remove( RandomValues.Type.GEOGRAPHIC_POINT ); - types.remove( RandomValues.Type.GEOGRAPHIC_POINT_3D ); + return RandomValues.excluding( + RandomValues.Type.CARTESIAN_POINT, + RandomValues.Type.CARTESIAN_POINT_ARRAY, + RandomValues.Type.CARTESIAN_POINT_3D, + RandomValues.Type.CARTESIAN_POINT_3D_ARRAY, + RandomValues.Type.GEOGRAPHIC_POINT, + RandomValues.Type.GEOGRAPHIC_POINT_ARRAY, + RandomValues.Type.GEOGRAPHIC_POINT_3D, + RandomValues.Type.GEOGRAPHIC_POINT_3D_ARRAY ); } - return types; + return RandomValues.Type.values(); } public void consistencyCheck( IndexAccessor accessor ) diff --git a/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/SimpleRandomizedIndexAccessorCompatibility.java b/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/SimpleRandomizedIndexAccessorCompatibility.java index fa161317fd9b9..68fe38fd962ce 100644 --- a/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/SimpleRandomizedIndexAccessorCompatibility.java +++ b/community/community-it/kernel-it/src/test/java/org/neo4j/kernel/api/index/SimpleRandomizedIndexAccessorCompatibility.java @@ -19,20 +19,22 @@ */ package org.neo4j.kernel.api.index; +import org.junit.Assume; import org.junit.Ignore; import org.junit.Test; import java.util.ArrayList; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; import org.neo4j.internal.kernel.api.IndexQuery; -import org.neo4j.internal.kernel.api.schema.SchemaDescriptor; import org.neo4j.kernel.api.schema.index.TestIndexDescriptorFactory; import org.neo4j.values.storable.RandomValues; import org.neo4j.values.storable.Value; +import org.neo4j.values.storable.Values; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; @@ -56,22 +58,9 @@ public SimpleRandomizedIndexAccessorCompatibility( IndexProviderCompatibilityTes public void testExactMatchOnRandomValues() throws Exception { // given - List types = testSuite.supportedValueTypes(); - Collections.shuffle( types, random.random() ); - types = types.subList( 0, random.nextInt( 2, types.size() ) ); - - List> updates = new ArrayList<>(); - Set duplicateChecker = new HashSet<>(); - for ( long id = 0; id < 30_000; id++ ) - { - IndexEntryUpdate update; - do - { - update = add( id, descriptor.schema(), random.nextValue( random.among( types ) ) ); - } - while ( !duplicateChecker.add( update.values()[0] ) ); - updates.add( update ); - } + RandomValues.Type[] types = randomSetOfSupportedTypes(); + List values = generateValuesFromType( types ); + List> updates = generateUpdatesFromValues( values ); updateAndCommit( updates ); // when @@ -83,4 +72,72 @@ public void testExactMatchOnRandomValues() throws Exception assertThat( single( hits ), equalTo( update.getEntityId() ) ); } } + + @Test + public void testRangeMatchOnRandomValues() throws Exception + { + Assume.assumeTrue( "Assume support for granular composite queries", testSuite.supportsGranularCompositeQueries() ); + // given + RandomValues.Type[] types = randomSetOfSupportedAndSortableTypes(); + List values = generateValuesFromType( types ); + List> updates = generateUpdatesFromValues( values ); + updateAndCommit( updates ); + TreeSet sortedValues = new TreeSet<>( ( u1, u2 ) -> Values.COMPARATOR.compare( u1.values()[0], u2.values()[0] ) ); + sortedValues.addAll( updates ); + + for ( int i = 0; i < 100; i++ ) + { + RandomValues.Type type = random.among( types ); + Value from = random.randomValues().nextValueOfType( type ); + Value to = random.randomValues().nextValueOfType( type ); + if ( Values.COMPARATOR.compare( from, to ) > 0 ) + { + Value tmp = from; + from = to; + to = tmp; + } + boolean fromInclusive = random.nextBoolean(); + boolean toInclusive = random.nextBoolean(); + + // when + List expectedIds = sortedValues.subSet( add( 0, descriptor.schema(), from ), fromInclusive, add( 0, descriptor.schema(), to ), toInclusive ) + .stream() + .map( IndexEntryUpdate::getEntityId ) + .collect( Collectors.toList() ); + List actualIds = query( IndexQuery.range( 0, from, fromInclusive, to, toInclusive ) ); + expectedIds.sort( Long::compare ); + actualIds.sort( Long::compare ); + + // then + assertThat( actualIds, equalTo( expectedIds ) ); + } + } + + private List generateValuesFromType( RandomValues.Type[] types ) + { + List values = new ArrayList<>(); + Set duplicateChecker = new HashSet<>(); + for ( long i = 0; i < 30_000; i++ ) + { + Value value; + do + { + value = random.randomValues().nextValueOfTypes( types ); + } + while ( !duplicateChecker.add( value ) ); + values.add( value ); + } + return values; + } + + private List> generateUpdatesFromValues( List values ) + { + List> updates = new ArrayList<>(); + int id = 0; + for ( Value value : values ) + { + updates.add( add( id++, descriptor.schema(), value ) ); + } + return updates; + } } diff --git a/community/random-values/src/main/java/org/neo4j/values/storable/RandomValues.java b/community/random-values/src/main/java/org/neo4j/values/storable/RandomValues.java index 7afd2cc8e849d..9132ddbded9d6 100644 --- a/community/random-values/src/main/java/org/neo4j/values/storable/RandomValues.java +++ b/community/random-values/src/main/java/org/neo4j/values/storable/RandomValues.java @@ -294,10 +294,15 @@ public Value nextValueOfTypes( Type... types ) /** * Create an array containing all value types, excluding provided types. */ - public Type[] excluding( Type... types ) + public static Type[] excluding( Type... exclude ) { - return Arrays.stream( ALL_TYPES ) - .filter( t -> !ArrayUtils.contains( types, t ) ) + return excluding( Type.values(), exclude ); + } + + public static Type[] excluding( Type[] among, Type... exclude ) + { + return Arrays.stream( among ) + .filter( t -> !ArrayUtils.contains( exclude, t ) ) .toArray( Type[]::new ); }