diff --git a/community/kernel-api/src/main/java/org/neo4j/internal/kernel/api/IndexQuery.java b/community/kernel-api/src/main/java/org/neo4j/internal/kernel/api/IndexQuery.java index 523924524a2dd..566ab1995aec1 100644 --- a/community/kernel-api/src/main/java/org/neo4j/internal/kernel/api/IndexQuery.java +++ b/community/kernel-api/src/main/java/org/neo4j/internal/kernel/api/IndexQuery.java @@ -514,6 +514,11 @@ public String from() return (String)from.asObject(); } + public Value fromAsValue() + { + return from; + } + public boolean fromInclusive() { return fromInclusive; @@ -524,6 +529,11 @@ public String to() return (String)to.asObject(); } + public Value toAsValue() + { + return to; + } + public boolean toInclusive() { return toInclusive; diff --git a/community/kernel/pom.xml b/community/kernel/pom.xml index 8275838d85d5b..9599eaeae909c 100644 --- a/community/kernel/pom.xml +++ b/community/kernel/pom.xml @@ -254,6 +254,13 @@ the relevant Commercial Agreement. test-jar test + + org.neo4j + neo4j-values + ${project.version} + test-jar + test + org.apache.commons commons-compress diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringLayout.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringLayout.java new file mode 100644 index 0000000000000..dfaeefa82b1c6 --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringLayout.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2002-2018 "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 org.neo4j.index.internal.gbptree.Layout; +import org.neo4j.io.pagecache.PageCursor; + +import static java.lang.String.format; + +import static org.neo4j.kernel.impl.index.schema.StringSchemaKey.ENTITY_ID_SIZE; + +/** + * {@link Layout} for numbers where numbers doesn't need to be unique. + */ +abstract class StringLayout extends Layout.Adapter +{ + @Override + public StringSchemaKey newKey() + { + return new StringSchemaKey(); + } + + @Override + public StringSchemaKey copyKey( StringSchemaKey key, StringSchemaKey into ) + { + // TODO when we have reuse of byte[] take that into consideration here too + into.bytes = key.bytes.clone(); + into.setEntityId( key.getEntityId() ); + into.setEntityIdIsSpecialTieBreaker( key.getEntityIdIsSpecialTieBreaker() ); + return into; + } + + @Override + public NativeSchemaValue newValue() + { + return NativeSchemaValue.INSTANCE; + } + + @Override + public int keySize( StringSchemaKey key ) + { + return key.size(); + } + + @Override + public int valueSize( NativeSchemaValue value ) + { + return NativeSchemaValue.SIZE; + } + + @Override + public void writeKey( PageCursor cursor, StringSchemaKey key ) + { + cursor.putLong( key.getEntityId() ); + cursor.putBytes( key.bytes ); + } + + @Override + public void writeValue( PageCursor cursor, NativeSchemaValue value ) + { + } + + @Override + public void readKey( PageCursor cursor, StringSchemaKey into, int keySize ) + { + // TODO consider reusing byte[] instances somehow + into.setEntityId( cursor.getLong() ); + into.bytes = new byte[keySize - ENTITY_ID_SIZE]; + cursor.getBytes( into.bytes ); + } + + @Override + public void readValue( PageCursor cursor, NativeSchemaValue into, int valueSize ) + { + } + + @Override + public boolean fixedSize() + { + return false; + } + + @Override + public String toString() + { + return format( "%s[version:%d.%d, identifier:%d]", getClass().getSimpleName(), majorVersion(), minorVersion(), identifier() ); + } +} diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringLayoutNonUnique.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringLayoutNonUnique.java new file mode 100644 index 0000000000000..b63de1da04bf2 --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringLayoutNonUnique.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2002-2018 "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 org.neo4j.index.internal.gbptree.Layout; + +public class StringLayoutNonUnique extends StringLayout +{ + private static final String IDENTIFIER_NAME = "NUSI"; + static final int MAJOR_VERSION = 0; + static final int MINOR_VERSION = 1; + static long IDENTIFIER = Layout.namedIdentifier( IDENTIFIER_NAME, NativeSchemaValue.SIZE ); + + @Override + public long identifier() + { + return IDENTIFIER; + } + + @Override + public int majorVersion() + { + return MAJOR_VERSION; + } + + @Override + public int minorVersion() + { + return MINOR_VERSION; + } + + @Override + public int compare( StringSchemaKey o1, StringSchemaKey o2 ) + { + int comparison = o1.compareValueTo( o2 ); + return comparison != 0 ? comparison : Long.compare( o1.getEntityId(), o2.getEntityId() ); + } +} diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringLayoutUnique.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringLayoutUnique.java new file mode 100644 index 0000000000000..02aa5bbd9647d --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringLayoutUnique.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2002-2018 "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 org.neo4j.index.internal.gbptree.Layout; + +/** + * {@link Layout} for numbers where numbers need to be unique. + */ +class StringLayoutUnique extends StringLayout +{ + private static final String IDENTIFIER_NAME = "USI"; + static final int MAJOR_VERSION = 0; + static final int MINOR_VERSION = 1; + static long IDENTIFIER = Layout.namedIdentifier( IDENTIFIER_NAME, NativeSchemaValue.SIZE ); + + @Override + public long identifier() + { + return IDENTIFIER; + } + + @Override + public int majorVersion() + { + return MAJOR_VERSION; + } + + @Override + public int minorVersion() + { + return MINOR_VERSION; + } + + @Override + public int compare( StringSchemaKey o1, StringSchemaKey o2 ) + { + int comparison = o1.compareValueTo( o2 ); + if ( comparison == 0 ) + { + // This is a special case where we need also compare entityId to support inclusive/exclusive + if ( o1.getEntityIdIsSpecialTieBreaker() || o2.getEntityIdIsSpecialTieBreaker() ) + { + return Long.compare( o1.getEntityId(), o2.getEntityId() ); + } + } + return comparison; + } +} diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringSchemaIndexAccessor.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringSchemaIndexAccessor.java new file mode 100644 index 0000000000000..80ba1a83054d4 --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringSchemaIndexAccessor.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2002-2018 "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.File; +import java.io.IOException; + +import org.neo4j.index.internal.gbptree.Layout; +import org.neo4j.index.internal.gbptree.RecoveryCleanupWorkCollector; +import org.neo4j.io.fs.FileSystemAbstraction; +import org.neo4j.io.pagecache.PageCache; +import org.neo4j.kernel.api.index.SchemaIndexProvider; +import org.neo4j.kernel.api.schema.index.IndexDescriptor; +import org.neo4j.kernel.impl.api.index.sampling.IndexSamplingConfig; +import org.neo4j.storageengine.api.schema.IndexReader; + +public class StringSchemaIndexAccessor + extends NativeSchemaIndexAccessor +{ + StringSchemaIndexAccessor( + PageCache pageCache, + FileSystemAbstraction fs, + File storeFile, + Layout layout, + RecoveryCleanupWorkCollector recoveryCleanupWorkCollector, + SchemaIndexProvider.Monitor monitor, + IndexDescriptor descriptor, + long indexId, + IndexSamplingConfig samplingConfig ) throws IOException + { + super( pageCache, fs, storeFile, layout, recoveryCleanupWorkCollector, monitor, descriptor, indexId, samplingConfig ); + } + + @Override + public IndexReader newReader() + { + assertOpen(); + return new StringSchemaIndexReader<>( tree, layout, samplingConfig, descriptor ); + } +} diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringSchemaIndexReader.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringSchemaIndexReader.java new file mode 100644 index 0000000000000..d0999e9eef2d9 --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringSchemaIndexReader.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2002-2018 "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 org.neo4j.index.internal.gbptree.GBPTree; +import org.neo4j.index.internal.gbptree.Layout; +import org.neo4j.internal.kernel.api.IndexOrder; +import org.neo4j.internal.kernel.api.IndexQuery; +import org.neo4j.internal.kernel.api.IndexQuery.ExactPredicate; +import org.neo4j.internal.kernel.api.IndexQuery.StringRangePredicate; +import org.neo4j.kernel.api.schema.index.IndexDescriptor; +import org.neo4j.kernel.impl.api.index.sampling.IndexSamplingConfig; +import org.neo4j.values.storable.Value; +import org.neo4j.values.storable.ValueGroup; + +class StringSchemaIndexReader extends NativeSchemaIndexReader +{ + StringSchemaIndexReader( GBPTree tree, Layout layout, IndexSamplingConfig samplingConfig, IndexDescriptor descriptor ) + { + super( tree, layout, samplingConfig, descriptor ); + } + + @Override + void validateQuery( IndexOrder indexOrder, IndexQuery[] predicates ) + { + if ( predicates.length != 1 ) + { + throw new UnsupportedOperationException(); + } + + if ( indexOrder != IndexOrder.NONE ) + { + throw new UnsupportedOperationException( "unsupported order " + indexOrder ); + } + } + + @Override + void initializeRangeForQuery( KEY treeKeyFrom, KEY treeKeyTo, IndexQuery[] predicates ) + { + // todo initialize the keys to prepare for seek + IndexQuery predicate = predicates[0]; + switch ( predicate.type() ) + { + case exists: + treeKeyFrom.initAsLowest(); + treeKeyTo.initAsHighest(); + break; + case exact: + ExactPredicate exactPredicate = (ExactPredicate) predicate; + treeKeyFrom.from( Long.MIN_VALUE, exactPredicate.value() ); + // No need to do the String --> byte[] conversion twice, right? + treeKeyTo.bytes = treeKeyFrom.bytes; + treeKeyTo.setEntityIdIsSpecialTieBreaker( false ); + treeKeyTo.setEntityId( Long.MAX_VALUE ); + break; + case rangeString: + StringRangePredicate rangePredicate = (StringRangePredicate)predicate; + initFromForRange( rangePredicate, treeKeyFrom ); + initToForRange( rangePredicate, treeKeyTo ); + break; + default: + throw new IllegalArgumentException( "IndexQuery of type " + predicate.type() + " is not supported." ); + } + } + + private void initFromForRange( StringRangePredicate rangePredicate, KEY treeKeyFrom ) + { + Value fromValue = rangePredicate.fromAsValue(); + if ( fromValue.valueGroup() == ValueGroup.NO_VALUE ) + { + treeKeyFrom.initAsLowest(); + } + else + { + treeKeyFrom.from( rangePredicate.fromInclusive() ? Long.MIN_VALUE : Long.MAX_VALUE, fromValue ); + treeKeyFrom.setEntityIdIsSpecialTieBreaker( true ); + } + } + + private void initToForRange( StringRangePredicate rangePredicate, KEY treeKeyTo ) + { + Value toValue = rangePredicate.toAsValue(); + if ( toValue.valueGroup() == ValueGroup.NO_VALUE ) + { + treeKeyTo.initAsHighest(); + } + else + { + treeKeyTo.from( rangePredicate.toInclusive() ? Long.MAX_VALUE : Long.MIN_VALUE, toValue ); + treeKeyTo.setEntityIdIsSpecialTieBreaker( true ); + } + } + + @Override + public boolean hasFullValuePrecision( IndexQuery... predicates ) + { + return false; + } + // todo implement +} diff --git a/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringSchemaKey.java b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringSchemaKey.java new file mode 100644 index 0000000000000..52662de699821 --- /dev/null +++ b/community/kernel/src/main/java/org/neo4j/kernel/impl/index/schema/StringSchemaKey.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2002-2018 "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.util.Arrays; + +import org.neo4j.string.UTF8; +import org.neo4j.values.storable.TextValue; +import org.neo4j.values.storable.Value; +import org.neo4j.values.storable.ValueWriter; +import org.neo4j.values.storable.Values; + +import static java.lang.String.format; + +import static org.neo4j.values.storable.UTF8StringValue.codePointByteArrayCompare; + +/** + * Includes value and entity id (to be able to handle non-unique values). + * A value can be any {@link Number} and is represented as a {@code long} to store the raw bits and a type + * to say if it's a long, double or float. + * + * Distinction between double and float exists because coersions between each other and long may differ. + * TODO this should be figured out and potentially reduced to long, double types only. + */ +class StringSchemaKey extends ValueWriter.Adapter implements NativeSchemaKey +{ + static final int ENTITY_ID_SIZE = Long.BYTES; + + private long entityId; + private boolean entityIdIsSpecialTieBreaker; + + // TODO something better or? + // TODO this is UTF-8 bytes for now + byte[] bytes; + + @Override + public void setEntityIdIsSpecialTieBreaker( boolean entityIdIsSpecialTieBreaker ) + { + this.entityIdIsSpecialTieBreaker = entityIdIsSpecialTieBreaker; + } + + @Override + public boolean getEntityIdIsSpecialTieBreaker() + { + return entityIdIsSpecialTieBreaker; + } + + int size() + { + return ENTITY_ID_SIZE + bytes.length; + } + + @Override + public long getEntityId() + { + return entityId; + } + + @Override + public void setEntityId( long entityId ) + { + this.entityId = entityId; + } + + @Override + public void from( long entityId, Value... values ) + { + this.entityId = entityId; + entityIdIsSpecialTieBreaker = false; + assertValidValue( values ).writeTo( this ); + } + + private TextValue assertValidValue( Value... values ) + { + // TODO: support multiple values, right? + if ( values.length > 1 ) + { + throw new IllegalArgumentException( "Tried to create composite key with non-composite schema key layout" ); + } + if ( values.length < 1 ) + { + throw new IllegalArgumentException( "Tried to create key without value" ); + } + if ( !Values.isTextValue( values[0] ) ) + { + throw new IllegalArgumentException( + "Key layout does only support strings, tried to create key from " + values[0] ); + } + return (TextValue) values[0]; + } + + @Override + public String propertiesAsString() + { + return asValue().toString(); + } + + @Override + public Value asValue() + { + return bytes == null ? Values.NO_VALUE : Values.utf8Value( bytes ); + } + + // TODO perhaps merge into parent or something + @Override + public void initAsLowest() + { + bytes = null; + entityId = Long.MIN_VALUE; + entityIdIsSpecialTieBreaker = true; + } + + @Override + public void initAsHighest() + { + bytes = null; + entityId = Long.MAX_VALUE; + entityIdIsSpecialTieBreaker = true; + } + + private boolean isHighest() + { + return entityIdIsSpecialTieBreaker && entityId == Long.MAX_VALUE && bytes == null; + } + + private boolean isLowest() + { + return entityIdIsSpecialTieBreaker && entityId == Long.MIN_VALUE && bytes == null; + } + + /** + * Compares the value of this key to that of another key. + * This method is expected to be called in scenarios where inconsistent reads may happen (and later retried). + * + * @param other the {@link StringSchemaKey} to compare to. + * @return comparison against the {@code other} {@link StringSchemaKey}. + */ + int compareValueTo( StringSchemaKey other ) + { + // TODO cover all cases of bytes == null and special tie breaker and document + if ( bytes != other.bytes ) + { + if ( bytes == null ) + { + return isHighest() ? 1 : -1; + } + if ( other.bytes == null ) + { + return other.isHighest() ? -1 : 1; + } + } + else + { + return 0; + } + try + { + // TODO change to not throw + return codePointByteArrayCompare( bytes, other.bytes ); + } + catch ( Exception e ) + { + // We can not throw here because we will visit this method inside a pageCursor.shouldRetry() block. + // Just return a comparison that at least will be commutative. + return byteArrayCompare( bytes, other.bytes ); + } + } + + private static int byteArrayCompare( byte[] a, byte[] b ) + { + assert a != null && b != null : "Null arrays not supported."; + + if ( a == b ) + { + return 0; + } + + int length = Math.min( a.length, b.length ); + for ( int i = 0; i < length; i++ ) + { + int compare = Byte.compare( a[i], b[i] ); + if ( compare != 0 ) + { + return compare; + } + } + + if ( a.length < b.length ) + { + return -1; + } + + if ( a.length > b.length ) + { + return 1; + } + return 0; + } + + @Override + public String toString() + { + return format( "value=%s,entityId=%d,bytes=%s", asValue(), entityId, Arrays.toString( bytes ) ); + } + + @Override + public void writeString( String value ) + { + // expensiveness + bytes = UTF8.encode( value ); + } + + @Override + public void writeString( char value ) + { + throw new UnsupportedOperationException(); + } +} 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 9c3fecfd7cdde..01fe1e5d231fa 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 @@ -22,6 +22,8 @@ import org.apache.commons.lang3.ArrayUtils; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -42,6 +44,9 @@ abstract class LayoutTestUtil { + private static final Comparator> UPDATE_COMPARATOR = ( u1, u2 ) -> + Values.COMPARATOR.compare( u1.values()[0], u2.values()[0] ); + final IndexDescriptor indexDescriptor; LayoutTestUtil( IndexDescriptor indexDescriptor ) @@ -55,9 +60,7 @@ abstract class LayoutTestUtil> randomUpdateGenerator( RandomRule random ); + Iterator> randomUpdateGenerator( RandomRule random ) + { + double fractionDuplicates = fractionDuplicates(); + return new PrefetchingIterator>() + { + private final Set uniqueCompareValues = new HashSet<>(); + private final List uniqueValues = new ArrayList<>(); + private long currentEntityId; + + @Override + protected IndexEntryUpdate fetchNextOrNull() + { + Value value; + if ( fractionDuplicates > 0 && !uniqueValues.isEmpty() && + random.nextFloat() < fractionDuplicates ) + { + value = existingNonUniqueValue( random ); + } + else + { + value = newUniqueValue( random, uniqueCompareValues, uniqueValues ); + } + + return add( currentEntityId++, value ); + } + + private Value existingNonUniqueValue( RandomRule randomRule ) + { + return uniqueValues.get( randomRule.nextInt( uniqueValues.size() ) ); + } + }; + } + + abstract Value newUniqueValue( RandomRule random, Set uniqueCompareValues, List uniqueValues ); Value[] extractValuesFromUpdates( IndexEntryUpdate[] updates ) { @@ -86,22 +122,16 @@ Value[] extractValuesFromUpdates( IndexEntryUpdate[] updates ) return values; } - IndexEntryUpdate[] someUpdatesNoDuplicateValues() - { - return generateAddUpdatesFor( ALL_EXTREME_VALUES ); - } + abstract IndexEntryUpdate[] someUpdatesNoDuplicateValues(); - IndexEntryUpdate[] someUpdatesWithDuplicateValues() - { - return generateAddUpdatesFor( ArrayUtils.addAll( ALL_EXTREME_VALUES, ALL_EXTREME_VALUES ) ); - } + abstract IndexEntryUpdate[] someUpdatesWithDuplicateValues(); IndexEntryUpdate[] someSpatialUpdatesWithDuplicateValues() { return generateAddUpdatesFor( ArrayUtils.addAll( SOME_POINTS, SOME_POINTS ) ); } - private IndexEntryUpdate[] generateAddUpdatesFor( Object[] values ) + protected IndexEntryUpdate[] generateAddUpdatesFor( Object[] values ) { @SuppressWarnings( "unchecked" ) IndexEntryUpdate[] indexEntryUpdates = new IndexEntryUpdate[values.length]; @@ -120,28 +150,6 @@ private IndexEntryUpdate[] generateAddUpdatesFor( Object[] valu Values.pointValue( CoordinateReferenceSystem.WGS84, -50, -25 ) }; - private static final Number[] ALL_EXTREME_VALUES = new Number[] - { - Byte.MAX_VALUE, - Byte.MIN_VALUE, - Short.MAX_VALUE, - Short.MIN_VALUE, - Integer.MAX_VALUE, - Integer.MIN_VALUE, - Long.MAX_VALUE, - Long.MIN_VALUE, - Float.MAX_VALUE, - -Float.MAX_VALUE, - Double.MAX_VALUE, - -Double.MAX_VALUE, - Double.POSITIVE_INFINITY, - Double.NEGATIVE_INFINITY, - 0, - // These two values below coerce to the same double - 1234567890123456788L, - 1234567890123456789L - }; - protected IndexEntryUpdate add( long nodeId, Value value ) { return IndexEntryUpdate.add( nodeId, indexDescriptor, value ); @@ -156,4 +164,9 @@ static int countUniqueValues( Number[] updates ) { return Stream.of( updates ).collect( Collectors.toSet() ).size(); } + + void sort( IndexEntryUpdate[] updates ) + { + Arrays.sort( updates, UPDATE_COMPARATOR ); + } } diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexAccessorTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexAccessorTest.java index bb9778f00abd0..674df1088b300 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexAccessorTest.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexAccessorTest.java @@ -19,13 +19,11 @@ */ package org.neo4j.kernel.impl.index.schema; -import org.hamcrest.CoreMatchers; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; - import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -56,20 +54,17 @@ import org.neo4j.storageengine.api.schema.IndexReader; import org.neo4j.storageengine.api.schema.IndexSample; import org.neo4j.storageengine.api.schema.IndexSampler; -import org.neo4j.storageengine.api.schema.SimpleNodeValueClient; import org.neo4j.values.storable.Value; -import org.neo4j.values.storable.Values; - import static java.lang.String.format; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; 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.helpers.collection.Iterables.asUniqueSet; +import static org.neo4j.helpers.collection.Iterators.filter; import static org.neo4j.kernel.api.index.IndexEntryUpdate.change; import static org.neo4j.kernel.api.index.IndexEntryUpdate.remove; import static org.neo4j.kernel.impl.api.index.IndexUpdateMode.ONLINE; @@ -159,26 +154,13 @@ public void shouldIndexChange() throws Exception // given IndexEntryUpdate[] updates = layoutUtil.someUpdates(); processAll( updates ); + Iterator> generator = filter( skipExisting( updates ), layoutUtil.randomUpdateGenerator( random ) ); for ( int i = 0; i < updates.length; i++ ) { IndexEntryUpdate update = updates[i]; - Number newValue; - switch ( i % 3 ) - { - case 0: - newValue = NON_EXISTENT_VALUE + i; - break; - case 1: - newValue = (float) NON_EXISTENT_VALUE + i; - break; - case 2: - newValue = (double) NON_EXISTENT_VALUE + i; - break; - default: - throw new IllegalArgumentException(); - } - updates[i] = change( update.getEntityId(), indexDescriptor, update.values()[0], layoutUtil.asValue( newValue ) ); + Value newValue = generator.next().values()[0]; + updates[i] = change( update.getEntityId(), indexDescriptor, update.values()[0], newValue ); } // when @@ -245,7 +227,8 @@ public void shouldReturnZeroCountForEmptyIndex() try ( IndexReader reader = accessor.newReader() ) { // when - long count = reader.countIndexedNodes( 123, layoutUtil.asValue( 456 ) ); + IndexEntryUpdate update = layoutUtil.randomUpdateGenerator( random ).next(); + long count = reader.countIndexedNodes( 123, update.values()[0] ); // then assertEquals( 0, count ); @@ -271,7 +254,8 @@ public void shouldReturnCountOneForExistingData() throws Exception } // and when - long count = reader.countIndexedNodes( 123, layoutUtil.asValue( 456 ) ); + Iterator> generator = filter( skipExisting( updates ), layoutUtil.randomUpdateGenerator( random ) ); + long count = reader.countIndexedNodes( 123, generator.next().values()[0] ); // then assertEquals( 0, count ); @@ -292,7 +276,7 @@ public void shouldReturnCountZeroForMismatchingData() throws Exception { long countWithMismatchingData = reader.countIndexedNodes( update.getEntityId() + 1, update.values() ); long countWithNonExistentEntityId = reader.countIndexedNodes( NON_EXISTENT_ENTITY_ID, update.values() ); - long countWithNonExistentValue = reader.countIndexedNodes( update.getEntityId(), layoutUtil.asValue( NON_EXISTENT_VALUE ) ); + long countWithNonExistentValue = reader.countIndexedNodes( update.getEntityId(), generateUniqueValue( updates ) ); // then assertEquals( 0, countWithMismatchingData ); @@ -354,7 +338,7 @@ public void shouldReturnNoEntriesForMismatchingExactPredicate() throws Exception // when IndexReader reader = accessor.newReader(); - Object value = layoutUtil.asValue( NON_EXISTENT_VALUE ); + Object value = generateUniqueValue( updates ); PrimitiveLongIterator result = query( reader, IndexQuery.exact( 0, value ) ); assertEntityIdHits( EMPTY_LONG_ARRAY, result ); } @@ -365,12 +349,13 @@ public void shouldReturnMatchingEntriesForRangePredicateWithInclusiveStartAndExc // given IndexEntryUpdate[] updates = layoutUtil.someUpdates(); processAll( updates ); + layoutUtil.sort( updates ); // when IndexReader reader = accessor.newReader(); PrimitiveLongIterator result = query( reader, - layoutUtil.rangeQuery( Double.NEGATIVE_INFINITY, true, Double.POSITIVE_INFINITY, false ) ); - assertEntityIdHits( extractEntityIds( updates, lessThan( layoutUtil.asValue( Double.POSITIVE_INFINITY ) ) ), result ); + layoutUtil.rangeQuery( valueOf( updates[0] ), true, valueOf( updates[updates.length - 1] ), false ) ); + assertEntityIdHits( extractEntityIds( Arrays.copyOf( updates, updates.length - 1 ), alwaysTrue() ), result ); } @Test @@ -379,11 +364,12 @@ public void shouldReturnMatchingEntriesForRangePredicateWithInclusiveStartAndInc // given IndexEntryUpdate[] updates = layoutUtil.someUpdates(); processAll( updates ); + layoutUtil.sort( updates ); // when IndexReader reader = accessor.newReader(); PrimitiveLongIterator result = query( reader, - layoutUtil.rangeQuery( Double.NEGATIVE_INFINITY, true, Double.POSITIVE_INFINITY, true ) ); + layoutUtil.rangeQuery( valueOf( updates[0] ), true, valueOf( updates[updates.length - 1] ), true ) ); assertEntityIdHits( extractEntityIds( updates, alwaysTrue() ), result ); } @@ -393,13 +379,13 @@ public void shouldReturnMatchingEntriesForRangePredicateWithExclusiveStartAndExc // given IndexEntryUpdate[] updates = layoutUtil.someUpdates(); processAll( updates ); + layoutUtil.sort( updates ); // when IndexReader reader = accessor.newReader(); PrimitiveLongIterator result = query( reader, - layoutUtil.rangeQuery( Double.NEGATIVE_INFINITY, false, Double.POSITIVE_INFINITY, false ) ); - assertEntityIdHits( extractEntityIds( updates, - all( greaterThan( layoutUtil.asValue( Double.NEGATIVE_INFINITY ) ), lessThan( layoutUtil.asValue( Double.POSITIVE_INFINITY ) ) ) ), result ); + layoutUtil.rangeQuery( valueOf( updates[0] ), false, valueOf( updates[updates.length - 1] ), false ) ); + assertEntityIdHits( extractEntityIds( Arrays.copyOfRange( updates, 1, updates.length - 1 ), alwaysTrue() ), result ); } @Test @@ -408,80 +394,46 @@ public void shouldReturnMatchingEntriesForRangePredicateWithExclusiveStartAndInc // given IndexEntryUpdate[] updates = layoutUtil.someUpdates(); processAll( updates ); + layoutUtil.sort( updates ); // when IndexReader reader = accessor.newReader(); PrimitiveLongIterator result = query( reader, - layoutUtil.rangeQuery( Double.NEGATIVE_INFINITY, false, Double.POSITIVE_INFINITY, true ) ); - assertEntityIdHits( extractEntityIds( updates, - all( - greaterThan( layoutUtil.asValue( Double.NEGATIVE_INFINITY ) ), - lessThanOrEqual( layoutUtil.asValue( Double.POSITIVE_INFINITY ) ) - ) ), result ); - } - - // - - @Test - public void throwForUnsupportedIndexOrder() throws Exception - { - // given - // Unsupported index order for query - IndexReader reader = accessor.newReader(); - IndexOrder unsupportedOrder = IndexOrder.DESCENDING; - IndexQuery.ExactPredicate unsupportedQuery = IndexQuery.exact( 0, "Legolas" ); - - // then - expected.expect( UnsupportedOperationException.class ); - expected.expectMessage( CoreMatchers.allOf( - CoreMatchers.containsString( "unsupported order" ), - CoreMatchers.containsString( unsupportedOrder.toString() ), - CoreMatchers.containsString( unsupportedQuery.toString() ) ) ); - - // when - reader.query( new SimpleNodeValueClient(), unsupportedOrder, unsupportedQuery ); + layoutUtil.rangeQuery( valueOf( updates[0] ), false, valueOf( updates[updates.length - 1] ), true ) ); + assertEntityIdHits( extractEntityIds( Arrays.copyOfRange( updates, 1, updates.length ), alwaysTrue() ), result ); } - // - @Test public void shouldReturnNoEntriesForRangePredicateOutsideAnyMatch() throws Exception { // given IndexEntryUpdate[] updates = layoutUtil.someUpdates(); - processAll( updates ); + layoutUtil.sort( updates ); + processAll( updates[0], updates[1], updates[updates.length - 1], updates[updates.length - 2] ); // when IndexReader reader = accessor.newReader(); PrimitiveLongIterator result = query( reader, - layoutUtil.rangeQuery( NON_EXISTENT_VALUE, true, NON_EXISTENT_VALUE + 10, true ) ); + layoutUtil.rangeQuery( valueOf( updates[2] ), true, valueOf( updates[updates.length - 3] ), true ) ); assertEntityIdHits( EMPTY_LONG_ARRAY, result ); } @Test( timeout = 10_000L ) - @SuppressWarnings( "unchecked" ) public void mustHandleNestedQueries() throws Exception { // given - IndexEntryUpdate[] updates = new IndexEntryUpdate[] - { - IndexEntryUpdate.add( 0, indexDescriptor, layoutUtil.asValue( 0 ) ), - IndexEntryUpdate.add( 1, indexDescriptor, layoutUtil.asValue( 1 ) ), - IndexEntryUpdate.add( 2, indexDescriptor, layoutUtil.asValue( 2 ) ), - IndexEntryUpdate.add( 3, indexDescriptor, layoutUtil.asValue( 3 ) ), - IndexEntryUpdate.add( 4, indexDescriptor, layoutUtil.asValue( 4 ) ), - IndexEntryUpdate.add( 5, indexDescriptor, layoutUtil.asValue( 5 ) ) - }; + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); processAll( updates ); + layoutUtil.sort( updates ); // when IndexReader reader = accessor.newReader(); - IndexQuery outerQuery = layoutUtil.rangeQuery( 2, true, 3, true ); - IndexQuery innerQuery = layoutUtil.rangeQuery( 0, true, 1, true ); + IndexQuery outerQuery = layoutUtil.rangeQuery( valueOf( updates[2] ), true, valueOf( updates[3] ), true ); + IndexQuery innerQuery = layoutUtil.rangeQuery( valueOf( updates[0] ), true, valueOf( updates[1] ), true ); - long[] expectedOuter = new long[]{2, 3}; - long[] expectedInner = new long[]{0, 1}; + long[] expectedOuter = new long[]{entityIdOf( updates[2] ), entityIdOf( updates[3] )}; + long[] expectedInner = new long[]{entityIdOf( updates[0] ), entityIdOf( updates[1] )}; PrimitiveLongIterator outerIter = query( reader, outerQuery ); Collection outerResult = new ArrayList<>(); @@ -494,32 +446,24 @@ public void mustHandleNestedQueries() throws Exception assertEntityIdHits( expectedOuter, outerResult ); } - @Test( timeout = 10_000L ) - @SuppressWarnings( "unchecked" ) + @Test public void mustHandleMultipleNestedQueries() throws Exception { // given - IndexEntryUpdate[] updates = new IndexEntryUpdate[] - { - IndexEntryUpdate.add( 0, indexDescriptor, layoutUtil.asValue( 0 ) ), - IndexEntryUpdate.add( 1, indexDescriptor, layoutUtil.asValue( 1 ) ), - IndexEntryUpdate.add( 2, indexDescriptor, layoutUtil.asValue( 2 ) ), - IndexEntryUpdate.add( 3, indexDescriptor, layoutUtil.asValue( 3 ) ), - IndexEntryUpdate.add( 4, indexDescriptor, layoutUtil.asValue( 4 ) ), - IndexEntryUpdate.add( 5, indexDescriptor, layoutUtil.asValue( 5 ) ) - }; + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); processAll( updates ); + layoutUtil.sort( updates ); // when IndexReader reader = accessor.newReader(); - IndexQuery query1 = layoutUtil.rangeQuery( 4, true, 5, true ); - IndexQuery query2 = layoutUtil.rangeQuery( 2, true, 3, true ); - IndexQuery query3 = layoutUtil.rangeQuery( 0, true, 1, true ); + IndexQuery query1 = layoutUtil.rangeQuery( valueOf( updates[4] ), true, valueOf( updates[5] ), true ); + IndexQuery query2 = layoutUtil.rangeQuery( valueOf( updates[2] ), true, valueOf( updates[3] ), true ); + IndexQuery query3 = layoutUtil.rangeQuery( valueOf( updates[0] ), true, valueOf( updates[1] ), true ); - long[] expected1 = new long[]{4, 5}; - long[] expected2 = new long[]{2, 3}; - long[] expected3 = new long[]{0, 1}; + long[] expected1 = new long[]{entityIdOf( updates[4] ), entityIdOf( updates[5] )}; + long[] expected2 = new long[]{entityIdOf( updates[2] ), entityIdOf( updates[3] )}; + long[] expected3 = new long[]{entityIdOf( updates[0] ), entityIdOf( updates[1] )}; Collection result1 = new ArrayList<>(); PrimitiveLongIterator iter1 = query( reader, query1 ); @@ -546,6 +490,11 @@ public void mustHandleMultipleNestedQueries() throws Exception assertEntityIdHits( expected1, result1 ); } + private long entityIdOf( IndexEntryUpdate update ) + { + return update.getEntityId(); + } + @Test public void shouldHandleMultipleConsecutiveUpdaters() throws Exception { @@ -735,6 +684,73 @@ public void shouldSeeNoEntriesInAllEntriesReaderOnEmptyIndex() assertEquals( expectedIds, ids ); } + @Test + public void shouldNotSeeFilteredEntries() throws Exception + { + // given + IndexEntryUpdate[] updates = layoutUtil.someUpdates(); + processAll( updates ); + layoutUtil.sort( updates ); + IndexReader reader = accessor.newReader(); + + // when + NodeValueIterator iter = new NodeValueIterator(); + IndexQuery.ExactPredicate filter = IndexQuery.exact( 0, valueOf( updates[1] ) ); + IndexQuery rangeQuery = layoutUtil.rangeQuery( valueOf( updates[0] ), true, valueOf( updates[2] ), true ); + IndexProgressor.NodeValueClient filterClient = filterClient( iter, filter ); + reader.query( filterClient, IndexOrder.NONE, rangeQuery ); + + // then + assertTrue( iter.hasNext() ); + assertEquals( entityIdOf( updates[1] ), iter.next() ); + assertFalse( iter.hasNext() ); + } + + private Value generateUniqueValue( IndexEntryUpdate[] updates ) + { + return filter( skipExisting( updates ), layoutUtil.randomUpdateGenerator( random ) ).next().values()[0]; + } + + private static Predicate> skipExisting( IndexEntryUpdate[] existing ) + { + Set> set = new HashSet<>( Arrays.asList( existing ) ); + return candidate -> set.add( candidate ); + } + + private Object valueOf( IndexEntryUpdate update ) + { + return update.values()[0].asObject(); + } + + private IndexProgressor.NodeValueClient filterClient( final NodeValueIterator iter, final IndexQuery.ExactPredicate filter ) + { + return new IndexProgressor.NodeValueClient() + { + @Override + public void initialize( IndexDescriptor descriptor, IndexProgressor progressor, IndexQuery[] query ) + { + iter.initialize( descriptor, progressor, query ); + } + + @Override + public boolean acceptNode( long reference, Value... values ) + { + //noinspection SimplifiableIfStatement + if ( values.length > 1 ) + { + return false; + } + return filter.acceptsValue( values[0] ) && iter.acceptNode( reference, values ); + } + + @Override + public boolean needsValues() + { + return true; + } + }; + } + private PrimitiveLongIterator query( IndexReader reader, IndexQuery query ) throws IndexNotApplicableKernelException { NodeValueIterator client = new NodeValueIterator(); diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexTestUtil.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexTestUtil.java index 8dbad3eb8e6af..a3bcafab8b21b 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexTestUtil.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NativeSchemaIndexTestUtil.java @@ -57,7 +57,6 @@ public abstract class NativeSchemaIndexTestUtil { - static final long NON_EXISTENT_VALUE = Short.MAX_VALUE + 1; static final long NON_EXISTENT_ENTITY_ID = 1_000_000_000; final FileSystemRule fs = new DefaultFileSystemRule(); diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NumberLayoutTestUtil.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NumberLayoutTestUtil.java index 12acf32598483..353065dd64f33 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NumberLayoutTestUtil.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NumberLayoutTestUtil.java @@ -19,13 +19,11 @@ */ package org.neo4j.kernel.impl.index.schema; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Iterator; +import org.apache.commons.lang3.ArrayUtils; + import java.util.List; import java.util.Set; -import org.neo4j.helpers.collection.PrefetchingIterator; import org.neo4j.internal.kernel.api.IndexQuery; import org.neo4j.kernel.api.index.IndexEntryUpdate; import org.neo4j.kernel.api.schema.index.IndexDescriptor; @@ -35,21 +33,37 @@ abstract class NumberLayoutTestUtil extends LayoutTestUtil { + private static final Number[] ALL_EXTREME_VALUES = new Number[] + { + Byte.MAX_VALUE, + Byte.MIN_VALUE, + Short.MAX_VALUE, + Short.MIN_VALUE, + Integer.MAX_VALUE, + Integer.MIN_VALUE, + Long.MAX_VALUE, + Long.MIN_VALUE, + Float.MAX_VALUE, + -Float.MAX_VALUE, + Double.MAX_VALUE, + -Double.MAX_VALUE, + Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY, + 0, + // These two values below coerce to the same double + 1234567890123456788L, + 1234567890123456789L + }; + NumberLayoutTestUtil( IndexDescriptor indexDescriptor ) { super( indexDescriptor ); } @Override - IndexQuery rangeQuery( Number from, boolean fromInclusive, Number to, boolean toInclusive ) + IndexQuery rangeQuery( Object from, boolean fromInclusive, Object to, boolean toInclusive ) { - return IndexQuery.range( 0, from, fromInclusive, to, toInclusive ); - } - - @Override - Value asValue( Number value ) - { - return Values.of( value ); + return IndexQuery.range( 0, (Number) from, fromInclusive, (Number) to, toInclusive ); } @Override @@ -63,51 +77,30 @@ int compareIndexedPropertyValue( NumberSchemaKey key1, NumberSchemaKey key2 ) return typeCompare; } - @Override - Iterator> randomUpdateGenerator( RandomRule random ) + IndexEntryUpdate[] someUpdatesNoDuplicateValues() { - double fractionDuplicates = fractionDuplicates(); - return new PrefetchingIterator>() - { - private final Set uniqueCompareValues = new HashSet<>(); - private final List uniqueValues = new ArrayList<>(); - private long currentEntityId; - - @Override - protected IndexEntryUpdate fetchNextOrNull() - { - Value value; - if ( fractionDuplicates > 0 && !uniqueValues.isEmpty() && random.nextFloat() < fractionDuplicates ) - { - value = existingNonUniqueValue( random ); - } - else - { - value = newUniqueValue( random ); - } - - return add( currentEntityId++, value ); - } + return generateAddUpdatesFor( ALL_EXTREME_VALUES ); + } - private Value newUniqueValue( RandomRule randomRule ) - { - Number value; - Double compareValue; - do - { - value = randomRule.numberPropertyValue(); - compareValue = value.doubleValue(); - } - while ( !uniqueCompareValues.add( compareValue ) ); - Value storableValue = asValue( value ); - uniqueValues.add( storableValue ); - return storableValue; - } + @Override + IndexEntryUpdate[] someUpdatesWithDuplicateValues() + { + return generateAddUpdatesFor( ArrayUtils.addAll( ALL_EXTREME_VALUES, ALL_EXTREME_VALUES ) ); + } - private Value existingNonUniqueValue( RandomRule randomRule ) - { - return uniqueValues.get( randomRule.nextInt( uniqueValues.size() ) ); - } - }; + @Override + Value newUniqueValue( RandomRule random, Set uniqueCompareValues, List uniqueValues ) + { + Number value; + Double compareValue; + do + { + value = random.numberPropertyValue(); + compareValue = value.doubleValue(); + } + while ( !uniqueCompareValues.add( compareValue ) ); + Value storableValue = Values.of( value ); + uniqueValues.add( storableValue ); + return storableValue; } } diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NumberSchemaIndexAccessorTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NumberSchemaIndexAccessorTest.java index 22a0dc3c6ec2b..e6944e41c115b 100644 --- a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NumberSchemaIndexAccessorTest.java +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/NumberSchemaIndexAccessorTest.java @@ -19,6 +19,7 @@ */ package org.neo4j.kernel.impl.index.schema; +import org.hamcrest.CoreMatchers; import org.junit.Test; import java.io.IOException; @@ -29,7 +30,6 @@ import org.neo4j.kernel.api.index.IndexEntryUpdate; import org.neo4j.kernel.api.schema.index.IndexDescriptor; import org.neo4j.kernel.impl.api.index.sampling.IndexSamplingConfig; -import org.neo4j.storageengine.api.schema.IndexProgressor; import org.neo4j.storageengine.api.schema.IndexReader; import org.neo4j.storageengine.api.schema.SimpleNodeValueClient; import org.neo4j.values.storable.Value; @@ -37,13 +37,13 @@ import org.neo4j.values.storable.Values; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.neo4j.index.internal.gbptree.RecoveryCleanupWorkCollector.IMMEDIATE; public abstract class NumberSchemaIndexAccessorTest extends NativeSchemaIndexAccessorTest { + @Override NativeSchemaIndexAccessor makeAccessorWithSamplingConfig( IndexSamplingConfig samplingConfig ) throws IOException { return new NumberSchemaIndexAccessor<>( pageCache, fs, indexFile, layout, IMMEDIATE, monitor, indexDescriptor, indexId, samplingConfig ); @@ -84,55 +84,27 @@ public void respectIndexOrder() throws Exception } } + // + @Test - public void shouldNotSeeFilteredEntries() throws Exception + public void throwForUnsupportedIndexOrder() throws Exception { // given - IndexEntryUpdate[] updates = new IndexEntryUpdate[]{IndexEntryUpdate.add( 0, indexDescriptor, layoutUtil.asValue( 0 ) ), - IndexEntryUpdate.add( 1, indexDescriptor, layoutUtil.asValue( 1 ) ), IndexEntryUpdate.add( 2, indexDescriptor, layoutUtil.asValue( 2 ) ),}; - //noinspection unchecked - processAll( updates ); + // Unsupported index order for query IndexReader reader = accessor.newReader(); - - // when - NodeValueIterator iter = new NodeValueIterator(); - IndexQuery.ExactPredicate filter = IndexQuery.exact( 0, layoutUtil.asValue( 1 ) ); - IndexQuery rangeQuery = layoutUtil.rangeQuery( 0, true, 2, true ); - IndexProgressor.NodeValueClient filterClient = filterClient( iter, filter ); - reader.query( filterClient, IndexOrder.NONE, rangeQuery ); + IndexOrder unsupportedOrder = IndexOrder.DESCENDING; + IndexQuery.ExactPredicate unsupportedQuery = IndexQuery.exact( 0, "Legolas" ); // then - assertTrue( iter.hasNext() ); - assertEquals( 1, iter.next() ); - assertFalse( iter.hasNext() ); - } - - private IndexProgressor.NodeValueClient filterClient( final NodeValueIterator iter, final IndexQuery.ExactPredicate filter ) - { - return new IndexProgressor.NodeValueClient() - { - @Override - public void initialize( IndexDescriptor descriptor, IndexProgressor progressor, IndexQuery[] query ) - { - iter.initialize( descriptor, progressor, query ); - } - - @Override - public boolean acceptNode( long reference, Value... values ) - { - //noinspection SimplifiableIfStatement - if ( values.length > 1 ) - { - return false; - } - return filter.acceptsValue( values[0] ) && iter.acceptNode( reference, values ); - } + expected.expect( UnsupportedOperationException.class ); + expected.expectMessage( CoreMatchers.allOf( + CoreMatchers.containsString( "unsupported order" ), + CoreMatchers.containsString( unsupportedOrder.toString() ), + CoreMatchers.containsString( unsupportedQuery.toString() ) ) ); - @Override - public boolean needsValues() - { - return true; - } - }; + // when + reader.query( new SimpleNodeValueClient(), unsupportedOrder, unsupportedQuery ); } + + // } diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringLayoutTestUtil.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringLayoutTestUtil.java new file mode 100644 index 0000000000000..b3aba8d22d125 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringLayoutTestUtil.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2002-2018 "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.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.neo4j.internal.kernel.api.IndexQuery; +import org.neo4j.kernel.api.index.IndexEntryUpdate; +import org.neo4j.kernel.api.schema.index.IndexDescriptor; +import org.neo4j.test.rule.RandomRule; +import org.neo4j.values.storable.TextValue; +import org.neo4j.values.storable.Value; +import org.neo4j.values.storable.Values; + +import static java.util.Arrays.asList; + +import static org.neo4j.values.storable.StringsLibrary.STRINGS; +import static org.neo4j.values.storable.UTF8StringValue.codePointByteArrayCompare; + +abstract class StringLayoutTestUtil extends LayoutTestUtil +{ + StringLayoutTestUtil( IndexDescriptor indexDescriptor ) + { + super( indexDescriptor ); + } + + @Override + IndexQuery rangeQuery( Object from, boolean fromInclusive, Object to, boolean toInclusive ) + { + return IndexQuery.range( 0, (String) from, fromInclusive, (String) to, toInclusive ); + } + + @Override + int compareIndexedPropertyValue( StringSchemaKey key1, StringSchemaKey key2 ) + { + return codePointByteArrayCompare( key1.bytes, key2.bytes ); + } + + @Override + IndexEntryUpdate[] someUpdates() + { + return generateAddUpdatesFor( STRINGS ); + } + + @Override + IndexEntryUpdate[] someUpdatesNoDuplicateValues() + { + return generateAddUpdatesFor( STRINGS ); + } + + @Override + IndexEntryUpdate[] someUpdatesWithDuplicateValues() + { + Collection duplicates = new ArrayList<>( asList( STRINGS ) ); + duplicates.addAll( asList( STRINGS ) ); + return generateAddUpdatesFor( STRINGS ); + } + + @Override + protected Value newUniqueValue( RandomRule random, Set uniqueCompareValues, List uniqueValues ) + { + String candidate; + do + { + candidate = random.string(); + } + while ( !uniqueCompareValues.add( candidate ) ); + TextValue result = Values.stringValue( candidate ); + uniqueValues.add( result ); + return result; + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringNonUniqueLayoutTestUtil.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringNonUniqueLayoutTestUtil.java new file mode 100644 index 0000000000000..69360eba1c8aa --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringNonUniqueLayoutTestUtil.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2002-2018 "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 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 StringNonUniqueLayoutTestUtil extends StringLayoutTestUtil +{ + StringNonUniqueLayoutTestUtil() + { + super( IndexDescriptorFactory.forLabel( 42, 666 ) ); + } + + @Override + Layout createLayout() + { + return new StringLayoutNonUnique(); + } + + @Override + IndexEntryUpdate[] someUpdates() + { + return someUpdatesWithDuplicateValues(); + } + + @Override + protected double fractionDuplicates() + { + return 0.1; + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringNonUniqueSchemaIndexAccessorTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringNonUniqueSchemaIndexAccessorTest.java new file mode 100644 index 0000000000000..5e1f0902daad3 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringNonUniqueSchemaIndexAccessorTest.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2002-2018 "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; + +public class StringNonUniqueSchemaIndexAccessorTest extends StringSchemaIndexAccessorTest +{ + @Override + protected LayoutTestUtil createLayoutTestUtil() + { + return new StringNonUniqueLayoutTestUtil(); + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringSchemaIndexAccessorTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringSchemaIndexAccessorTest.java new file mode 100644 index 0000000000000..db434d2bf9cf8 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringSchemaIndexAccessorTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2002-2018 "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 org.neo4j.kernel.impl.api.index.sampling.IndexSamplingConfig; + +import static org.neo4j.index.internal.gbptree.RecoveryCleanupWorkCollector.IMMEDIATE; + +public abstract class StringSchemaIndexAccessorTest + extends NativeSchemaIndexAccessorTest +{ + @Override + NativeSchemaIndexAccessor makeAccessorWithSamplingConfig( IndexSamplingConfig samplingConfig ) throws IOException + { + return new StringSchemaIndexAccessor<>( pageCache, fs, indexFile, layout, IMMEDIATE, monitor, indexDescriptor, indexId, samplingConfig ); + } + + // TODO test reader unsupported index order +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringUniqueLayoutTestUtil.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringUniqueLayoutTestUtil.java new file mode 100644 index 0000000000000..24a8b9b31cdb9 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringUniqueLayoutTestUtil.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2002-2018 "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 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 StringUniqueLayoutTestUtil extends StringLayoutTestUtil +{ + StringUniqueLayoutTestUtil() + { + super( IndexDescriptorFactory.forLabel( 42, 666 ) ); + } + + @Override + Layout createLayout() + { + return new StringLayoutNonUnique(); + } + + @Override + IndexEntryUpdate[] someUpdates() + { + return someUpdatesNoDuplicateValues(); + } + + @Override + protected double fractionDuplicates() + { + return 0; + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringUniqueSchemaIndexAccessorTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringUniqueSchemaIndexAccessorTest.java new file mode 100644 index 0000000000000..1fc946f65ede3 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/StringUniqueSchemaIndexAccessorTest.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2002-2018 "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; + +public class StringUniqueSchemaIndexAccessorTest extends StringSchemaIndexAccessorTest +{ + @Override + protected LayoutTestUtil createLayoutTestUtil() + { + return new StringUniqueLayoutTestUtil(); + } +} diff --git a/community/values/src/main/java/org/neo4j/values/storable/UTF8StringValue.java b/community/values/src/main/java/org/neo4j/values/storable/UTF8StringValue.java index 8ba0f76360ff4..e6a04b22d375a 100644 --- a/community/values/src/main/java/org/neo4j/values/storable/UTF8StringValue.java +++ b/community/values/src/main/java/org/neo4j/values/storable/UTF8StringValue.java @@ -99,6 +99,11 @@ String value() @Override public int length() + { + return numberOfCodePoints( bytes, offset, byteLength ); + } + + private static int numberOfCodePoints( byte[] bytes, int offset, int byteLength ) { int count = 0, i = offset, len = offset + byteLength; while ( i < len ) @@ -161,7 +166,7 @@ public int computeHash() bytesNeeded++; b = (byte) (b << 1); } - int codePoint = codePoint( b, i, bytesNeeded ); + int codePoint = codePoint( bytes, b, i, bytesNeeded ); i += bytesNeeded; hash = 31 * hash + codePoint; @@ -332,15 +337,26 @@ public int compareTo( TextValue other ) return super.compareTo( other ); } UTF8StringValue otherUTF8 = (UTF8StringValue) other; - int len1 = bytes.length; - int len2 = otherUTF8.bytes.length; + return codePointByteArrayCompare( bytes, offset, byteLength, otherUTF8.bytes, otherUTF8.offset, otherUTF8.byteLength ); + } + + public static int codePointByteArrayCompare( byte[] value1, byte[] value2 ) + { + return codePointByteArrayCompare( value1, 0, value1.length, value2, 0, value2.length ); + } + + public static int codePointByteArrayCompare( byte[] value1, int value1Offset, int value1Length, + byte[] value2, int value2Offset, int value2Length ) + { + int len1 = value1.length; + int len2 = value2.length; int lim = Math.min( len1, len2 ); int i = 0; while ( i < lim ) { - byte b = bytes[i]; + byte b = value1[i]; int thisCodePoint; - int thatCodePoint = codePointAt( otherUTF8.bytes, i ); + int thatCodePoint = codePointAt( value2, i ); if ( b >= 0 ) { i++; @@ -354,7 +370,7 @@ public int compareTo( TextValue other ) bytesNeeded++; b = (byte) (b << 1); } - thisCodePoint = codePoint( b, i, bytesNeeded ); + thisCodePoint = codePoint( value1, b, i, bytesNeeded ); i += bytesNeeded; } if ( thisCodePoint != thatCodePoint ) @@ -362,8 +378,7 @@ public int compareTo( TextValue other ) return thisCodePoint - thatCodePoint; } } - - return length() - other.length(); + return numberOfCodePoints( value1, value1Offset, value1Length ) - numberOfCodePoints( value2, value2Offset, value2Length ); } @Override @@ -372,7 +387,7 @@ Matcher matcher( Pattern pattern ) return pattern.matcher( value() ); // TODO: can we do better here? } - private int codePointAt( byte[] bytes, int i ) + private static int codePointAt( byte[] bytes, int i ) { assert i < bytes.length; byte b = bytes[i]; @@ -397,7 +412,7 @@ private int codePointAt( byte[] bytes, int i ) ((bytes[i + 2] & HIGH_BIT_MASK) << 6) | (bytes[i + 3] & HIGH_BIT_MASK); default: - throw new IllegalArgumentException( "Malformed UTF8 value" ); + throw new IllegalArgumentException( "Malformed UTF8 value " + bytesNeeded ); } } @@ -434,7 +449,7 @@ private int trimLeftIndex() bytesNeeded++; b = (byte) (b << 1); } - int codePoint = codePoint( b, i, bytesNeeded ); + int codePoint = codePoint( bytes, b, i, bytesNeeded ); if ( !Character.isWhitespace( codePoint ) ) { return i; @@ -477,7 +492,7 @@ private int trimRightIndex() b = bytes[--index]; } - int codePoint = codePoint( (byte) (b << bytesNeeded), index, bytesNeeded ); + int codePoint = codePoint( bytes, (byte) (b << bytesNeeded), index, bytesNeeded ); if ( !Character.isWhitespace( codePoint ) ) { return Math.min( index + bytesNeeded - 1, bytes.length - 1 ); @@ -493,7 +508,7 @@ public byte[] bytes() return bytes; } - private int codePoint( byte currentByte, int i, int bytesNeeded ) + private static int codePoint( byte[] bytes, byte currentByte, int i, int bytesNeeded ) { int codePoint; byte[] values = bytes; diff --git a/community/values/src/test/java/org/neo4j/values/storable/StringsLibrary.java b/community/values/src/test/java/org/neo4j/values/storable/StringsLibrary.java new file mode 100644 index 0000000000000..fdb570a8d3fb4 --- /dev/null +++ b/community/values/src/test/java/org/neo4j/values/storable/StringsLibrary.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2002-2018 "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.values.storable; + +/** + * Contains a collection of strings that needs to be supported throughout the product. + */ +public class StringsLibrary +{ + public static String[] STRINGS = { + "", + "1337", + " ", + "普通�?/普通話", + "\uD83D\uDE21", + "\uD83D\uDE21\uD83D\uDCA9\uD83D\uDC7B", + " a b c ", + "䤹᳽", + "熨", + "ۼ", + "ⲹ楡�?톜ഷۢ⼈�?�늉�?�₭샺ጚ砧攡跿家䯶�?⬖�?�犽ۼ", + " 㺂�?鋦毠",//first character is a thin space, + "\u0018", + ";먵�?裬岰鷲趫\uA8C5얱㓙髿ᚳᬼ≩�?� ", + "\u001cӳ", + "abcdefghijklmnopqrstuvwxyzåäöABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖ 1234567890-´!\"@#$%^&*()_+", + // TODO longer lorem ipsum string? + }; +} diff --git a/community/values/src/test/java/org/neo4j/values/storable/UTF8StringValueTest.java b/community/values/src/test/java/org/neo4j/values/storable/UTF8StringValueTest.java index 3cef16bb42432..ab1ab497a9494 100644 --- a/community/values/src/test/java/org/neo4j/values/storable/UTF8StringValueTest.java +++ b/community/values/src/test/java/org/neo4j/values/storable/UTF8StringValueTest.java @@ -27,6 +27,8 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; + +import static org.neo4j.values.storable.StringsLibrary.STRINGS; import static org.neo4j.values.storable.Values.stringValue; import static org.neo4j.values.storable.Values.utf8Value; import static org.neo4j.values.utils.AnyValueTestUtil.assertEqual; @@ -36,17 +38,10 @@ public class UTF8StringValueTest @Rule public ExpectedException exception = ExpectedException.none(); - private String[] strings = {"", "1337", " ", "普通话/普通話", "\uD83D\uDE21", " a b c ", "䤹᳽", "熨", "ۼ", - "ⲹ楡톜ഷۢ⼈늉₭샺ጚ砧攡跿家䯶鲏⬖돛犽ۼ", - " 㺂࿝鋦毠",//first character is a thin space, - "\u0018", ";먵熍裬岰鷲趫\uA8C5얱㓙髿ᚳᬼ≩萌 ", "\u001cӳ", - "\uD83D\uDE21\uD83D\uDCA9\uD83D\uDC7B" - }; - @Test public void shouldHandleDifferentTypesOfStrings() { - for ( String string : strings ) + for ( String string : STRINGS ) { TextValue stringValue = stringValue( string ); byte[] bytes = string.getBytes( UTF_8 ); @@ -59,7 +54,7 @@ public void shouldHandleDifferentTypesOfStrings() @Test public void shouldTrimDifferentTypesOfStrings() { - for ( String string : strings ) + for ( String string : STRINGS ) { TextValue stringValue = stringValue( string ); byte[] bytes = string.getBytes( UTF_8 ); @@ -71,7 +66,7 @@ public void shouldTrimDifferentTypesOfStrings() @Test public void shouldLTrimDifferentTypesOfStrings() { - for ( String string : strings ) + for ( String string : STRINGS ) { TextValue stringValue = stringValue( string ); byte[] bytes = string.getBytes( UTF_8 ); @@ -83,7 +78,7 @@ public void shouldLTrimDifferentTypesOfStrings() @Test public void trimShouldBeSameAsLtrimAndRtrim() { - for ( String string : strings ) + for ( String string : STRINGS ) { TextValue utf8 = utf8Value( string.getBytes( UTF_8 ) ); assertSame( utf8.trim(), utf8.ltrim().rtrim() ); @@ -101,7 +96,7 @@ public void shouldSubstring() @Test public void shouldRTrimDifferentTypesOfStrings() { - for ( String string : strings ) + for ( String string : STRINGS ) { TextValue stringValue = stringValue( string ); byte[] bytes = string.getBytes( UTF_8 ); @@ -113,9 +108,9 @@ public void shouldRTrimDifferentTypesOfStrings() @Test public void shouldCompareTo() { - for ( String string1 : strings ) + for ( String string1 : STRINGS ) { - for ( String string2 : strings ) + for ( String string2 : STRINGS ) { int x = stringValue( string1 ).compareTo( utf8Value( string2.getBytes( UTF_8 ) ) ); @@ -132,7 +127,7 @@ public void shouldCompareTo() @Test public void shouldReverse() { - for ( String string : strings ) + for ( String string : STRINGS ) { TextValue stringValue = stringValue( string ); byte[] bytes = string.getBytes( UTF_8 );