Revision created by MOE tool push_codebase.
[gae.git] / java / src / main / com / google / appengine / api / datastore / ValidatedQuery.java
blob424f52e40fdbb6234ac421d38c7f5590dbe5d240
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;
15 import java.util.HashSet;
16 import java.util.Set;
18 /**
19 * Wrapper around {@link Query} that performs validation.
21 class ValidatedQuery extends NormalizedQuery {
22 static final Set<Operator> UNSUPPORTED_OPERATORS = makeImmutableSet(
23 Operator.IN);
25 private boolean isGeo;
27 /**
28 * @throws IllegalQueryException If the provided query fails validation.
30 ValidatedQuery(Query query) {
31 super(query);
32 validateQuery();
35 /**
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 this.isGeo = false;
107 for (Filter filter : query.filters()) {
108 int numProps = filter.propertySize();
109 if (numProps != 1) {
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) {
142 ineqProp = propName;
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) {
160 isGeo = true;
161 if (!filter.hasGeoRegion() || prop.getValue().hasPointValue()) {
162 throw new IllegalQueryException(
163 String.format(
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(
171 String.format(
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);
182 if (isGeo) {
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);
224 public boolean isGeo() {
225 return isGeo;
228 @Override
229 public boolean equals(Object o) {
230 if (this == o) {
231 return true;
233 if (o == null || getClass() != o.getClass()) {
234 return false;
237 ValidatedQuery that = (ValidatedQuery) o;
239 if (!query.equals(that.query)) {
240 return false;
243 return true;
246 @Override
247 public int hashCode() {
248 return query.hashCode();
251 enum IllegalQueryType {
252 KIND_REQUIRED,
253 UNSUPPORTED_FILTER,
254 FILTER_WITH_MULTIPLE_PROPS,
255 MULTIPLE_INEQ_FILTERS,
256 FIRST_SORT_NEQ_INEQ_PROP,
257 TRANSACTION_REQUIRES_ANCESTOR,
258 ILLEGAL_VALUE,
259 ILLEGAL_PROJECTION,
260 ILLEGAL_GROUPBY,
261 CURSOR_NOT_SUPPORTED,
264 static class IllegalQueryException extends ApiProxy.ApplicationException {
266 private final IllegalQueryType illegalQueryType;
268 IllegalQueryException(String errorDetail,
269 IllegalQueryType illegalQueryType) {
270 super(DatastoreV3Pb.Error.ErrorCode.BAD_REQUEST.getValue(), errorDetail);
271 this.illegalQueryType = illegalQueryType;
274 IllegalQueryType getIllegalQueryType() {
275 return illegalQueryType;