1 package com
.google
.appengine
.api
.datastore
;
3 import com
.google
.apphosting
.api
.ApiProxy
;
4 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
;
5 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.GeoRegion
;
6 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.Query
;
7 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.Query
.Filter
;
8 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.Query
.Filter
.Operator
;
9 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.Query
.Order
;
10 import com
.google
.common
.collect
.Sets
;
11 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.Property
;
12 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.PropertyValue
;
13 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.PropertyValue
.ReferenceValue
;
14 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.Reference
;
16 import java
.util
.HashSet
;
20 * Wrapper around {@link Query} that performs validation.
22 class ValidatedQuery
extends NormalizedQuery
{
23 static final Set
<Operator
> UNSUPPORTED_OPERATORS
= makeImmutableSet(
26 private boolean isGeo
;
29 * @throws IllegalQueryException If the provided query fails validation.
31 ValidatedQuery(Query query
) {
37 * Determines if a given query is supported by the datastore.
39 * @throws IllegalQueryException If the provided query fails validation.
41 private void validateQuery() {
42 if (query
.propertyNameSize() > 0 && query
.isKeysOnly()) {
43 throw new IllegalQueryException(
44 "projection and keys_only cannot both be set",
45 IllegalQueryType
.ILLEGAL_PROJECTION
);
48 Set
<String
> projectionProperties
= new HashSet
<String
>(query
.propertyNameSize());
49 for (String property
: query
.propertyNames()) {
50 if (!projectionProperties
.add(property
)) {
51 throw new IllegalQueryException(
52 "cannot project a property multiple times",
53 IllegalQueryType
.ILLEGAL_PROJECTION
);
57 Set
<String
> groupBySet
= Sets
.newHashSetWithExpectedSize(query
.groupByPropertyNameSize());
58 for (String name
: query
.groupByPropertyNames()) {
59 if (!groupBySet
.add(name
)) {
60 throw new IllegalQueryException("cannot group by a property multiple times",
61 IllegalQueryType
.ILLEGAL_GROUPBY
);
63 if (Entity
.RESERVED_NAME
.matcher(name
).matches()) {
64 throw new IllegalQueryException(
65 "group by is not supported for the property: " + name
,
66 IllegalQueryType
.ILLEGAL_GROUPBY
);
70 Set
<String
> groupByInOrderSet
=
71 Sets
.newHashSetWithExpectedSize(query
.groupByPropertyNameSize());
72 for (Order order
: query
.orders()) {
73 if (groupBySet
.contains(order
.getProperty())) {
74 groupByInOrderSet
.add(order
.getProperty());
75 } else if (groupByInOrderSet
.size() != groupBySet
.size()) {
76 throw new IllegalQueryException(
77 "must specify all group by orderings before any non group by orderings",
78 IllegalQueryType
.ILLEGAL_GROUPBY
);
82 if (query
.hasTransaction() && !query
.hasAncestor()) {
83 throw new IllegalQueryException(
84 "Only ancestor queries are allowed inside transactions.",
85 IllegalQueryType
.TRANSACTION_REQUIRES_ANCESTOR
);
88 if (!query
.hasKind()) {
89 for (Filter filter
: query
.filters()) {
90 if (!filter
.getProperty(0).getName().equals(Entity
.KEY_RESERVED_PROPERTY
)) {
91 throw new IllegalQueryException(
92 "kind is required for non-__key__ filters",
93 IllegalQueryType
.KIND_REQUIRED
);
96 for (Order order
: query
.orders()) {
97 if (!(order
.getProperty().equals(Entity
.KEY_RESERVED_PROPERTY
) &&
98 order
.getDirection() == Order
.Direction
.ASCENDING
.getValue())) {
99 throw new IllegalQueryException(
100 "kind is required for all orders except __key__ ascending",
101 IllegalQueryType
.KIND_REQUIRED
);
106 if (query
.hasAncestor()) {
107 Reference ancestor
= query
.getAncestor();
108 if (!ancestor
.getApp().equals(query
.getApp())) {
109 throw new IllegalQueryException(
110 "The query app is " + query
.getApp()
111 + " but ancestor app is " + ancestor
.getApp(),
112 IllegalQueryType
.ILLEGAL_VALUE
);
114 if (!ancestor
.getNameSpace().equals(query
.getNameSpace())) {
115 throw new IllegalQueryException(
116 "The query namespace is " + query
.getNameSpace()
117 + " but ancestor namespace is " + ancestor
.getNameSpace(),
118 IllegalQueryType
.ILLEGAL_VALUE
);
122 String ineqProp
= null;
124 for (Filter filter
: query
.filters()) {
125 int numProps
= filter
.propertySize();
127 throw new IllegalQueryException(
128 String
.format("Filter has %s properties, expected 1", numProps
),
129 IllegalQueryType
.FILTER_WITH_MULTIPLE_PROPS
);
132 Property prop
= filter
.getProperty(0);
133 String propName
= prop
.getName();
135 if (Entity
.KEY_RESERVED_PROPERTY
.equals(propName
)) {
136 PropertyValue value
= prop
.getValue();
137 if (!value
.hasReferenceValue()) {
138 throw new IllegalQueryException(
139 Entity
.KEY_RESERVED_PROPERTY
+ " filter value must be a Key",
140 IllegalQueryType
.ILLEGAL_VALUE
);
142 ReferenceValue refVal
= value
.getReferenceValue();
143 if (!refVal
.getApp().equals(query
.getApp())) {
144 throw new IllegalQueryException(
145 Entity
.KEY_RESERVED_PROPERTY
+ " filter app is " +
146 refVal
.getApp() + " but query app is " + query
.getApp(),
147 IllegalQueryType
.ILLEGAL_VALUE
);
149 if (!refVal
.getNameSpace().equals(query
.getNameSpace())) {
150 throw new IllegalQueryException(
151 Entity
.KEY_RESERVED_PROPERTY
+ " filter namespace is " +
152 refVal
.getNameSpace() + " but query namespace is " + query
.getNameSpace(),
153 IllegalQueryType
.ILLEGAL_VALUE
);
157 if (INEQUALITY_OPERATORS
.contains(filter
.getOpEnum())) {
158 if (ineqProp
== null) {
160 } else if (!ineqProp
.equals(propName
)) {
161 throw new IllegalQueryException(
162 String
.format("Only one inequality filter per query is supported. "
163 + "Encountered both %s and %s", ineqProp
, propName
),
164 IllegalQueryType
.MULTIPLE_INEQ_FILTERS
);
166 } else if (filter
.getOpEnum() == Operator
.EQUAL
) {
167 if (projectionProperties
.contains(propName
)) {
168 throw new IllegalQueryException(
169 "cannot use projection on a property with an equality filter",
170 IllegalQueryType
.ILLEGAL_PROJECTION
);
171 } else if (groupBySet
.contains(propName
)) {
172 throw new IllegalQueryException(
173 "cannot use group by on a property with an equality filter",
174 IllegalQueryType
.ILLEGAL_GROUPBY
);
176 } else if (filter
.getOpEnum() == Operator
.CONTAINED_IN_REGION
) {
178 if (!filter
.hasGeoRegion() || prop
.getValue().hasPointValue()) {
179 throw new IllegalQueryException(
181 "Geo-spatial filter on %s should specify GeoRegion rather than Property Value",
182 propName
), IllegalQueryType
.UNSUPPORTED_FILTER
);
184 GeoRegion region
= filter
.getGeoRegion();
185 if (region
.hasCircle() && region
.hasRectangle()
186 || !region
.hasCircle() && !region
.hasRectangle()) {
187 throw new IllegalQueryException(
189 "Geo-spatial filter on %s should specify Circle or Rectangle, but not both",
190 propName
), IllegalQueryType
.UNSUPPORTED_FILTER
);
192 } else if (UNSUPPORTED_OPERATORS
.contains(filter
.getOpEnum())) {
193 throw new IllegalQueryException(
194 String
.format("Unsupported filter operator: %s", filter
.getOp()),
195 IllegalQueryType
.UNSUPPORTED_FILTER
);
200 if (ineqProp
!= null) {
201 throw new IllegalQueryException(
202 "Inequality filter with geo-spatial query is not supported.",
203 IllegalQueryType
.UNSUPPORTED_FILTER
);
206 if (query
.hasAncestor()) {
207 throw new IllegalQueryException(
208 "Geo-spatial filter on ancestor query is not supported.",
209 IllegalQueryType
.UNSUPPORTED_FILTER
);
212 if (query
.hasCompiledCursor() || query
.hasEndCompiledCursor()) {
213 throw new IllegalQueryException(
214 "Start and end cursors are not supported on geo-spatial queries.",
215 IllegalQueryType
.CURSOR_NOT_SUPPORTED
);
219 if (ineqProp
!= null && query
.groupByPropertyNameSize() > 0) {
220 if (!groupBySet
.contains(ineqProp
)) {
221 throw new IllegalQueryException(
222 String
.format("Inequality filter on %s must also be a group by property when "
223 + "group by properties are set.", ineqProp
), IllegalQueryType
.ILLEGAL_GROUPBY
);
227 if (ineqProp
!= null) {
228 if (query
.orderSize() > 0) {
229 if (!ineqProp
.equals(query
.getOrder(0).getProperty())) {
230 throw new IllegalQueryException(
231 String
.format("The first sort property must be the same as the property to which "
232 + "the inequality filter is applied. In your query the first sort property is "
233 + "%s but the inequality filter is on %s",
234 query
.getOrder(0).getProperty(), ineqProp
),
235 IllegalQueryType
.FIRST_SORT_NEQ_INEQ_PROP
);
241 public boolean isGeo() {
246 public boolean equals(Object o
) {
250 if (o
== null || getClass() != o
.getClass()) {
254 ValidatedQuery that
= (ValidatedQuery
) o
;
256 if (!query
.equals(that
.query
)) {
264 public int hashCode() {
265 return query
.hashCode();
268 enum IllegalQueryType
{
271 FILTER_WITH_MULTIPLE_PROPS
,
272 MULTIPLE_INEQ_FILTERS
,
273 FIRST_SORT_NEQ_INEQ_PROP
,
274 TRANSACTION_REQUIRES_ANCESTOR
,
278 CURSOR_NOT_SUPPORTED
,
281 static class IllegalQueryException
extends ApiProxy
.ApplicationException
{
283 private final IllegalQueryType illegalQueryType
;
285 IllegalQueryException(String errorDetail
,
286 IllegalQueryType illegalQueryType
) {
287 super(DatastoreV3Pb
.Error
.ErrorCode
.BAD_REQUEST
.getValue(), errorDetail
);
288 this.illegalQueryType
= illegalQueryType
;
291 IllegalQueryType
getIllegalQueryType() {
292 return illegalQueryType
;