1 // Copyright 2008 Google Inc. All Rights Reserved.
2 package com
.google
.appengine
.api
.datastore
;
4 import com
.google
.apphosting
.api
.ApiProxy
;
5 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
;
6 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.GeoRegion
;
7 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.Query
;
8 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.Query
.Filter
;
9 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.Query
.Filter
.Operator
;
10 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.Query
.Order
;
11 import com
.google
.common
.collect
.Sets
;
12 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.Property
;
13 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.PropertyValue
;
14 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.PropertyValue
.ReferenceValue
;
16 import java
.util
.HashSet
;
20 * Wrapper around {@link Query} that performs validation.
23 class ValidatedQuery
extends NormalizedQuery
{
24 static final Set
<Operator
> UNSUPPORTED_OPERATORS
= makeImmutableSet(
28 * @throws IllegalQueryException If the provided query fails validation.
30 ValidatedQuery(Query query
) {
36 * Determines if a given query is supported by the datastore.
38 * @throws IllegalQueryException If the provided query fails validation.
40 private void validateQuery() {
41 if (query
.propertyNameSize() > 0 && query
.isKeysOnly()) {
42 throw new IllegalQueryException(
43 "projection and keys_only cannot both be set",
44 IllegalQueryType
.ILLEGAL_PROJECTION
);
47 Set
<String
> projectionProperties
= new HashSet
<String
>(query
.propertyNameSize());
48 for (String property
: query
.propertyNames()) {
49 if (!projectionProperties
.add(property
)) {
50 throw new IllegalQueryException(
51 "cannot project a property multiple times",
52 IllegalQueryType
.ILLEGAL_PROJECTION
);
56 Set
<String
> groupBySet
= Sets
.newHashSetWithExpectedSize(query
.groupByPropertyNameSize());
57 for (String name
: query
.groupByPropertyNames()) {
58 if (!groupBySet
.add(name
)) {
59 throw new IllegalQueryException("cannot group by a property multiple times",
60 IllegalQueryType
.ILLEGAL_GROUPBY
);
62 if (Entity
.RESERVED_NAME
.matcher(name
).matches()) {
63 throw new IllegalQueryException(
64 "group by is not supported for the property: " + name
,
65 IllegalQueryType
.ILLEGAL_GROUPBY
);
69 Set
<String
> groupByInOrderSet
=
70 Sets
.newHashSetWithExpectedSize(query
.groupByPropertyNameSize());
71 for (Order order
: query
.orders()) {
72 if (groupBySet
.contains(order
.getProperty())) {
73 groupByInOrderSet
.add(order
.getProperty());
74 } else if (groupByInOrderSet
.size() != groupBySet
.size()) {
75 throw new IllegalQueryException(
76 "must specify all group by orderings before any non group by orderings",
77 IllegalQueryType
.ILLEGAL_GROUPBY
);
81 if (query
.hasTransaction() && !query
.hasAncestor()) {
82 throw new IllegalQueryException(
83 "Only ancestor queries are allowed inside transactions.",
84 IllegalQueryType
.TRANSACTION_REQUIRES_ANCESTOR
);
87 if (!query
.hasKind()) {
88 for (Filter filter
: query
.filters()) {
89 if (!filter
.getProperty(0).getName().equals(Entity
.KEY_RESERVED_PROPERTY
)) {
90 throw new IllegalQueryException(
91 "kind is required for non-__key__ filters",
92 IllegalQueryType
.KIND_REQUIRED
);
95 for (Order order
: query
.orders()) {
96 if (!(order
.getProperty().equals(Entity
.KEY_RESERVED_PROPERTY
) &&
97 order
.getDirection() == Order
.Direction
.ASCENDING
.getValue())) {
98 throw new IllegalQueryException(
99 "kind is required for all orders except __key__ ascending",
100 IllegalQueryType
.KIND_REQUIRED
);
105 String ineqProp
= null;
106 boolean isGeo
= false;
107 for (Filter filter
: query
.filters()) {
108 int numProps
= filter
.propertySize();
110 throw new IllegalQueryException(
111 String
.format("Filter has %s properties, expected 1", numProps
),
112 IllegalQueryType
.FILTER_WITH_MULTIPLE_PROPS
);
115 Property prop
= filter
.getProperty(0);
116 String propName
= prop
.getName();
118 if (Entity
.KEY_RESERVED_PROPERTY
.equals(propName
)) {
119 PropertyValue value
= prop
.getValue();
120 if (!value
.hasReferenceValue()) {
121 throw new IllegalQueryException(
122 Entity
.KEY_RESERVED_PROPERTY
+ " filter value must be a Key",
123 IllegalQueryType
.ILLEGAL_VALUE
);
125 ReferenceValue refVal
= value
.getReferenceValue();
126 if (!refVal
.getApp().equals(query
.getApp())) {
127 throw new IllegalQueryException(
128 Entity
.KEY_RESERVED_PROPERTY
+ " filter app is " +
129 refVal
.getApp() + " but query app is " + query
.getApp(),
130 IllegalQueryType
.ILLEGAL_VALUE
);
132 if (!refVal
.getNameSpace().equals(query
.getNameSpace())) {
133 throw new IllegalQueryException(
134 Entity
.KEY_RESERVED_PROPERTY
+ " filter namespace is " +
135 refVal
.getNameSpace() + " but query namespace is " + query
.getNameSpace(),
136 IllegalQueryType
.ILLEGAL_VALUE
);
140 if (INEQUALITY_OPERATORS
.contains(filter
.getOpEnum())) {
141 if (ineqProp
== null) {
143 } else if (!ineqProp
.equals(propName
)) {
144 throw new IllegalQueryException(
145 String
.format("Only one inequality filter per query is supported. "
146 + "Encountered both %s and %s", ineqProp
, propName
),
147 IllegalQueryType
.MULTIPLE_INEQ_FILTERS
);
149 } else if (filter
.getOpEnum() == Operator
.EQUAL
) {
150 if (projectionProperties
.contains(propName
)) {
151 throw new IllegalQueryException(
152 "cannot use projection on a property with an equality filter",
153 IllegalQueryType
.ILLEGAL_PROJECTION
);
154 } else if (groupBySet
.contains(propName
)) {
155 throw new IllegalQueryException(
156 "cannot use group by on a property with an equality filter",
157 IllegalQueryType
.ILLEGAL_GROUPBY
);
159 } else if (filter
.getOpEnum() == Operator
.CONTAINED_IN_REGION
) {
161 if (!filter
.hasGeoRegion() || prop
.getValue().hasPointValue()) {
162 throw new IllegalQueryException(
164 "Geo-spatial filter on %s should specify GeoRegion rather than Property Value",
165 propName
), IllegalQueryType
.UNSUPPORTED_FILTER
);
167 GeoRegion region
= filter
.getGeoRegion();
168 if (region
.hasCircle() && region
.hasRectangle()
169 || !region
.hasCircle() && !region
.hasRectangle()) {
170 throw new IllegalQueryException(
172 "Geo-spatial filter on %s should specify Circle or Rectangle, but not both",
173 propName
), IllegalQueryType
.UNSUPPORTED_FILTER
);
175 } else if (UNSUPPORTED_OPERATORS
.contains(filter
.getOpEnum())) {
176 throw new IllegalQueryException(
177 String
.format("Unsupported filter operator: %s", filter
.getOp()),
178 IllegalQueryType
.UNSUPPORTED_FILTER
);
183 if (ineqProp
!= null) {
184 throw new IllegalQueryException(
185 "Inequality filter with geo-spatial query is not supported.",
186 IllegalQueryType
.UNSUPPORTED_FILTER
);
189 if (query
.hasAncestor()) {
190 throw new IllegalQueryException(
191 "Geo-spatial filter on ancestor query is not supported.",
192 IllegalQueryType
.UNSUPPORTED_FILTER
);
195 if (query
.hasCompiledCursor() || query
.hasEndCompiledCursor()) {
196 throw new IllegalQueryException(
197 "Start and end cursors are not supported on geo-spatial queries.",
198 IllegalQueryType
.CURSOR_NOT_SUPPORTED
);
202 if (ineqProp
!= null && query
.groupByPropertyNameSize() > 0) {
203 if (!groupBySet
.contains(ineqProp
)) {
204 throw new IllegalQueryException(
205 String
.format("Inequality filter on %s must also be a group by property when "
206 + "group by properties are set.", ineqProp
), IllegalQueryType
.ILLEGAL_GROUPBY
);
210 if (ineqProp
!= null) {
211 if (query
.orderSize() > 0) {
212 if (!ineqProp
.equals(query
.getOrder(0).getProperty())) {
213 throw new IllegalQueryException(
214 String
.format("The first sort property must be the same as the property to which "
215 + "the inequality filter is applied. In your query the first sort property is "
216 + "%s but the inequality filter is on %s",
217 query
.getOrder(0).getProperty(), ineqProp
),
218 IllegalQueryType
.FIRST_SORT_NEQ_INEQ_PROP
);
225 public boolean equals(Object o
) {
229 if (o
== null || getClass() != o
.getClass()) {
233 ValidatedQuery that
= (ValidatedQuery
) o
;
235 if (!query
.equals(that
.query
)) {
243 public int hashCode() {
244 return query
.hashCode();
247 enum IllegalQueryType
{
250 FILTER_WITH_MULTIPLE_PROPS
,
251 MULTIPLE_INEQ_FILTERS
,
252 FIRST_SORT_NEQ_INEQ_PROP
,
253 TRANSACTION_REQUIRES_ANCESTOR
,
257 CURSOR_NOT_SUPPORTED
,
260 static class IllegalQueryException
extends ApiProxy
.ApplicationException
{
262 private final IllegalQueryType illegalQueryType
;
264 IllegalQueryException(String errorDetail
,
265 IllegalQueryType illegalQueryType
) {
266 super(DatastoreV3Pb
.Error
.ErrorCode
.BAD_REQUEST
.getValue(), errorDetail
);
267 this.illegalQueryType
= illegalQueryType
;
270 IllegalQueryType
getIllegalQueryType() {
271 return illegalQueryType
;