1 // Copyright 2009 Google Inc. All Rights Reserved.
2 package com
.google
.appengine
.api
.datastore
;
4 import com
.google
.apphosting
.api
.DatastorePb
;
5 import com
.google
.apphosting
.api
.DatastorePb
.Query
.Filter
;
6 import com
.google
.apphosting
.api
.DatastorePb
.Query
.Order
;
7 import com
.google
.common
.base
.Pair
;
8 import com
.google
.common
.collect
.Lists
;
9 import com
.google
.common
.collect
.Sets
;
10 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.Index
;
11 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.Index
.Property
;
12 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.Index
.Property
.Direction
;
14 import java
.util
.ArrayList
;
15 import java
.util
.Collection
;
16 import java
.util
.Collections
;
17 import java
.util
.Comparator
;
18 import java
.util
.HashMap
;
19 import java
.util
.Iterator
;
20 import java
.util
.List
;
25 * Composite index management operations needed by the datastore api.
28 public class CompositeIndexManager
{
31 * The format of a datastore-index xml element when it has properties.
33 private static final String DATASTORE_INDEX_WITH_PROPERTIES_XML_FORMAT
=
34 " <datastore-index kind=\"%s\" ancestor=\"%s\" source=\"%s\">\n%s"
35 + " </datastore-index>\n\n";
38 * The format of a datastore-index xml element when it does not have
41 private static final String DATASTORE_INDEX_NO_PROPERTIES_XML_FORMAT
=
42 " <datastore-index kind=\"%s\" ancestor=\"%s\" source=\"%s\"/>\n\n";
45 * The format of a property xml element.
47 private static final String PROPERTY_XML_FORMAT
=
48 " <property name=\"%s\" direction=\"%s\"/>\n";
51 * Apologies for the lowercase literals, but the whole point of these enums
52 * is to represent constants in an xml document, and it's silly to have
53 * the code literals not match the xml literals - you end up with a bunch
54 * of case conversion just support the java naming conversion.
58 * The source of an index in the index file. These are used as literals
59 * in an xml document that we read and write.
61 protected enum IndexSource
{ auto
, manual
}
64 * Generate an xml representation of the provided {@link Index}.
66 * <datastore-indexes autoGenerate="true">
67 * <datastore-index kind="a" ancestor="false">
68 * <property name="yam" direction="asc"/>
69 * <property name="not yam" direction="desc"/>
71 * </datastore-indexes>
73 * @param index The index for which we want an xml representation.
74 * @param source The source of the provided index.
75 * @return The xml representation of the provided index.
77 protected String
generateXmlForIndex(Index index
, IndexSource source
) {
78 boolean isAncestor
= index
.isAncestor();
79 if (index
.propertySize() == 0) {
81 DATASTORE_INDEX_NO_PROPERTIES_XML_FORMAT
,
82 index
.getEntityType(), isAncestor
, source
);
84 StringBuilder sb
= new StringBuilder();
85 for (Property prop
: index
.propertys()) {
86 String dir
= prop
.getDirectionEnum() == Direction
.ASCENDING ?
"asc" : "desc";
87 sb
.append(String
.format(PROPERTY_XML_FORMAT
, prop
.getName(), dir
));
90 DATASTORE_INDEX_WITH_PROPERTIES_XML_FORMAT
,
91 index
.getEntityType(), isAncestor
, source
, sb
.toString());
95 * Given a {@link IndexComponentsOnlyQuery}, return the {@link Index}
96 * needed to fulfill the query, or {@code null} if no index is needed.
98 * This code needs to remain in sync with its counterparts in other
99 * languages. If you modify this code please make sure you make the
100 * same update in the local datastore for other languages.
102 * @param indexOnlyQuery The query.
103 * @return The index that must be present in order to fulfill the query, or
104 * {@code null} if no index is needed.
106 protected Index
compositeIndexForQuery(final IndexComponentsOnlyQuery indexOnlyQuery
) {
107 DatastorePb
.Query query
= indexOnlyQuery
.getQuery();
109 boolean hasKind
= query
.hasKind();
110 boolean isAncestor
= query
.hasAncestor();
111 List
<Filter
> filters
= query
.filters();
112 List
<Order
> orders
= query
.orders();
114 if (filters
.isEmpty() && orders
.isEmpty()) {
118 Set
<String
> eqProps
= indexOnlyQuery
.getEqualityProps();
119 List
<Property
> indexProperties
= getRecommendedIndexProps(indexOnlyQuery
);
121 if (hasKind
&& !eqProps
.isEmpty() &&
122 eqProps
.size() == filters
.size() &&
123 !indexOnlyQuery
.hasKeyProperty() &&
128 if (hasKind
&& !isAncestor
&& indexProperties
.size() <= 1 &&
129 (!indexOnlyQuery
.hasKeyProperty() ||
130 indexProperties
.get(0).getDirectionEnum() == Property
.Direction
.ASCENDING
)) {
134 Index index
= new Index();
135 index
.setEntityType(query
.getKind());
136 index
.setAncestor(isAncestor
);
137 index
.mutablePropertys().addAll(indexProperties
);
141 private static Property
newIndexProperty(String name
, Direction direction
) {
142 Property indexProperty
= new Property();
143 indexProperty
.setName(name
);
144 indexProperty
.setDirection(direction
);
145 return indexProperty
;
149 * We compare {@link Property Properties} by comparing their names.
151 private static final Comparator
<Property
> PROPERTY_NAME_COMPARATOR
= new Comparator
<Property
>() {
153 public int compare(Property o1
, Property o2
) {
154 return o1
.getName().compareTo(o2
.getName());
158 private List
<Property
> getRecommendedIndexProps(IndexComponentsOnlyQuery query
) {
159 List
<Property
> indexProps
= new ArrayList
<Property
>(
160 query
.getEqualityProps().size() + query
.getOrderProps().size() +
161 query
.getExistsProps().size());
163 for (String name
: query
.getEqualityProps()) {
164 indexProps
.add(newIndexProperty(name
, Direction
.ASCENDING
));
167 Collections
.sort(indexProps
, PROPERTY_NAME_COMPARATOR
);
169 for (Property orderProp
: query
.getOrderProps()) {
170 if (!orderProp
.hasDirection()) {
171 orderProp
= orderProp
.clone();
172 orderProp
.setDirection(Direction
.ASCENDING
);
174 indexProps
.add(orderProp
);
177 List
<String
> sortedExists
= new ArrayList
<String
>(query
.getExistsProps());
178 Collections
.sort(sortedExists
);
179 for (String name
: sortedExists
) {
180 indexProps
.add(newIndexProperty(name
, Direction
.ASCENDING
));
187 * Given a {@link IndexComponentsOnlyQuery} and a collection of existing
188 * {@link Index}s, return the minimum {@link Index} needed to fulfill
189 * the query, or {@code null} if no index is needed.
191 * This code needs to remain in sync with its counterparts in other
192 * languages. If you modify this code please make sure you make the
193 * same update in the local datastore for other languages.
195 * @param indexOnlyQuery The query.
196 * @param indexes The existing indexes.
197 * @return The minimum index that must be present in order to fulfill the
198 * query, or {@code null} if no index is needed.
200 protected Index
minimumCompositeIndexForQuery(IndexComponentsOnlyQuery indexOnlyQuery
,
201 Collection
<Index
> indexes
) {
203 Index suggestedIndex
= compositeIndexForQuery(indexOnlyQuery
);
204 if (suggestedIndex
== null) {
208 Map
<List
<Property
>, Pair
<Set
<String
>, Boolean
>> remainingMap
=
209 new HashMap
<List
<Property
>, Pair
<Set
<String
>, Boolean
>>();
212 for (Index index
: indexes
) {
214 !indexOnlyQuery
.getQuery().getKind().equals(index
.getEntityType()) ||
215 (!indexOnlyQuery
.getQuery().hasAncestor() && index
.isAncestor())) {
219 List
<String
> indexExistsList
=
220 Lists
.newArrayListWithExpectedSize(indexOnlyQuery
.getExistsProps().size());
221 int postfixSplit
= index
.propertySize() - 1;
222 for (; postfixSplit
>= 0; --postfixSplit
) {
223 String name
= index
.getProperty(postfixSplit
).getName();
224 if (!indexOnlyQuery
.getExistsProps().contains(name
)) {
227 indexExistsList
.add(name
);
231 if (!indexOnlyQuery
.getExistsProps().containsAll(indexExistsList
) ||
232 indexExistsList
.size() != indexOnlyQuery
.getExistsProps().size()) {
236 int postfixStart
= postfixSplit
- indexOnlyQuery
.getOrderProps().size();
237 if (postfixStart
< 0) {
241 List
<Property
> indexOrderProps
= index
.propertys().subList(postfixStart
, postfixSplit
);
242 Iterator
<Property
> indexItr
= indexOrderProps
.iterator();
243 for (Property prop
: indexOnlyQuery
.getOrderProps()) {
244 Property indexProp
= indexItr
.next();
245 if (!indexProp
.getName().equals(prop
.getName()) ||
246 (prop
.hasDirection() && prop
.getDirection() != indexProp
.getDirection())) {
251 Set
<String
> indexEqProps
= Sets
.newHashSetWithExpectedSize(postfixStart
);
252 for (Property prop
: index
.propertys().subList(0, postfixStart
)) {
253 indexEqProps
.add(prop
.getName());
256 if (!indexOnlyQuery
.getEqualityProps().containsAll(indexEqProps
)) {
260 List
<Property
> indexPostfix
= index
.propertys().subList(postfixStart
, index
.propertySize());
262 Set
<String
> remainingEqProps
;
263 boolean remainingAncestor
;
265 Pair
<Set
<String
>, Boolean
> remaining
= remainingMap
.get(indexPostfix
);
266 if (remaining
== null) {
267 remainingEqProps
= Sets
.newHashSet(indexOnlyQuery
.getEqualityProps());
268 remainingAncestor
= indexOnlyQuery
.getQuery().hasAncestor();
270 remainingEqProps
= remaining
.first
;
271 remainingAncestor
= remaining
.second
;
275 boolean modified
= remainingEqProps
.removeAll(indexEqProps
);
276 if (remainingAncestor
&& index
.isAncestor()) {
278 remainingAncestor
= false;
281 if (remainingEqProps
.isEmpty() && !remainingAncestor
) {
289 remainingMap
.put(indexPostfix
, Pair
.of(remainingEqProps
, remainingAncestor
));
292 if (remainingMap
.isEmpty()) {
293 return suggestedIndex
;
296 int minimumCost
= Integer
.MAX_VALUE
;
297 List
<Property
> minimumPostfix
= null;
298 Pair
<Set
<String
>, Boolean
> minimumRemaining
= null;
299 for (Map
.Entry
<List
<Property
>, Pair
<Set
<String
>, Boolean
>> entry
: remainingMap
.entrySet()) {
300 int cost
= entry
.getValue().first
.size();
301 if (entry
.getValue().second
) {
304 if (cost
< minimumCost
) {
306 minimumPostfix
= entry
.getKey();
307 minimumRemaining
= entry
.getValue();
311 suggestedIndex
.clearProperty();
312 suggestedIndex
.setAncestor(minimumRemaining
.second
);
313 for (String name
: minimumRemaining
.first
) {
314 suggestedIndex
.addProperty().setName(name
).setDirection(Direction
.ASCENDING
);
316 Collections
.sort(suggestedIndex
.mutablePropertys(), PROPERTY_NAME_COMPARATOR
);
318 suggestedIndex
.mutablePropertys().addAll(minimumPostfix
);
319 return suggestedIndex
;
323 * Protected alias that allows us to make this class available to the local
324 * datastore without publicly exposing it in the api.
326 protected static class IndexComponentsOnlyQuery
327 extends com
.google
.appengine
.api
.datastore
.IndexComponentsOnlyQuery
{
328 public IndexComponentsOnlyQuery(DatastorePb
.Query query
) {
334 * Protected alias that allows us to make this class available to the local
335 * datastore without publicly exposing it in the api.
337 protected static class ValidatedQuery
338 extends com
.google
.appengine
.api
.datastore
.ValidatedQuery
{
339 public ValidatedQuery(DatastorePb
.Query query
) {
345 * Protected alias that allows us to make this class available to the local
346 * datastore without publicly exposing it in the api.
348 protected static class KeyTranslator
extends com
.google
.appengine
.api
.datastore
.KeyTranslator
{
349 protected KeyTranslator() { }