Skip to content

Commit

Permalink
Fixes spatial inequalities for non-indexed and txstate cases
Browse files Browse the repository at this point in the history
  • Loading branch information
craigtaverner committed Aug 24, 2018
1 parent 9704303 commit 0ccdd2c
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 49 deletions.
Expand Up @@ -233,7 +233,8 @@ else if ( lhs instanceof BooleanValue && rhs instanceof BooleanValue )
}
else if ( lhs instanceof PointValue && rhs instanceof PointValue )
{
return lessThanOrEqual( AnyValues.TERNARY_COMPARATOR.ternaryCompare( lhs, rhs ) );
// The ternary comparator cannot handle the '=' part of the >= and <= cases, so we need to switch to the within function
return ((PointValue) lhs).withinRange( null, false, (PointValue) rhs, true ) ? TRUE : FALSE;
}
else if ( lhs instanceof DateValue && rhs instanceof DateValue )
{
Expand Down Expand Up @@ -329,7 +330,8 @@ else if ( lhs instanceof BooleanValue && rhs instanceof BooleanValue )
}
else if ( lhs instanceof PointValue && rhs instanceof PointValue )
{
return greaterThanOrEqual( AnyValues.TERNARY_COMPARATOR.ternaryCompare( lhs, rhs ) );
// The ternary comparator cannot handle the '=' part of the >= and <= cases, so we need to switch to the within function
return ((PointValue) lhs).withinRange( (PointValue) rhs, true, null, false ) ? TRUE : FALSE;
}
else if ( lhs instanceof DateValue && rhs instanceof DateValue )
{
Expand Down
Expand Up @@ -393,6 +393,14 @@ public boolean toInclusive()
{
return toInclusive;
}

/**
* @return true if the order defined for this type can also be relied on for bounds comparisons.
*/
public boolean isRegularOrder()
{
return true;
}
}

public static final class GeometryRangePredicate extends RangePredicate<PointValue>
Expand Down Expand Up @@ -437,6 +445,15 @@ public PointValue to()
{
return to;
}

/**
* The order defined for spatial types cannot be used for bounds comparisons.
* @return false
*/
public boolean isRegularOrder()
{
return false;
}
}

public static final class NumberRangePredicate extends RangePredicate<NumberValue>
Expand Down
Expand Up @@ -287,18 +287,13 @@ private void rangeQuery( IndexDescriptor descriptor, IndexQuery.RangePredicate<?

if ( needsValues )
{
ReadableDiffSets<NodeWithPropertyValues> changes =
TxStateIndexChanges.indexUpdatesWithValuesForRangeSeek( txState, descriptor, valueGroup, predicate.fromValue(),
predicate.fromInclusive(), predicate.toValue(), predicate.toInclusive() );
ReadableDiffSets<NodeWithPropertyValues> changes = TxStateIndexChanges.indexUpdatesWithValuesForRangeSeek( txState, descriptor, predicate );
addedWithValues = changes.getAdded().iterator();
removed = removed( txState, changes.getRemoved() );
}
else
{
LongDiffSets changes = TxStateIndexChanges.indexUpdatesForRangeSeek( txState,
descriptor, valueGroup,
predicate.fromValue(), predicate.fromInclusive(),
predicate.toValue(), predicate.toInclusive() );
LongDiffSets changes = TxStateIndexChanges.indexUpdatesForRangeSeek( txState, descriptor, predicate );
added = changes.augment( ImmutableEmptyLongIterator.INSTANCE );
removed = removed( txState, changes );
}
Expand Down
Expand Up @@ -21,7 +21,6 @@

import org.eclipse.collections.impl.UnmodifiableMap;

import java.util.Collection;
import java.util.Map;
import java.util.NavigableMap;

Expand All @@ -34,7 +33,6 @@
import org.neo4j.storageengine.api.txstate.ReadableTransactionState;
import org.neo4j.values.storable.TextValue;
import org.neo4j.values.storable.Value;
import org.neo4j.values.storable.ValueGroup;
import org.neo4j.values.storable.ValueTuple;
import org.neo4j.values.storable.Values;

Expand Down Expand Up @@ -144,9 +142,10 @@ static LongDiffSets indexUpdatesForSeek( ReadableTransactionState txState, Index
return LongDiffSets.EMPTY;
}

static LongDiffSets indexUpdatesForRangeSeek( ReadableTransactionState txState, IndexDescriptor descriptor, ValueGroup valueGroup, Value lower,
boolean includeLower, Value upper, boolean includeUpper )
static LongDiffSets indexUpdatesForRangeSeek( ReadableTransactionState txState, IndexDescriptor descriptor, IndexQuery.RangePredicate<?> predicate )
{
Value lower = predicate.fromValue();
Value upper = predicate.toValue();
if ( lower == null || upper == null )
{
throw new IllegalStateException( "Use Values.NO_VALUE to encode the lack of a bound" );
Expand All @@ -166,46 +165,48 @@ static LongDiffSets indexUpdatesForRangeSeek( ReadableTransactionState txState,

if ( lower == NO_VALUE )
{
selectedLower = ValueTuple.of( Values.minValue( valueGroup, upper ) );
selectedLower = ValueTuple.of( Values.minValue( predicate.valueGroup(), upper ) );
selectedIncludeLower = true;
}
else
{
selectedLower = ValueTuple.of( lower );
selectedIncludeLower = includeLower;
selectedIncludeLower = predicate.fromInclusive();
}

if ( upper == NO_VALUE )
{
selectedUpper = ValueTuple.of( Values.maxValue( valueGroup, lower ) );
selectedUpper = ValueTuple.of( Values.maxValue( predicate.valueGroup(), lower ) );
selectedIncludeUpper = false;
}
else
{
selectedUpper = ValueTuple.of( upper );
selectedIncludeUpper = includeUpper;
selectedIncludeUpper = predicate.toInclusive();
}

return indexUpdatesForRangeSeek( sortedUpdates, selectedLower, selectedIncludeLower, selectedUpper, selectedIncludeUpper );
}

private static LongDiffSets indexUpdatesForRangeSeek( NavigableMap<ValueTuple,? extends LongDiffSets> sortedUpdates, ValueTuple lower, boolean includeLower,
ValueTuple upper, boolean includeUpper )
{
MutableLongDiffSetsImpl diffs = new MutableLongDiffSetsImpl();

Collection<? extends LongDiffSets> inRange = sortedUpdates.subMap( lower, includeLower, upper, includeUpper ).values();
for ( LongDiffSets diffForSpecificValue : inRange )
Map<ValueTuple,? extends LongDiffSets> inRange = sortedUpdates.subMap( selectedLower, selectedIncludeLower, selectedUpper, selectedIncludeUpper );
for ( Map.Entry<ValueTuple,? extends LongDiffSets> entry : inRange.entrySet() )
{
diffs.addAll( diffForSpecificValue.getAdded() );
diffs.removeAll( diffForSpecificValue.getRemoved() );
ValueTuple values = entry.getKey();
LongDiffSets diffForSpecificValue = entry.getValue();
// The TreeMap cannot perfectly order multi-dimensional types (spatial) and need additional filtering out false positives
if ( predicate.isRegularOrder() || predicate.acceptsValue( values.getOnlyValue() ) )
{
diffs.addAll( diffForSpecificValue.getAdded() );
diffs.removeAll( diffForSpecificValue.getRemoved() );
}
}
return diffs;
}

static ReadableDiffSets<NodeWithPropertyValues> indexUpdatesWithValuesForRangeSeek( ReadableTransactionState txState, IndexDescriptor descriptor,
ValueGroup valueGroup, Value lower, boolean includeLower, Value upper, boolean includeUpper )
IndexQuery.RangePredicate<?> predicate )
{
Value lower = predicate.fromValue();
Value upper = predicate.toValue();
if ( lower == null || upper == null )
{
throw new IllegalStateException( "Use Values.NO_VALUE to encode the lack of a bound" );
Expand All @@ -225,32 +226,26 @@ static ReadableDiffSets<NodeWithPropertyValues> indexUpdatesWithValuesForRangeSe

if ( lower == NO_VALUE )
{
selectedLower = ValueTuple.of( Values.minValue( valueGroup, upper ) );
selectedLower = ValueTuple.of( Values.minValue( predicate.valueGroup(), upper ) );
selectedIncludeLower = true;
}
else
{
selectedLower = ValueTuple.of( lower );
selectedIncludeLower = includeLower;
selectedIncludeLower = predicate.fromInclusive();
}

if ( upper == NO_VALUE )
{
selectedUpper = ValueTuple.of( Values.maxValue( valueGroup, lower ) );
selectedUpper = ValueTuple.of( Values.maxValue( predicate.valueGroup(), lower ) );
selectedIncludeUpper = false;
}
else
{
selectedUpper = ValueTuple.of( upper );
selectedIncludeUpper = includeUpper;
selectedIncludeUpper = predicate.toInclusive();
}

return indexUpdatesWithValuesForRangeSeek( sortedUpdates, selectedLower, selectedIncludeLower, selectedUpper, selectedIncludeUpper );
}

private static ReadableDiffSets<NodeWithPropertyValues> indexUpdatesWithValuesForRangeSeek( NavigableMap<ValueTuple,? extends LongDiffSets> sortedUpdates,
ValueTuple selectedLower, boolean selectedIncludeLower, ValueTuple selectedUpper, boolean selectedIncludeUpper )
{
DiffSets<NodeWithPropertyValues> diffs = new DiffSets<>();

Map<ValueTuple,? extends LongDiffSets> inRange = sortedUpdates.subMap( selectedLower, selectedIncludeLower, selectedUpper, selectedIncludeUpper );
Expand All @@ -260,8 +255,12 @@ private static ReadableDiffSets<NodeWithPropertyValues> indexUpdatesWithValuesFo
Value[] valuesArray = values.getValues();
LongDiffSets diffForSpecificValue = entry.getValue();

diffForSpecificValue.getAdded().each( nodeId -> diffs.add( new NodeWithPropertyValues( nodeId, valuesArray ) ) );
diffForSpecificValue.getRemoved().each( nodeId -> diffs.remove( new NodeWithPropertyValues( nodeId, valuesArray ) ) );
// The TreeMap cannot perfectly order multi-dimensional types (spatial) and need additional filtering out false positives
if ( predicate.isRegularOrder() || predicate.acceptsValue( values.getOnlyValue() ) )
{
diffForSpecificValue.getAdded().each( nodeId -> diffs.add( new NodeWithPropertyValues( nodeId, valuesArray ) ) );
diffForSpecificValue.getRemoved().each( nodeId -> diffs.remove( new NodeWithPropertyValues( nodeId, valuesArray ) ) );
}
}
return diffs;
}
Expand Down
Expand Up @@ -217,10 +217,9 @@ private DynamicTest rangeTest( ReadableTransactionState state, Value lo, boolean
{
return DynamicTest.dynamicTest( String.format( "range seek: lo=%s (incl: %s), hi=%s (incl: %s)", lo, includeLo, hi, includeHi ), () ->
{
final LongDiffSets diffSets =
TxStateIndexChanges.indexUpdatesForRangeSeek( state, index, ValueGroup.NUMBER, lo, includeLo, hi, includeHi );
final LongDiffSets diffSets = TxStateIndexChanges.indexUpdatesForRangeSeek( state, index, IndexQuery.range( -1, lo, includeLo, hi, includeHi ) );
final ReadableDiffSets<NodeWithPropertyValues> diffSets2 =
TxStateIndexChanges.indexUpdatesWithValuesForRangeSeek( state, index, ValueGroup.NUMBER, lo, includeLo, hi, includeHi );
TxStateIndexChanges.indexUpdatesWithValuesForRangeSeek( state, index, IndexQuery.range( -1, lo, includeLo, hi, includeHi ) );

final LongSet expectedNodeIds = LongSets.immutable.ofAll( Arrays.stream( expected ).mapToLong( NodeWithPropertyValues::getNodeId ) );
assertEquals( expectedNodeIds, diffSets.getAdded() );
Expand Down
Expand Up @@ -171,9 +171,14 @@ Comparison unsafeTernaryCompareTo( Value otherValue )
for ( int i = 0; i < coordinate.length; i++ )
{
int cmpVal = Double.compare( this.coordinate[i], other.coordinate[i] );
if ( cmpVal != 0 && cmpVal != result )
if ( cmpVal == 0 && result != 0 )
{
if ( (cmpVal < 0 && result > 0) || (cmpVal > 0 && result < 0) )
// Equal on one dimension, but not others, is not defined
return Comparison.UNDEFINED;
}
if ( cmpVal != result )
{
if ( (cmpVal < 0 && result > 0) || (cmpVal > 0 && result < 0) || (i > 0 && result == 0) )
{
return Comparison.UNDEFINED;
}
Expand Down Expand Up @@ -278,7 +283,7 @@ public boolean withinRange( PointValue lower, boolean includeLower, PointValue u
{
if ( lower == null && upper == null )
{
return false;
return true;
}
if ( (lower != null) && (this.crs.getCode() != lower.crs.getCode() || this.coordinate.length != lower.coordinate.length) )
{
Expand Down
Expand Up @@ -22,9 +22,11 @@
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Test;

import org.neo4j.values.Comparison;
import org.neo4j.values.utils.InvalidValuesArgumentException;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
Expand Down Expand Up @@ -83,6 +85,51 @@ void shouldHaveValueGroup()
assertNotNull( pointValue( WGS84, 1, 2 ).valueGroup() );
}

//-------------------------------------------------------------
// Comparison tests

@Test
public void shouldCompareTwoPoints()
{
assertThat( "Two identical points should be equal", pointValue( Cartesian, 1, 2 ).compareTo( pointValue( Cartesian, 1, 2 ) ), equalTo( 0 ) );
assertThat( "Different CRS should compare CRS codes", pointValue( Cartesian, 1, 2 ).compareTo( pointValue( WGS84, 1, 2 ) ), equalTo( 1 ) );
assertThat( "Point greater on both dimensions is greater", pointValue( Cartesian, 2, 3 ).compareTo( pointValue( Cartesian, 1, 2 ) ), equalTo( 1 ) );
assertThat( "Point greater on first dimensions is greater", pointValue( Cartesian, 2, 2 ).compareTo( pointValue( Cartesian, 1, 2 ) ), equalTo( 1 ) );
assertThat( "Point greater on second dimensions is greater", pointValue( Cartesian, 1, 3 ).compareTo( pointValue( Cartesian, 1, 2 ) ), equalTo( 1 ) );
assertThat( "Point smaller on both dimensions is smaller", pointValue( Cartesian, 0, 1 ).compareTo( pointValue( Cartesian, 1, 2 ) ), equalTo( -1 ) );
assertThat( "Point smaller on first dimensions is smaller", pointValue( Cartesian, 0, 2 ).compareTo( pointValue( Cartesian, 1, 2 ) ), equalTo( -1 ) );
assertThat( "Point smaller on second dimensions is smaller", pointValue( Cartesian, 1, 1 ).compareTo( pointValue( Cartesian, 1, 2 ) ), equalTo( -1 ) );
assertThat( "Point greater on first and smaller on second dimensions is greater",
pointValue( Cartesian, 2, 1 ).compareTo( pointValue( Cartesian, 1, 2 ) ), equalTo( 1 ) );
assertThat( "Point smaller on first and greater on second dimensions is smaller",
pointValue( Cartesian, 0, 3 ).compareTo( pointValue( Cartesian, 1, 2 ) ), equalTo( -1 ) );
}

@Test
public void shouldTernaryCompareTwoPoints()
{
assertThat( "Two identical points should be equal", pointValue( Cartesian, 1, 2 ).unsafeTernaryCompareTo( pointValue( Cartesian, 1, 2 ) ),
equalTo( Comparison.EQUAL ) );
assertThat( "Different CRS should be incomparable", pointValue( Cartesian, 1, 2 ).unsafeTernaryCompareTo( pointValue( WGS84, 1, 2 ) ),
equalTo( Comparison.UNDEFINED ) );
assertThat( "Point greater on both dimensions is greater", pointValue( Cartesian, 2, 3 ).unsafeTernaryCompareTo( pointValue( Cartesian, 1, 2 ) ),
equalTo( Comparison.GREATER_THAN ) );
assertThat( "Point greater on first dimensions is UNDEFINED", pointValue( Cartesian, 2, 2 ).unsafeTernaryCompareTo( pointValue( Cartesian, 1, 2 ) ),
equalTo( Comparison.UNDEFINED ) );
assertThat( "Point greater on second dimensions is UNDEFINED", pointValue( Cartesian, 1, 3 ).unsafeTernaryCompareTo( pointValue( Cartesian, 1, 2 ) ),
equalTo( Comparison.UNDEFINED ) );
assertThat( "Point smaller on both dimensions is smaller", pointValue( Cartesian, 0, 1 ).unsafeTernaryCompareTo( pointValue( Cartesian, 1, 2 ) ),
equalTo( Comparison.SMALLER_THAN ) );
assertThat( "Point smaller on first dimensions is UNDEFINED", pointValue( Cartesian, 0, 2 ).unsafeTernaryCompareTo( pointValue( Cartesian, 1, 2 ) ),
equalTo( Comparison.UNDEFINED ) );
assertThat( "Point smaller on second dimensions is UNDEFINED", pointValue( Cartesian, 1, 1 ).unsafeTernaryCompareTo( pointValue( Cartesian, 1, 2 ) ),
equalTo( Comparison.UNDEFINED ) );
assertThat( "Point greater on first and smaller on second dimensions is UNDEFINED",
pointValue( Cartesian, 2, 1 ).unsafeTernaryCompareTo( pointValue( Cartesian, 1, 2 ) ), equalTo( Comparison.UNDEFINED ) );
assertThat( "Point smaller on first and greater on second dimensions is UNDEFINED",
pointValue( Cartesian, 0, 3 ).unsafeTernaryCompareTo( pointValue( Cartesian, 1, 2 ) ), equalTo( Comparison.UNDEFINED ) );
}

//-------------------------------------------------------------
// Parser tests

Expand Down

0 comments on commit 0ccdd2c

Please sign in to comment.