App Engine Java SDK version 1.9.25
[gae.git] / java / src / main / com / google / appengine / api / datastore / ValidatedQuery.java
blob6f4219c3f39866bae58dcd4efa814fcf6024e238
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;
17 import java.util.Set;
19 /**
20 * Wrapper around {@link Query} that performs validation.
22 class ValidatedQuery extends NormalizedQuery {
23 static final Set<Operator> UNSUPPORTED_OPERATORS = makeImmutableSet(
24 Operator.IN);
26 private boolean isGeo;
28 /**
29 * @throws IllegalQueryException If the provided query fails validation.
31 ValidatedQuery(Query query) {
32 super(query);
33 validateQuery();
36 /**
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;
123 this.isGeo = false;
124 for (Filter filter : query.filters()) {
125 int numProps = filter.propertySize();
126 if (numProps != 1) {
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) {
159 ineqProp = propName;
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) {
177 isGeo = true;
178 if (!filter.hasGeoRegion() || prop.getValue().hasPointValue()) {
179 throw new IllegalQueryException(
180 String.format(
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(
188 String.format(
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);
199 if (isGeo) {
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() {
242 return isGeo;
245 @Override
246 public boolean equals(Object o) {
247 if (this == o) {
248 return true;
250 if (o == null || getClass() != o.getClass()) {
251 return false;
254 ValidatedQuery that = (ValidatedQuery) o;
256 if (!query.equals(that.query)) {
257 return false;
260 return true;
263 @Override
264 public int hashCode() {
265 return query.hashCode();
268 enum IllegalQueryType {
269 KIND_REQUIRED,
270 UNSUPPORTED_FILTER,
271 FILTER_WITH_MULTIPLE_PROPS,
272 MULTIPLE_INEQ_FILTERS,
273 FIRST_SORT_NEQ_INEQ_PROP,
274 TRANSACTION_REQUIRES_ANCESTOR,
275 ILLEGAL_VALUE,
276 ILLEGAL_PROJECTION,
277 ILLEGAL_GROUPBY,
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;