diff --git a/community/index/src/test/java/org/neo4j/index/internal/gbptree/FormatCompatibilityTest.java b/community/index/src/test/java/org/neo4j/index/internal/gbptree/FormatCompatibilityTest.java deleted file mode 100644 index d06edef0de370..0000000000000 --- a/community/index/src/test/java/org/neo4j/index/internal/gbptree/FormatCompatibilityTest.java +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright (c) 2002-2018 "Neo4j," - * Neo4j Sweden AB [http://neo4j.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.index.internal.gbptree; - -import org.apache.commons.lang3.mutable.MutableLong; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.RuleChain; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.URL; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import org.neo4j.cursor.RawCursor; -import org.neo4j.io.compress.ZipUtils; -import org.neo4j.io.pagecache.IOLimiter; -import org.neo4j.io.pagecache.PageCache; -import org.neo4j.test.rule.PageCacheRule; -import org.neo4j.test.rule.RandomRule; -import org.neo4j.test.rule.TestDirectory; -import org.neo4j.test.rule.fs.DefaultFileSystemRule; - -import static java.lang.String.format; -import static java.util.Arrays.asList; -import static org.hamcrest.CoreMatchers.containsString; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.neo4j.index.internal.gbptree.SimpleLongLayout.longLayout; - -/** - * A little trick to automatically tell whether or not index format was changed without - * incrementing the format version. This is done by keeping a zipped tree which is opened and tested on. - * On failure this test will fail saying that the format version needs update and also update the zipped - * store with the new version. - */ -@RunWith( Parameterized.class ) -public class FormatCompatibilityTest -{ - private static final String STORE = "store"; - private static final int INITIAL_KEY_COUNT = 10_000; - private static final String CURRENT_FIXED_SIZE_FORMAT_ZIP = "current-format.zip"; - private static final String CURRENT_DYNAMIC_SIZE_FORMAT_ZIP = "current-dynamic-format.zip"; - - @Parameters - public static List data() - { - return asList( - new Object[] {longLayout().withFixedSize( true ).build(), CURRENT_FIXED_SIZE_FORMAT_ZIP}, - new Object[] {longLayout().withFixedSize( false ).build(), CURRENT_DYNAMIC_SIZE_FORMAT_ZIP} ); - } - - @Parameter - public SimpleLongLayout layout; - @Parameter( 1 ) - public String zipName; - - private final TestDirectory directory = TestDirectory.testDirectory(); - private final PageCacheRule pageCacheRule = new PageCacheRule(); - private final DefaultFileSystemRule fsRule = new DefaultFileSystemRule(); - private final RandomRule random = new RandomRule(); - - @Rule - public final RuleChain chain = RuleChain.outerRule( random ).around( fsRule ).around( directory ).around( pageCacheRule ); - - @Test - public void shouldDetectFormatChange() throws Throwable - { - List initialKeys = initialKeys(); - List keysToAdd = keysToAdd(); - List allKeys = new ArrayList<>(); - allKeys.addAll( initialKeys ); - allKeys.addAll( keysToAdd ); - allKeys.sort( Long::compare ); - - // GIVEN stored tree - File storeFile = directory.file( STORE ); - try - { - unzipTo( storeFile ); - } - catch ( FileNotFoundException e ) - { - // First time this test is run, eh? - createAndZipTree( storeFile ); - tellDeveloperToCommitThisFormatVersion(); - } - assertTrue( zipName + " seems to be missing from resources directory", fsRule.get().fileExists( storeFile ) ); - - PageCache pageCache = pageCacheRule.getPageCache( fsRule.get() ); - try ( GBPTree tree = - new GBPTreeBuilder<>( pageCache, storeFile, layout ).build() ) - { - try - { - { - // WHEN reading from the tree - // THEN initial keys should be there - tree.consistencyCheck(); - try ( RawCursor,IOException> cursor = - tree.seek( layout.key( 0 ), layout.key( Long.MAX_VALUE ) ) ) - { - for ( Long expectedKey : initialKeys ) - { - assertHit( cursor, expectedKey ); - } - assertFalse( cursor.next() ); - } - } - - { - // WHEN writing more to the tree - // THEN we should not see any format conflicts - try ( Writer writer = tree.writer() ) - { - while ( keysToAdd.size() > 0 ) - { - int next = random.nextInt( keysToAdd.size() ); - put( writer, keysToAdd.get( next ) ); - keysToAdd.remove( next ); - } - } - } - - { - // WHEN reading from the tree again - // THEN all keys including newly added should be there - tree.consistencyCheck(); - try ( RawCursor,IOException> cursor = - tree.seek( layout.key( 0 ), layout.key( 2 * INITIAL_KEY_COUNT ) ) ) - { - for ( Long expectedKey : allKeys ) - { - assertHit( cursor, expectedKey ); - } - assertFalse( cursor.next() ); - } - } - - { - // WHEN randomly removing half of tree content - // THEN we should not see any format conflicts - try ( Writer writer = tree.writer() ) - { - int size = allKeys.size(); - while ( allKeys.size() > size / 2 ) - { - int next = random.nextInt( allKeys.size() ); - MutableLong key = layout.key( allKeys.get( next ) ); - writer.remove( key ); - allKeys.remove( next ); - } - } - } - - { - // WHEN reading from the tree after remove - // THEN we should see everything that is left in the tree - tree.consistencyCheck(); - try ( RawCursor,IOException> cursor = - tree.seek( layout.key( 0 ), layout.key( 2 * INITIAL_KEY_COUNT ) ) ) - { - for ( Long expectedKey : allKeys ) - { - assertHit( cursor, expectedKey ); - } - assertFalse( cursor.next() ); - } - } - } - catch ( Throwable t ) - { - throw new AssertionError( "If this is the single failing test in this component then this failure is a strong indication that format " + - "has changed without also incrementing TreeNode version(s). Please make necessary format version changes.", t ); - } - } - catch ( MetadataMismatchException e ) - { - // Good actually, or? - assertThat( e.getMessage(), containsString( "format version" ) ); - - fsRule.get().deleteFile( storeFile ); - createAndZipTree( storeFile ); - - tellDeveloperToCommitThisFormatVersion(); - } - } - - private void tellDeveloperToCommitThisFormatVersion() - { - fail( format( "This is merely a notification to developer. Format has changed and its version has also " + - "been properly incremented. A tree with this new format has been generated and should be committed. " + - "Please move:%n %s%ninto %n %s, %nreplacing the existing file there", - directory.file( zipName ), - "" + pathify( ".src.test.resources." ) + - pathify( getClass().getPackage().getName() + "." ) + zipName ) ); - } - - private static String pathify( String name ) - { - return name.replace( '.', File.separatorChar ); - } - - private void unzipTo( File storeFile ) throws IOException - { - URL resource = getClass().getResource( zipName ); - if ( resource == null ) - { - throw new FileNotFoundException(); - } - - try ( ZipFile zipFile = new ZipFile( resource.getFile() ) ) - { - Enumeration entries = zipFile.entries(); - assertTrue( entries.hasMoreElements() ); - ZipEntry entry = entries.nextElement(); - assertEquals( STORE, entry.getName() ); - Files.copy( zipFile.getInputStream( entry ), storeFile.toPath() ); - } - } - - private void createAndZipTree( File storeFile ) throws IOException - { - List initialKeys = initialKeys(); - PageCache pageCache = pageCacheRule.getPageCache( fsRule.get() ); - try ( GBPTree tree = - new GBPTreeBuilder<>( pageCache, storeFile, layout ).build() ) - { - try ( Writer writer = tree.writer() ) - { - for ( Long key : initialKeys ) - { - put( writer, key ); - } - } - tree.checkpoint( IOLimiter.UNLIMITED ); - } - ZipUtils.zip( fsRule.get(), storeFile, directory.file( zipName ) ); - } - - private static long value( long key ) - { - return key * 2; - } - - private List initialKeys() - { - List initialKeys = new ArrayList<>(); - for ( long i = 0, key = 0; i < INITIAL_KEY_COUNT; i++, key += 2 ) - { - initialKeys.add( key ); - } - return initialKeys; - } - - private List keysToAdd() - { - List keysToAdd = new ArrayList<>(); - for ( long i = 0, key = 1; i < INITIAL_KEY_COUNT; i++, key += 2 ) - { - keysToAdd.add( key ); - } - return keysToAdd; - } - - private void assertHit( RawCursor,IOException> cursor, Long expectedKey ) throws IOException - { - assert cursor.next() : "Had no next when expecting key " + expectedKey; - Hit hit = cursor.get(); - assertEquals( expectedKey.longValue(), hit.key().longValue() ); - assertEquals( value( expectedKey ), hit.value().longValue() ); - } - - private void put( Writer writer, long key ) throws IOException - { - MutableLong insertKey = layout.key( key ); - MutableLong insertValue = layout.value( value( key ) ); - writer.put( insertKey, insertValue ); - } -} diff --git a/community/index/src/test/java/org/neo4j/index/internal/gbptree/GBPTreeFormatTest.java b/community/index/src/test/java/org/neo4j/index/internal/gbptree/GBPTreeFormatTest.java new file mode 100644 index 0000000000000..a11ad912f1b42 --- /dev/null +++ b/community/index/src/test/java/org/neo4j/index/internal/gbptree/GBPTreeFormatTest.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2002-2018 "Neo4j," + * Neo4j Sweden AB [http://neo4j.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.index.internal.gbptree; + +import org.apache.commons.lang3.mutable.MutableLong; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.RuleChain; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.neo4j.cursor.RawCursor; +import org.neo4j.io.pagecache.IOLimiter; +import org.neo4j.io.pagecache.PageCache; +import org.neo4j.test.FormatCompatibilityVerifier; +import org.neo4j.test.rule.PageCacheRule; +import org.neo4j.test.rule.RandomRule; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.neo4j.index.internal.gbptree.SimpleLongLayout.longLayout; + +@RunWith( Parameterized.class ) +public class GBPTreeFormatTest extends FormatCompatibilityVerifier +{ + private static final String STORE = "store"; + private static final int INITIAL_KEY_COUNT = 10_000; + private static final String CURRENT_FIXED_SIZE_FORMAT_ZIP = "current-format.zip"; + private static final String CURRENT_DYNAMIC_SIZE_FORMAT_ZIP = "current-dynamic-format.zip"; + + @Parameters + public static List data() + { + return asList( + new Object[] {longLayout().withFixedSize( true ).build(), CURRENT_FIXED_SIZE_FORMAT_ZIP}, + new Object[] {longLayout().withFixedSize( false ).build(), CURRENT_DYNAMIC_SIZE_FORMAT_ZIP} ); + } + + @Parameter + public SimpleLongLayout layout; + @Parameter( 1 ) + public String zipName; + + private final PageCacheRule pageCacheRule = new PageCacheRule(); + private final RandomRule random = new RandomRule(); + private final List initialKeys = initialKeys(); + private final List keysToAdd = keysToAdd(); + private List allKeys; + + @Before + public void setup() + { + allKeys = new ArrayList<>(); + allKeys.addAll( initialKeys ); + allKeys.addAll( keysToAdd ); + allKeys.sort( Long::compare ); + } + + @Rule + public final RuleChain chain = RuleChain.outerRule( random ).around( pageCacheRule ); + + @Override + protected String zipName() + { + return zipName; + } + + @Override + protected String storeFileName() + { + return STORE; + } + + @Override + protected void createStoreFile( File storeFile ) throws IOException + { + List initialKeys = initialKeys(); + PageCache pageCache = pageCacheRule.getPageCache( globalFs.get() ); + try ( GBPTree tree = + new GBPTreeBuilder<>( pageCache, storeFile, layout ).build() ) + { + try ( Writer writer = tree.writer() ) + { + for ( Long key : initialKeys ) + { + put( writer, key ); + } + } + tree.checkpoint( IOLimiter.UNLIMITED ); + } + } + + /** + * Throws {@link FormatViolationException} if format has changed. + */ + @SuppressWarnings( "EmptyTryBlock" ) + @Override + protected void verifyFormat( File storeFile ) throws IOException, FormatViolationException + { + PageCache pageCache = pageCacheRule.getPageCache( globalFs.get() ); + try ( GBPTree ignored = + new GBPTreeBuilder<>( pageCache, storeFile, layout ).build() ) + { + } + catch ( MetadataMismatchException e ) + { + throw new FormatViolationException( e ); + } + } + + @Override + public void verifyContent( File storeFile ) throws IOException + { + PageCache pageCache = pageCacheRule.getPageCache( globalFs.get() ); + try ( GBPTree tree = + new GBPTreeBuilder<>( pageCache, storeFile, layout ).build() ) + { + { + // WHEN reading from the tree + // THEN initial keys should be there + tree.consistencyCheck(); + try ( RawCursor,IOException> cursor = + tree.seek( layout.key( 0 ), layout.key( Long.MAX_VALUE ) ) ) + { + for ( Long expectedKey : initialKeys ) + { + assertHit( cursor, expectedKey ); + } + assertFalse( cursor.next() ); + } + } + + { + // WHEN writing more to the tree + // THEN we should not see any format conflicts + try ( Writer writer = tree.writer() ) + { + while ( keysToAdd.size() > 0 ) + { + int next = random.nextInt( keysToAdd.size() ); + put( writer, keysToAdd.get( next ) ); + keysToAdd.remove( next ); + } + } + } + + { + // WHEN reading from the tree again + // THEN all keys including newly added should be there + tree.consistencyCheck(); + try ( RawCursor,IOException> cursor = + tree.seek( layout.key( 0 ), layout.key( 2 * INITIAL_KEY_COUNT ) ) ) + { + for ( Long expectedKey : allKeys ) + { + assertHit( cursor, expectedKey ); + } + assertFalse( cursor.next() ); + } + } + + { + // WHEN randomly removing half of tree content + // THEN we should not see any format conflicts + try ( Writer writer = tree.writer() ) + { + int size = allKeys.size(); + while ( allKeys.size() > size / 2 ) + { + int next = random.nextInt( allKeys.size() ); + MutableLong key = layout.key( allKeys.get( next ) ); + writer.remove( key ); + allKeys.remove( next ); + } + } + } + + { + // WHEN reading from the tree after remove + // THEN we should see everything that is left in the tree + tree.consistencyCheck(); + try ( RawCursor,IOException> cursor = + tree.seek( layout.key( 0 ), layout.key( 2 * INITIAL_KEY_COUNT ) ) ) + { + for ( Long expectedKey : allKeys ) + { + assertHit( cursor, expectedKey ); + } + assertFalse( cursor.next() ); + } + } + } + } + + private static long value( long key ) + { + return key * 2; + } + + private static List initialKeys() + { + List initialKeys = new ArrayList<>(); + for ( long i = 0, key = 0; i < INITIAL_KEY_COUNT; i++, key += 2 ) + { + initialKeys.add( key ); + } + return initialKeys; + } + + private static List keysToAdd() + { + List keysToAdd = new ArrayList<>(); + for ( long i = 0, key = 1; i < INITIAL_KEY_COUNT; i++, key += 2 ) + { + keysToAdd.add( key ); + } + return keysToAdd; + } + + private static void assertHit( RawCursor,IOException> cursor, Long expectedKey ) throws IOException + { + assert cursor.next() : "Had no next when expecting key " + expectedKey; + Hit hit = cursor.get(); + assertEquals( expectedKey.longValue(), hit.key().longValue() ); + assertEquals( value( expectedKey ), hit.value().longValue() ); + } + + private void put( Writer writer, long key ) + { + MutableLong insertKey = layout.key( key ); + MutableLong insertValue = layout.value( value( key ) ); + writer.put( insertKey, insertValue ); + } +} diff --git a/community/io/src/test/java/org/neo4j/test/FormatCompatibilityVerifier.java b/community/io/src/test/java/org/neo4j/test/FormatCompatibilityVerifier.java new file mode 100644 index 0000000000000..5cca6f37e20fd --- /dev/null +++ b/community/io/src/test/java/org/neo4j/test/FormatCompatibilityVerifier.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2002-2018 "Neo4j," + * Neo4j Sweden AB [http://neo4j.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.test; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import org.neo4j.io.compress.ZipUtils; +import org.neo4j.test.rule.TestDirectory; +import org.neo4j.test.rule.fs.DefaultFileSystemRule; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * A little trick to automatically tell whether or not index format was changed without + * incrementing the format version. This is done by keeping a zipped store file which is opened and tested on. + * On failure this test will fail saying that the format version needs update and also update the zipped + * store with the new version. + */ +public abstract class FormatCompatibilityVerifier +{ + private final TestDirectory globalDir = TestDirectory.testDirectory(); + protected final DefaultFileSystemRule globalFs = new DefaultFileSystemRule(); + + @Rule + public final RuleChain globalRuleChain = RuleChain.outerRule( globalFs ).around( globalDir ); + + @Test + public void shouldDetectFormatChange() throws Throwable + { + File storeFile = globalDir.file( storeFileName() ); + doShouldDetectFormatChange( zipName(), storeFile ); + } + + protected abstract String zipName(); + + protected abstract String storeFileName(); + + protected abstract void createStoreFile( File storeFile ) throws IOException; + + protected abstract void verifyFormat( File storeFile ) throws IOException, FormatViolationException; + + protected abstract void verifyContent( File storeFile ) throws IOException; + + private void doShouldDetectFormatChange( String zipName, File storeFile ) throws Throwable + { + try + { + unzip( zipName, storeFile ); + } + catch ( FileNotFoundException e ) + { + // First time this test is run, eh? + createStoreFile( storeFile ); + ZipUtils.zip( globalFs.get(), storeFile, globalDir.file( zipName ) ); + tellDeveloperToCommitThisFormatVersion( zipName ); + } + assertTrue( zipName + " seems to be missing from resources directory", globalFs.get().fileExists( storeFile ) ); + + // Verify format + try + { + verifyFormat( storeFile ); + } + catch ( FormatViolationException e ) + { + // Good actually, or? + assertThat( e.getMessage(), containsString( "format version" ) ); + + globalFs.get().deleteFile( storeFile ); + createStoreFile( storeFile ); + ZipUtils.zip( globalFs.get(), storeFile, globalDir.file( zipName ) ); + + tellDeveloperToCommitThisFormatVersion( zipName ); + } + + // Verify content + try + { + verifyContent( storeFile ); + } + catch ( Throwable t ) + { + throw new AssertionError( "If this is the single failing test in this component then this failure is a strong indication that format " + + "has changed without also incrementing TreeNode version(s). Please make necessary format version changes.", t ); + } + } + + private void tellDeveloperToCommitThisFormatVersion( String zipName ) + { + fail( String.format( "This is merely a notification to developer. Format has changed and its version has also " + + "been properly incremented. A tree with this new format has been generated and should be committed. " + + "Please move:%n %s%ninto %n %s, %nreplacing the existing file there", + globalDir.file( zipName ), + "" + pathify( ".src.test.resources." ) + + pathify( getClass().getPackage().getName() + "." ) + zipName ) ); + } + + private void unzip( String zipName, File storeFile ) throws IOException + { + URL resource = getClass().getResource( zipName ); + if ( resource == null ) + { + throw new FileNotFoundException(); + } + + try ( ZipFile zipFile = new ZipFile( resource.getFile() ) ) + { + Enumeration entries = zipFile.entries(); + assertTrue( entries.hasMoreElements() ); + ZipEntry entry = entries.nextElement(); + assertEquals( storeFile.getName(), entry.getName() ); + Files.copy( zipFile.getInputStream( entry ), storeFile.toPath() ); + } + } + + private static String pathify( String name ) + { + return name.replace( '.', File.separatorChar ); + } + + public class FormatViolationException extends Throwable + { + public FormatViolationException( Throwable cause ) + { + super( cause ); + } + + public FormatViolationException( String message ) + { + super( message ); + } + } +} diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/GenericKeyStateFormatTest.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/GenericKeyStateFormatTest.java new file mode 100644 index 0000000000000..c4decda34edd8 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/GenericKeyStateFormatTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2002-2018 "Neo4j," + * Neo4j Sweden AB [http://neo4j.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.junit.Before; +import org.junit.Rule; + +import java.io.File; +import java.io.IOException; +import java.nio.file.OpenOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import org.neo4j.io.pagecache.PageCache; +import org.neo4j.io.pagecache.PageCursor; +import org.neo4j.io.pagecache.PagedFile; +import org.neo4j.test.FormatCompatibilityVerifier; +import org.neo4j.test.rule.PageCacheRule; +import org.neo4j.test.rule.RandomRule; +import org.neo4j.values.storable.RandomValues; +import org.neo4j.values.storable.Value; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class GenericKeyStateFormatTest extends FormatCompatibilityVerifier +{ + @Rule + public PageCacheRule pageCacheRule = new PageCacheRule(); + + @Rule + public RandomRule randomRule = new RandomRule().withSeedForAllTests( 20051116 ); + + private static final int NUMBER_OF_SLOTS = 2; + private List values; + + @Before + public void setup() + { + RandomValues rnd = randomRule.randomValues(); + values = new ArrayList<>(); + // ZONED_DATE_TIME_ARRAY + values.add( rnd.nextDateTimeArray() ); + // LOCAL_DATE_TIME_ARRAY + values.add( rnd.nextLocalDateTimeArray() ); + // DATE_ARRAY + values.add( rnd.nextDateArray() ); + // ZONED_TIME_ARRAY + values.add( rnd.nextTimeArray() ); + // LOCAL_TIME_ARRAY + values.add( rnd.nextLocalTimeArray() ); + // DURATION_ARRAY + values.add( rnd.nextDurationArray() ); + // TEXT_ARRAY + values.add( rnd.nextStringArray() ); + // BOOLEAN_ARRAY + values.add( rnd.nextBooleanArray() ); + // NUMBER_ARRAY (byte, short, int, long, float, double) + values.add( rnd.nextByteArray() ); + values.add( rnd.nextShortArray() ); + values.add( rnd.nextIntArray() ); + values.add( rnd.nextLongArray() ); + values.add( rnd.nextFloatArray() ); + values.add( rnd.nextDoubleArray() ); + // ZONED_DATE_TIME + values.add( rnd.nextDateTimeValue() ); + // LOCAL_DATE_TIME + values.add( rnd.nextLocalDateTimeValue() ); + // DATE + values.add( rnd.nextDateValue() ); + // ZONED_TIME + values.add( rnd.nextTimeValue() ); + // LOCAL_TIME + values.add( rnd.nextLocalTimeValue() ); + // DURATION + values.add( rnd.nextDuration() ); + // TEXT + values.add( rnd.nextTextValue() ); + // BOOLEAN + values.add( rnd.nextBooleanValue() ); + // NUMBER (byte, short, int, long, float, double) + values.add( rnd.nextByteValue() ); + values.add( rnd.nextShortValue() ); + values.add( rnd.nextIntValue() ); + values.add( rnd.nextLongValue() ); + values.add( rnd.nextFloatValue() ); + values.add( rnd.nextDoubleValue() ); + // todo GEOMETRY + // todo GEOMETRY_ARRAY + } + + @Override + protected String zipName() + { + return "current-generic-key-state-format.zip"; + } + + @Override + protected String storeFileName() + { + return "generic-key-state-store"; + } + + @Override + protected void createStoreFile( File storeFile ) throws IOException + { + withCursor( storeFile, true, c -> { + putFormatVersion( c ); + putData( c ); + } ); + } + + @Override + protected void verifyFormat( File storeFile ) throws FormatViolationException, IOException + { + AtomicReference exception = new AtomicReference<>(); + withCursor( storeFile, false, c -> + { + int major = c.getInt(); + int minor = c.getInt(); + GenericLayout layout = getLayout(); + if ( major != layout.majorVersion() || minor != layout.minorVersion() ) + { + exception.set( new FormatViolationException( String.format( "Read format version %d.%d, but layout has version %d.%d", + major, minor, layout.majorVersion(), layout.minorVersion() ) ) ); + } + } ); + if ( exception.get() != null ) + { + throw exception.get(); + } + } + + @Override + protected void verifyContent( File storeFile ) throws IOException + { + withCursor( storeFile, false, c -> + { + readFormatVersion( c ); + verifyData( c ); + } ); + } + + private void putFormatVersion( PageCursor cursor ) + { + GenericLayout layout = getLayout(); + int major = layout.majorVersion(); + cursor.putInt( major ); + int minor = layout.minorVersion(); + cursor.putInt( minor ); + } + + private void readFormatVersion( PageCursor c ) + { + c.getInt(); // Major version + c.getInt(); // Minor version + } + + private void putData( PageCursor c ) + { + GenericLayout layout = getLayout(); + CompositeGenericKey key = layout.newKey(); + for ( Value value : values ) + { + key.initialize( 19570320 ); + for ( int i = 0; i < NUMBER_OF_SLOTS; i++ ) + { + key.initFromValue( i, value, NativeIndexKey.Inclusion.NEUTRAL ); + } + c.putInt( key.size() ); + layout.writeKey( c, key ); + } + } + + private void verifyData( PageCursor c ) + { + GenericLayout layout = getLayout(); + CompositeGenericKey into = layout.newKey(); + for ( Value value : values ) + { + int keySize = c.getInt(); + layout.readKey( c, into, keySize ); + for ( Value readValue : into.asValues() ) + { + assertEquals( value, readValue, "expected read value to be " + value + ", but was " + readValue ); + } + } + } + + private GenericLayout getLayout() + { + return new GenericLayout( NUMBER_OF_SLOTS ); + } + + private void withCursor( File storeFile, boolean create, Consumer cursorConsumer ) throws IOException + { + OpenOption[] openOptions = create ? + new OpenOption[]{StandardOpenOption.WRITE, StandardOpenOption.CREATE} : + new OpenOption[]{StandardOpenOption.WRITE}; + try ( PageCache pageCache = pageCacheRule.getPageCache( globalFs.get() ); + PagedFile pagedFile = pageCache.map( storeFile, pageCache.pageSize(), openOptions ); + PageCursor cursor = pagedFile.io( 0, PagedFile.PF_SHARED_WRITE_LOCK ) ) + { + cursor.next(); + cursorConsumer.accept( cursor ); + } + } +} diff --git a/community/kernel/src/test/resources/org/neo4j/kernel/impl/index/schema/current-generic-key-state-format.zip b/community/kernel/src/test/resources/org/neo4j/kernel/impl/index/schema/current-generic-key-state-format.zip new file mode 100644 index 0000000000000..ee6abe214da70 Binary files /dev/null and b/community/kernel/src/test/resources/org/neo4j/kernel/impl/index/schema/current-generic-key-state-format.zip differ