Skip to content

Commit

Permalink
Enforce composite uniqueness constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
fickludd committed Mar 9, 2017
1 parent 36eafb1 commit 7e5cd46
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 56 deletions.
Expand Up @@ -28,6 +28,8 @@
import org.neo4j.collection.primitive.PrimitiveIntSet;
import org.neo4j.collection.primitive.PrimitiveLongIterator;
import org.neo4j.cursor.Cursor;
import org.neo4j.helpers.collection.CastingIterator;
import org.neo4j.helpers.collection.Iterators;
import org.neo4j.kernel.api.exceptions.EntityNotFoundException;
import org.neo4j.kernel.api.exceptions.InvalidTransactionTypeKernelException;
import org.neo4j.kernel.api.exceptions.KernelException;
Expand All @@ -53,7 +55,6 @@
import org.neo4j.kernel.api.schema_new.OrderedPropertyValues;
import org.neo4j.kernel.api.schema_new.RelationTypeSchemaDescriptor;
import org.neo4j.kernel.api.schema_new.SchemaDescriptor;
import org.neo4j.kernel.api.schema_new.SchemaDescriptorFactory;
import org.neo4j.kernel.api.schema_new.constaints.ConstraintDescriptor;
import org.neo4j.kernel.api.schema_new.constaints.NodeExistenceConstraintDescriptor;
import org.neo4j.kernel.api.schema_new.constaints.RelExistenceConstraintDescriptor;
Expand All @@ -64,6 +65,7 @@
import org.neo4j.kernel.impl.api.operations.EntityWriteOperations;
import org.neo4j.kernel.impl.api.operations.SchemaReadOperations;
import org.neo4j.kernel.impl.api.operations.SchemaWriteOperations;
import org.neo4j.kernel.impl.api.schema.NodeSchemaMatcher;
import org.neo4j.kernel.impl.api.store.NodeLoadingIterator;
import org.neo4j.kernel.impl.constraints.ConstraintSemantics;
import org.neo4j.kernel.impl.locking.LockTracer;
Expand All @@ -75,7 +77,9 @@

import static java.lang.String.format;
import static org.neo4j.kernel.api.StatementConstants.NO_SUCH_NODE;
import static org.neo4j.kernel.api.StatementConstants.NO_SUCH_PROPERTY_KEY;
import static org.neo4j.kernel.api.exceptions.schema.ConstraintValidationException.Phase.VALIDATION;
import static org.neo4j.kernel.api.schema_new.SchemaDescriptorPredicates.hasProperty;
import static org.neo4j.kernel.api.schema_new.constaints.ConstraintDescriptor.Type.UNIQUE;
import static org.neo4j.kernel.impl.locking.ResourceTypes.INDEX_ENTRY;
import static org.neo4j.kernel.impl.locking.ResourceTypes.indexEntryResourceId;
Expand All @@ -87,6 +91,7 @@ public class ConstraintEnforcingEntityOperations implements EntityOperations, Sc
private final SchemaWriteOperations schemaWriteOperations;
private final SchemaReadOperations schemaReadOperations;
private final ConstraintSemantics constraintSemantics;
private final NodeSchemaMatcher<UniquenessConstraintDescriptor> nodeSchemaMatcher;

public ConstraintEnforcingEntityOperations(
ConstraintSemantics constraintSemantics, EntityWriteOperations entityWriteOperations,
Expand All @@ -99,6 +104,7 @@ public ConstraintEnforcingEntityOperations(
this.entityReadOperations = entityReadOperations;
this.schemaWriteOperations = schemaWriteOperations;
this.schemaReadOperations = schemaReadOperations;
nodeSchemaMatcher = new NodeSchemaMatcher( entityReadOperations );
}

@Override
Expand Down Expand Up @@ -133,47 +139,65 @@ public Property nodeSetProperty( KernelStatement state, long nodeId, DefinedProp
{
try ( Cursor<NodeItem> cursor = nodeCursorById( state, nodeId ) )
{
// TODO: support composite indexes
NodeItem node = cursor.get();
node.labels().visitKeys( labelId ->
{
int propertyKeyId = property.propertyKeyId();
Iterator<ConstraintDescriptor> constraints =
schemaReadOperations.constraintsGetForSchema( state,
SchemaDescriptorFactory.forLabel( labelId, propertyKeyId ) );
while ( constraints.hasNext() )
{
ConstraintDescriptor constraint = constraints.next();
if ( constraint.type() == UNIQUE )
{
validateNoExistingNodeWithExactValues(
state,
(UniquenessConstraintDescriptor) constraint,
new ExactPredicate[]{IndexQuery.exact( propertyKeyId, property.value() )},
Iterator<ConstraintDescriptor> constraints = getConstraintsForProperty( state, property.propertyKeyId() );
Iterator<UniquenessConstraintDescriptor> uniqueness =
new CastingIterator<>( constraints, UniquenessConstraintDescriptor.class );

nodeSchemaMatcher.onMatchingSchema( state, uniqueness, node, property.propertyKeyId(),
constraint -> {
validateNoExistingNodeWithExactValues( state, constraint,
getAllPropertyValues( state, constraint.schema(), node, property ),
node.id() );
}
}
return false;
} );
} );
}

return entityWriteOperations.nodeSetProperty( state, nodeId, property );
}

private Iterator<ConstraintDescriptor> getConstraintsForProperty( KernelStatement state, int propertyId )
{
Iterator<ConstraintDescriptor> allConstraints = schemaReadOperations.constraintsGetAll( state );

return Iterators.filter( hasProperty( propertyId ), allConstraints );
}

private ExactPredicate[] getAllPropertyValues( KernelStatement state, SchemaDescriptor schema, NodeItem node )
{
int[] propertyIds = schema.getPropertyIds();
ExactPredicate[] values = new ExactPredicate[propertyIds.length];
return getAllPropertyValues( state, schema, node, DefinedProperty.NO_SUCH_PROPERTY );
}

private ExactPredicate[] getAllPropertyValues( KernelStatement state, SchemaDescriptor schema, NodeItem node,
DefinedProperty specialProperty )
{
int[] schemaPropertyIds = schema.getPropertyIds();
ExactPredicate[] values = new ExactPredicate[schemaPropertyIds.length];

int nMatched = 0;
Cursor<PropertyItem> nodePropertyCursor = nodeGetProperties( state, node );
int specialPropId = specialProperty.propertyKeyId();
while ( nodePropertyCursor.next() )
{
PropertyItem property = nodePropertyCursor.get();
int k = ArrayUtils.indexOf( propertyIds, property.propertyKeyId() );

int nodePropertyId = property.propertyKeyId();
int k = ArrayUtils.indexOf( schemaPropertyIds, nodePropertyId );
if ( k >= 0 )
{
if ( nodePropertyId != specialPropId )
{
values[k] = IndexQuery.exact( nodePropertyId, property.value() );
}
nMatched++;
}
}

if ( specialPropId != NO_SUCH_PROPERTY_KEY )
{
int k = ArrayUtils.indexOf( schemaPropertyIds, specialPropId );
if ( k >= 0 )
{
values[k] = IndexQuery.exact( property.propertyKeyId(), property.value() );
values[k] = IndexQuery.exact( specialPropId, specialProperty.value() );
nMatched++;
}
}
Expand Down
@@ -0,0 +1,254 @@
/*
* Copyright (c) 2002-2017 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.kernel.impl.api.integrationtest;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.io.IOException;
import java.util.Arrays;
import java.util.Map;

import org.neo4j.helpers.collection.Iterators;
import org.neo4j.helpers.collection.MapUtil;
import org.neo4j.kernel.api.Statement;
import org.neo4j.kernel.api.StatementTokenNameLookup;
import org.neo4j.kernel.api.TokenNameLookup;
import org.neo4j.kernel.api.exceptions.KernelException;
import org.neo4j.kernel.api.exceptions.schema.IllegalTokenNameException;
import org.neo4j.kernel.api.exceptions.schema.TooManyLabelsException;
import org.neo4j.kernel.api.exceptions.schema.UniquePropertyValueValidationException;
import org.neo4j.kernel.api.schema_new.index.NewIndexDescriptorFactory;
import org.neo4j.kernel.api.security.SecurityContext;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.neo4j.kernel.api.CompositeIndexingIT.LABEL_ID;
import static org.neo4j.kernel.api.properties.Property.property;
import static org.neo4j.kernel.api.schema_new.SchemaDescriptorFactory.forLabel;
import static org.neo4j.test.assertion.Assert.assertException;

@RunWith( Parameterized.class )
public class CompositeUniquenessConstraintValidationIT extends KernelIntegrationTest
{
@Rule
public final TestName testName = new TestName();

@Parameterized.Parameters( name = "Index: {0}" )
public static Iterable<Object[]> parameterValues() throws IOException
{
return Arrays.<Object[]>asList(
Iterators.array( "v1", "v2", "v1", "v2" ),
Iterators.array( 10, 20, 10, 20 ),
Iterators.array( 10L, 20L, 10, 20 ),
Iterators.array( 10, 20, 10L, 20L ),
Iterators.array( 10, 20, 10.0, 20.0 ),
Iterators.array( new int[]{1,2}, "v2", new int[]{1,2}, "v2" ),
Iterators.array( 'v', "v2", "v", "v2" )
);
}

private static final String prop1 = "key1";
private static final String prop2 = "key2";
private static final String label = "Label1";
private Statement statement;

private final Object valueA1;
private final Object valueA2;

private final Object valueB1;
private final Object valueB2;

public CompositeUniquenessConstraintValidationIT( Object valueA1, Object valueA2, Object valueB1, Object valueB2 )
{
this.valueA1 = valueA1;
this.valueA2 = valueA2;
this.valueB1 = valueB1;
this.valueB2 = valueB2;
}

@Test
public void shouldEnforceOnSetProperty() throws Exception
{
// given
constrainedNode( label, MapUtil.map( prop1, valueA1, prop2, valueA2 ) );

// when
newTransaction();
long node = createLabeledNode( label );
assertException( () -> {
setProperty( node, prop1, valueB1 ); // still ok
setProperty( node, prop2, valueB2 ); // boom!

}, UniquePropertyValueValidationException.class, "" );
}

@Test
public void shouldEnforceOnSetLabel() throws Exception
{
// given
constrainedNode( label, MapUtil.map( prop1, valueA1, prop2, valueA2 ) );

// when
newTransaction();
long node = createNode();
assertException( () -> {
setProperty( node, prop1, valueB1 ); // still ok
setProperty( node, prop2, valueB2 ); // and fine
addLabel( node, label ); // boom again

}, UniquePropertyValueValidationException.class, "" );
}

@Test
public void shouldEnforceOnSetPropertyInTx() throws Exception
{
// given
createConstraint( label, prop1, prop2 );

// when
newTransaction();
long nodeA = createLabeledNode( label );
setProperty( nodeA, prop1, valueA1 );
setProperty( nodeA, prop2, valueA2 );

long nodeB = createLabeledNode( label );
assertException( () -> {
setProperty( nodeB, prop1, valueB1 ); // still ok
setProperty( nodeB, prop2, valueB2 ); // boom!

}, UniquePropertyValueValidationException.class, "" );
}

@Test
public void shouldEnforceOnSetLabelInTx() throws Exception
{
// given
createConstraint( label, prop1, prop2 );

// when
newTransaction();
long nodeA = createLabeledNode( label );
setProperty( nodeA, prop1, valueA1 );
setProperty( nodeA, prop2, valueA2 );

long nodeB = createNode();
assertException( () -> {
setProperty( nodeB, prop1, valueB1 ); // still ok
setProperty( nodeB, prop2, valueB2 ); // and fine
addLabel( nodeB, label ); // boom again

}, UniquePropertyValueValidationException.class, "" );
}

private void newTransaction() throws KernelException
{
statement = statementInNewTransaction( SecurityContext.AUTH_DISABLED );
}

private long createLabeledNode( String label ) throws KernelException
{
long node = statement.dataWriteOperations().nodeCreate();
int labelId = statement.tokenWriteOperations().labelGetOrCreateForName( label );
statement.dataWriteOperations().nodeAddLabel( node, labelId );
return node;
}

private void addLabel( long nodeId, String label ) throws KernelException
{
addLabel( nodeId, getLabelId( label ) );
}

private void addLabel( long nodeId, int labelId ) throws KernelException
{
statement.dataWriteOperations().nodeAddLabel( nodeId, labelId );
}

private void setProperty( long nodeId, int propertyId, Object value ) throws KernelException
{
statement.dataWriteOperations().nodeSetProperty( nodeId, property( propertyId, value ) );
}

private void setProperty( long nodeId, String propertyName, Object value ) throws KernelException
{
statement.dataWriteOperations().nodeSetProperty( nodeId, property( getPropertyId( propertyName ), value ) );
}

private long createNode() throws KernelException
{
return statement.dataWriteOperations().nodeCreate();
}

private long constrainedNode( String label, Map<String,Object> properties )
throws KernelException
{
newTransaction();
int labelId = getLabelId( label );
long nodeId = createNode();
addLabel( nodeId, labelId );
for ( Map.Entry<String,Object> entry : properties.entrySet() )
{
int propertyId = getPropertyId( entry.getKey() );
setProperty( nodeId, propertyId, entry.getValue() );
}
commit();

createConstraint( label, properties.keySet().toArray( new String[0] ) );
return nodeId;
}

private void createConstraint( String label, String... propertyNames ) throws KernelException
{
newTransaction();
int labelId = getLabelId( label );
int[] propertyIds =
Arrays.stream( propertyNames )
.mapToInt( this::getPropertyId )
.toArray();

commit();

newTransaction();
statement.schemaWriteOperations().uniquePropertyConstraintCreate( forLabel( labelId, propertyIds ) );
commit();
}

private int getLabelId( String label ) throws IllegalTokenNameException, TooManyLabelsException
{
return statement.tokenWriteOperations().labelGetOrCreateForName( label );
}

private int getPropertyId( String propertyName )
{
try
{
return statement.tokenWriteOperations().propertyKeyGetOrCreateForName( propertyName );
}
catch ( IllegalTokenNameException e )
{
throw new RuntimeException( e );
}
}
}

0 comments on commit 7e5cd46

Please sign in to comment.