diff --git a/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/config/SpatialIndexValueTestUtil.java b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/config/SpatialIndexValueTestUtil.java new file mode 100644 index 0000000000000..799f58a566924 --- /dev/null +++ b/community/kernel/src/test/java/org/neo4j/kernel/impl/index/schema/config/SpatialIndexValueTestUtil.java @@ -0,0 +1,48 @@ +/* + * 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.config; + +import org.neo4j.gis.spatial.index.curves.SpaceFillingCurve; +import org.neo4j.helpers.collection.Pair; +import org.neo4j.kernel.configuration.Config; +import org.neo4j.values.storable.CoordinateReferenceSystem; +import org.neo4j.values.storable.PointValue; +import org.neo4j.values.storable.Values; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; + +public class SpatialIndexValueTestUtil +{ + public static Pair pointsWithSameValueOnSpaceFillingCurve( Config config ) + { + SpaceFillingCurveSettingsFactory spaceFillingCurveSettingsFactory = new SpaceFillingCurveSettingsFactory( config ); + SpaceFillingCurveSettings spaceFillingCurveSettings = spaceFillingCurveSettingsFactory.settingsFor( CoordinateReferenceSystem.WGS84 ); + SpaceFillingCurve curve = spaceFillingCurveSettings.curve(); + double[] origin = {0.0, 0.0}; + Long spaceFillingCurveMapForOrigin = curve.derivedValueFor( origin ); + double[] centerPointForOriginTile = curve.centerPointFor( spaceFillingCurveMapForOrigin ); + PointValue originValue = Values.pointValue( CoordinateReferenceSystem.WGS84, origin ); + PointValue centerPointValue = Values.pointValue( CoordinateReferenceSystem.WGS84, centerPointForOriginTile ); + assertThat( "need non equal points for this test", origin, not( equalTo( centerPointValue ) ) ); + return Pair.of( originValue, centerPointValue ); + } +} diff --git a/community/lucene-index/src/test/java/org/neo4j/kernel/api/impl/schema/UniqueSpatialIndexIT.java b/community/lucene-index/src/test/java/org/neo4j/kernel/api/impl/schema/UniqueSpatialIndexIT.java new file mode 100644 index 0000000000000..f9bfa9a64d965 --- /dev/null +++ b/community/lucene-index/src/test/java/org/neo4j/kernel/api/impl/schema/UniqueSpatialIndexIT.java @@ -0,0 +1,220 @@ +/* + * 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.api.impl.schema; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.concurrent.TimeUnit; + +import org.neo4j.graphdb.ConstraintViolationException; +import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.ResourceIterator; +import org.neo4j.graphdb.Transaction; +import org.neo4j.graphdb.factory.GraphDatabaseBuilder; +import org.neo4j.graphdb.factory.GraphDatabaseSettings; +import org.neo4j.helpers.collection.Pair; +import org.neo4j.kernel.configuration.Config; +import org.neo4j.kernel.impl.index.schema.config.SpatialIndexValueTestUtil; +import org.neo4j.test.TestGraphDatabaseFactory; +import org.neo4j.test.TestLabels; +import org.neo4j.test.rule.TestDirectory; +import org.neo4j.values.storable.PointValue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith( Parameterized.class ) +public class UniqueSpatialIndexIT +{ + private static final String KEY = "prop"; + private static final TestLabels LABEL = TestLabels.LABEL_ONE; + + @Parameterized.Parameters( name = "{0}" ) + public static GraphDatabaseSettings.SchemaIndex[] schemaIndexes() + { + return GraphDatabaseSettings.SchemaIndex.values(); + } + + @Parameterized.Parameter( 0 ) + public GraphDatabaseSettings.SchemaIndex schemaIndex; + + @Rule + public TestDirectory directory = TestDirectory.testDirectory(); + private GraphDatabaseService db; + private PointValue point1; + private PointValue point2; + + @Before + public void setup() + { + Pair collidingPoints = SpatialIndexValueTestUtil.pointsWithSameValueOnSpaceFillingCurve( Config.defaults() ); + point1 = collidingPoints.first(); + point2 = collidingPoints.other(); + } + + @After + public void tearDown() + { + if ( db != null ) + { + db.shutdown(); + } + } + + @Test + public void shouldPopulateIndexWithUniquePointsThatCollideOnSpaceFillingCurve() + { + // given + setupDb( schemaIndex ); + Pair nodeIds = createUniqueNodes(); + + // when + createUniquenessConstraint(); + + // then + assertBothNodesArePresent( nodeIds ); + } + + @Test + public void shouldAddPointsThatCollideOnSpaceFillingCurveToUniqueIndexInSameTx() + { + // given + setupDb( schemaIndex ); + createUniquenessConstraint(); + + // when + Pair nodeIds = createUniqueNodes(); + + // then + assertBothNodesArePresent( nodeIds ); + } + + @Test + public void shouldThrowWhenPopulatingWithNonUniquePoints() + { + // given + setupDb( schemaIndex ); + createNonUniqueNodes(); + + // then + try + { + createUniquenessConstraint(); + fail( "Should have failed" ); + } + catch ( ConstraintViolationException e ) + { // good + } + } + + @Test + public void shouldThrowWhenAddingNonUniquePoints() + { + // given + setupDb( schemaIndex ); + createUniquenessConstraint(); + + // when + try + { + createNonUniqueNodes(); + fail( "Should have failed" ); + } + catch ( ConstraintViolationException e ) + { // good + } + } + + private void createNonUniqueNodes() + { + try ( Transaction tx = db.beginTx() ) + { + Node originNode = db.createNode( LABEL ); + originNode.setProperty( KEY, point1 ); + Node centerNode = db.createNode( LABEL ); + centerNode.setProperty( KEY, point1 ); + tx.success(); + } + } + + private Pair createUniqueNodes() + { + Pair nodeIds; + try ( Transaction tx = db.beginTx() ) + { + Node originNode = db.createNode( LABEL ); + originNode.setProperty( KEY, point1 ); + Node centerNode = db.createNode( LABEL ); + centerNode.setProperty( KEY, point2 ); + + nodeIds = Pair.of( originNode.getId(), centerNode.getId() ); + tx.success(); + } + return nodeIds; + } + + private void assertBothNodesArePresent( Pair nodeIds ) + { + try ( Transaction tx = db.beginTx() ) + { + ResourceIterator origin = db.findNodes( LABEL, KEY, point1 ); + assertTrue( origin.hasNext() ); + assertEquals( nodeIds.first().longValue(), origin.next().getId() ); + assertFalse( origin.hasNext() ); + + ResourceIterator center = db.findNodes( LABEL, KEY, point2 ); + assertTrue( center.hasNext() ); + assertEquals( nodeIds.other().longValue(), center.next().getId() ); + assertFalse( center.hasNext() ); + + tx.success(); + } + } + + private void createUniquenessConstraint() + { + try ( Transaction tx = db.beginTx() ) + { + db.schema().constraintFor( TestLabels.LABEL_ONE ).assertPropertyIsUnique( KEY ).create(); + tx.success(); + } + try ( Transaction tx = db.beginTx() ) + { + db.schema().awaitIndexesOnline( 1, TimeUnit.MINUTES ); + tx.success(); + } + } + + private void setupDb( GraphDatabaseSettings.SchemaIndex schemaIndex ) + { + TestGraphDatabaseFactory dbFactory = new TestGraphDatabaseFactory(); + GraphDatabaseBuilder builder = dbFactory.newEmbeddedDatabaseBuilder( directory.graphDbDir() ) + .setConfig( GraphDatabaseSettings.default_schema_provider, schemaIndex.providerName() ); + db = builder.newGraphDatabase(); + } +} diff --git a/community/lucene-index/src/test/java/org/neo4j/unsafe/batchinsert/internal/BatchInsertIndexProviderTest.java b/community/lucene-index/src/test/java/org/neo4j/unsafe/batchinsert/internal/BatchInsertIndexProviderTest.java index 34275bb22b981..ef5d5e4ba8845 100644 --- a/community/lucene-index/src/test/java/org/neo4j/unsafe/batchinsert/internal/BatchInsertIndexProviderTest.java +++ b/community/lucene-index/src/test/java/org/neo4j/unsafe/batchinsert/internal/BatchInsertIndexProviderTest.java @@ -28,18 +28,26 @@ import java.io.File; import java.util.Arrays; import java.util.Collection; -import java.util.Map; +import java.util.Iterator; import java.util.concurrent.TimeUnit; import org.neo4j.graphdb.DependencyResolver; import org.neo4j.graphdb.GraphDatabaseService; +import org.neo4j.graphdb.Node; +import org.neo4j.graphdb.ResourceIterator; import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.factory.GraphDatabaseSettings; +import org.neo4j.graphdb.schema.IndexDefinition; +import org.neo4j.graphdb.schema.Schema; +import org.neo4j.helpers.collection.MapUtil; +import org.neo4j.helpers.collection.Pair; import org.neo4j.internal.kernel.api.CapableIndexReference; import org.neo4j.internal.kernel.api.SchemaRead; import org.neo4j.internal.kernel.api.TokenRead; import org.neo4j.kernel.api.KernelTransaction; +import org.neo4j.kernel.configuration.Config; import org.neo4j.kernel.impl.core.ThreadToStatementContextBridge; +import org.neo4j.kernel.impl.index.schema.config.SpatialIndexValueTestUtil; import org.neo4j.kernel.internal.GraphDatabaseAPI; import org.neo4j.test.TestGraphDatabaseFactory; import org.neo4j.test.TestLabels; @@ -48,7 +56,12 @@ import org.neo4j.test.rule.fs.DefaultFileSystemRule; import org.neo4j.unsafe.batchinsert.BatchInserter; import org.neo4j.unsafe.batchinsert.BatchInserters; +import org.neo4j.values.storable.CoordinateReferenceSystem; +import org.neo4j.values.storable.PointValue; +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.graphdb.factory.GraphDatabaseSettings.default_schema_provider; import static org.neo4j.helpers.collection.MapUtil.stringMap; @@ -82,11 +95,12 @@ public BatchInsertIndexProviderTest( GraphDatabaseSettings.SchemaIndex schemaInd @Test public void batchInserterShouldUseConfiguredIndexProvider() throws Exception { - Map config = stringMap( default_schema_provider.name(), schemaIndex.providerName() ); + Config config = Config.defaults( stringMap( default_schema_provider.name(), schemaIndex.providerName() ) ); BatchInserter inserter = newBatchInserter( config ); inserter.createDeferredSchemaIndex( TestLabels.LABEL_ONE ).on( "key" ).create(); inserter.shutdown(); GraphDatabaseService db = graphDatabaseService( inserter.getStoreDir(), config ); + awaitIndexesOnline( db ); try ( Transaction tx = db.beginTx() ) { DependencyResolver dependencyResolver = ((GraphDatabaseAPI) db).getDependencyResolver(); @@ -107,27 +121,94 @@ public void batchInserterShouldUseConfiguredIndexProvider() throws Exception } } - private BatchInserter newBatchInserter( Map config ) throws Exception + @Test + public void shouldPopulateIndexWithUniquePointsThatCollideOnSpaceFillingCurve() throws Exception + { + Config config = Config.defaults( stringMap( default_schema_provider.name(), schemaIndex.providerName() ) ); + BatchInserter inserter = newBatchInserter( config ); + Pair collidingPoints = SpatialIndexValueTestUtil.pointsWithSameValueOnSpaceFillingCurve( config ); + inserter.createNode( MapUtil.map( "prop", collidingPoints.first() ), TestLabels.LABEL_ONE ); + inserter.createNode( MapUtil.map( "prop", collidingPoints.other() ), TestLabels.LABEL_ONE ); + inserter.createDeferredConstraint( TestLabels.LABEL_ONE ).assertPropertyIsUnique( "prop" ).create(); + inserter.shutdown(); + + GraphDatabaseService db = graphDatabaseService( inserter.getStoreDir(), config ); + try + { + awaitIndexesOnline( db ); + try ( Transaction tx = db.beginTx() ) + { + assertSingleCorrectHit( db, collidingPoints.first() ); + assertSingleCorrectHit( db, collidingPoints.other() ); + tx.success(); + } + } + finally + { + db.shutdown(); + } + } + + @Test + public void shouldThrowWhenPopulatingWithNonUniquePoints() throws Exception + { + Config config = Config.defaults( stringMap( default_schema_provider.name(), schemaIndex.providerName() ) ); + BatchInserter inserter = newBatchInserter( config ); + PointValue point = Values.pointValue( CoordinateReferenceSystem.WGS84, 0.0, 0.0 ); + inserter.createNode( MapUtil.map( "prop", point ), TestLabels.LABEL_ONE ); + inserter.createNode( MapUtil.map( "prop", point ), TestLabels.LABEL_ONE ); + inserter.createDeferredConstraint( TestLabels.LABEL_ONE ).assertPropertyIsUnique( "prop" ).create(); + inserter.shutdown(); + + GraphDatabaseService db = graphDatabaseService( inserter.getStoreDir(), config ); + try ( Transaction tx = db.beginTx() ) + { + Iterator indexes = db.schema().getIndexes().iterator(); + assertTrue( indexes.hasNext() ); + IndexDefinition index = indexes.next(); + Schema.IndexState indexState = db.schema().getIndexState( index ); + assertEquals( Schema.IndexState.FAILED, indexState ); + assertFalse( indexes.hasNext() ); + tx.success(); + } + finally + { + db.shutdown(); + } + } + + private void assertSingleCorrectHit( GraphDatabaseService db, PointValue point ) { - return BatchInserters.inserter( storeDir.absolutePath(), fileSystemRule.get(), config ); + ResourceIterator nodes = db.findNodes( TestLabels.LABEL_ONE, "prop", point ); + assertTrue( nodes.hasNext() ); + Node node = nodes.next(); + Object prop = node.getProperty( "prop" ); + assertEquals( point, prop ); + assertFalse( nodes.hasNext() ); } - private GraphDatabaseService graphDatabaseService( String storeDir, Map config ) + private BatchInserter newBatchInserter( Config config ) throws Exception + { + return BatchInserters.inserter( storeDir.absolutePath(), fileSystemRule.get(), config.getRaw() ); + } + + private GraphDatabaseService graphDatabaseService( String storeDir, Config config ) { TestGraphDatabaseFactory factory = new TestGraphDatabaseFactory(); factory.setFileSystem( fileSystemRule.get() ); - GraphDatabaseService db = factory.newImpermanentDatabaseBuilder( new File( storeDir ) ) + return factory.newImpermanentDatabaseBuilder( new File( storeDir ) ) // Shouldn't be necessary to set dense node threshold since it's a stick config - .setConfig( config ) + .setConfig( config.getRaw() ) .newGraphDatabase(); + } + private void awaitIndexesOnline( GraphDatabaseService db ) + { try ( Transaction tx = db.beginTx() ) { db.schema().awaitIndexesOnline( 10, TimeUnit.SECONDS ); tx.success(); } - - return db; } private String unexpectedIndexProviderMessage( CapableIndexReference index )