1 package com
.google
.appengine
.api
.datastore
;
3 import static com
.google
.common
.base
.Preconditions
.checkArgument
;
5 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
;
6 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.Query
.Filter
;
7 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.RegionPoint
;
9 import java
.util
.ArrayList
;
10 import java
.util
.Collections
;
11 import java
.util
.List
;
14 * Simulates the filter matching process used in the datastore for a single
17 * The true behavior of the datastore becomes extremely strange for multi-valued
18 * properties especially when there are inequality and equality filters on the
19 * same property. Here is the current logic that governs filtering:
21 * All inequality filters are merged together into a range so that:
22 * a > 1 && a <=3 && a >=2
24 * "There exists an x such that x is an element of a and 2 <= x <= 3"
26 * All equality filters are handled independently so:
29 * "For all x in [1, 4] there x is an element of a"
31 * When there are both equality and inequality filters 'a' must meet both
34 * For example consider:
37 * This may seem like a query with this filter should always return an empty
38 * result set, but this is actually not the case when 'a' has multiple values.
39 * This is currently planned in the datastore as a composite index scan on the
40 * index (a, a) where the first 'a' is set to 4 and the second 'a' has 'a < 0'
44 * a = [-1, 4, 2] will produces the following index data:
55 * the a = 4 is applied first so we restrict our scan of the index to:
60 * then a < 0 is applied to restrict our scan further to:
63 * thus a = [-1, 4, 2] matches our query.
65 * It is also important to note that 'a < 0 && a > 1' will always return no
66 * results as this is converted into '1 < a < 0' before being applied.
70 public static final FilterMatcher MATCH_ALL
= new FilterMatcher() {
72 public void addFilter(Filter filter
) {
73 throw new UnsupportedOperationException("FilterMatcher.MATCH_ALL is immutable");
77 public boolean matches(List
<Comparable
<Object
>> values
) {
82 boolean matchesRange(Comparable
<Object
> value
) {
88 static class NoValue
implements Comparable
<Object
> {
89 static final FilterMatcher
.NoValue INSTANCE
= new NoValue();
94 public int compareTo(Object o
) {
95 throw new UnsupportedOperationException();
99 Comparable
<Object
> min
= NoValue
.INSTANCE
;
100 boolean minInclusive
;
102 Comparable
<Object
> max
= NoValue
.INSTANCE
;
103 boolean maxInclusive
;
104 List
<Comparable
<Object
>> equalValues
= new ArrayList
<Comparable
<Object
>>();
105 List
<Query
.GeoRegion
> geoRegions
= new ArrayList
<>();
108 * Returns true if the given value should be taken into account when determining order.
110 public boolean considerValueForOrder(Comparable
<Object
> value
) {
111 return matchesRange(value
);
114 boolean matchesRange(Comparable
<Object
> value
) {
115 if (min
!= NoValue
.INSTANCE
) {
116 int cmp
= EntityProtoComparators
.MULTI_TYPE_COMPARATOR
.compare(value
, min
);
117 if (cmp
< 0 || (cmp
== 0 && !minInclusive
)) {
122 if (max
!= NoValue
.INSTANCE
) {
123 int cmp
= EntityProtoComparators
.MULTI_TYPE_COMPARATOR
.compare(value
, max
);
124 if (cmp
> 0 || (cmp
== 0 && !maxInclusive
)) {
133 * Returns true if the given values match the filters provided through {@link #addFilter}.
135 public boolean matches(List
<Comparable
<Object
>> values
) {
136 if (values
.size() > 1) {
137 Collections
.sort(values
, EntityProtoComparators
.MULTI_TYPE_COMPARATOR
);
139 for (Comparable
<Object
> eqValue
: equalValues
) {
140 if (Collections
.binarySearch(
141 values
, eqValue
, EntityProtoComparators
.MULTI_TYPE_COMPARATOR
) < 0) {
146 for (Query
.GeoRegion region
: geoRegions
) {
147 boolean contained
= false;
148 for (Comparable
<Object
> value
: values
) {
150 if (o
instanceof GeoPt
&& region
.contains((GeoPt
) o
)) {
160 for (Comparable
<Object
> value
: values
) {
161 if (matchesRange(value
)) {
169 public void addFilter(Filter filter
) {
170 Comparable
<Object
> value
= DataTypeTranslator
.getComparablePropertyValue(filter
.getProperty(0));
171 switch (filter
.getOpEnum()) {
173 equalValues
.add(value
);
176 if (min
== NoValue
.INSTANCE
||
177 EntityProtoComparators
.MULTI_TYPE_COMPARATOR
.compare(min
, value
) <= 0) {
179 minInclusive
= false;
182 case GREATER_THAN_OR_EQUAL
:
183 if (min
== NoValue
.INSTANCE
||
184 EntityProtoComparators
.MULTI_TYPE_COMPARATOR
.compare(min
, value
) < 0) {
190 if (max
== NoValue
.INSTANCE
||
191 EntityProtoComparators
.MULTI_TYPE_COMPARATOR
.compare(max
, value
) >= 0) {
193 maxInclusive
= false;
196 case LESS_THAN_OR_EQUAL
:
197 if (max
== NoValue
.INSTANCE
||
198 EntityProtoComparators
.MULTI_TYPE_COMPARATOR
.compare(max
, value
) > 0) {
205 case CONTAINED_IN_REGION
:
206 geoRegions
.add(fromProto(filter
.getGeoRegion()));
209 throw new IllegalArgumentException(
210 "Unable to perform filter using operator " + filter
.getOp());
214 private static GeoPt
fromProto(RegionPoint point
) {
215 return new GeoPt((float) point
.getLatitude(), (float) point
.getLongitude());
218 private static Query
.GeoRegion
fromProto(DatastoreV3Pb
.GeoRegion pb
) {
219 if (pb
.hasCircle()) {
220 return new Query
.GeoRegion
.Circle(fromProto(pb
.getCircle().getCenter()),
221 pb
.getCircle().getRadiusMeters());
223 checkArgument(pb
.hasRectangle());
224 return new Query
.GeoRegion
.Rectangle(fromProto(pb
.getRectangle().getSouthwest()),
225 fromProto(pb
.getRectangle().getNortheast()));
229 public String
toString() {
230 StringBuilder result
= new StringBuilder("FilterMatcher [");
232 if (min
!= NoValue
.INSTANCE
|| max
!= NoValue
.INSTANCE
) {
233 if (min
!= NoValue
.INSTANCE
) {
235 result
.append(minInclusive ?
" <= " : " < ");
238 if (max
!= NoValue
.INSTANCE
) {
239 result
.append(maxInclusive ?
" <= " : " < ");
242 if (!equalValues
.isEmpty()) {
243 result
.append(" && ");
246 if (!equalValues
.isEmpty()) {
247 result
.append("X CONTAINS ");
248 result
.append(equalValues
);
252 return result
.toString();