1 package com
.google
.appengine
.api
.datastore
;
3 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
;
4 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.Query
.Filter
;
5 import com
.google
.apphosting
.datastore
.DatastoreV3Pb
.Query
.Order
;
6 import com
.google
.common
.collect
.Lists
;
7 import com
.google
.common
.collect
.Sets
;
8 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.Index
;
9 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.Index
.Property
;
10 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.Index
.Property
.Direction
;
11 import com
.google
.storage
.onestore
.v3
.OnestoreEntity
.Index
.Property
.Mode
;
13 import java
.util
.ArrayList
;
14 import java
.util
.Collection
;
15 import java
.util
.Collections
;
16 import java
.util
.Comparator
;
17 import java
.util
.HashMap
;
18 import java
.util
.List
;
23 * Composite index management operations needed by the datastore api.
25 public class CompositeIndexManager
{
28 * The format of a datastore-index XML element when it has properties.
30 private static final String DATASTORE_INDEX_WITH_PROPERTIES_XML_FORMAT
=
31 " <datastore-index kind=\"%s\" %s source=\"%s\">\n%s"
32 + " </datastore-index>\n\n";
33 private static final String ANCESTOR_ATTRIBUTE_FORMAT
= "ancestor=\"%s\"";
36 * The format of a datastore-index XML element when it does not have
39 private static final String DATASTORE_INDEX_NO_PROPERTIES_XML_FORMAT
=
40 " <datastore-index kind=\"%s\" ancestor=\"%s\" source=\"%s\"/>\n\n";
43 * The format of a property XML element.
45 private static final String PROPERTY_XML_FORMAT
=
46 " <property name=\"%s\" %s/>\n";
47 private static final String ASC_ATTRIBUTE
= "direction=\"asc\"";
48 private static final String DESC_ATTRIBUTE
= "direction=\"desc\"";
49 private static final String GEOSPATIAL_ATTRIBUTE
= "mode=\"geospatial\"";
52 * Apologies for the lowercase literals, but the whole point of these enums
53 * is to represent constants in an xml document, and it's silly to have
54 * the code literals not match the xml literals - you end up with a bunch
55 * of case conversion just support the java naming conversion.
59 * The source of an index in the index file. These are used as literals
60 * in an xml document that we read and write.
62 protected enum IndexSource
{ auto
, manual
}
65 * Generate an xml representation of the provided {@link Index}.
67 * <datastore-indexes autoGenerate="true">
68 * <datastore-index kind="a" ancestor="false">
69 * <property name="yam" direction="asc"/>
70 * <property name="not yam" direction="desc"/>
72 * </datastore-indexes>
74 * @param index The index for which we want an xml representation.
75 * @param source The source of the provided index.
76 * @return The xml representation of the provided index.
78 protected String
generateXmlForIndex(Index index
, IndexSource source
) {
79 boolean isAncestor
= index
.isAncestor();
80 if (index
.propertySize() == 0) {
82 DATASTORE_INDEX_NO_PROPERTIES_XML_FORMAT
,
83 index
.getEntityType(), isAncestor
, source
);
85 boolean isSearchIndex
= false;
86 StringBuilder sb
= new StringBuilder();
87 for (Property prop
: index
.propertys()) {
88 String extraAttribute
;
89 if (prop
.getDirectionEnum() == Direction
.ASCENDING
) {
90 extraAttribute
= ASC_ATTRIBUTE
;
91 } else if (prop
.getDirectionEnum() == Direction
.DESCENDING
) {
92 extraAttribute
= DESC_ATTRIBUTE
;
93 } else if (prop
.getModeEnum() == Mode
.GEOSPATIAL
) {
95 extraAttribute
= GEOSPATIAL_ATTRIBUTE
;
99 sb
.append(String
.format(PROPERTY_XML_FORMAT
, prop
.getName(), extraAttribute
));
101 String ancestorAttribute
= isSearchIndex ?
""
102 : String
.format(ANCESTOR_ATTRIBUTE_FORMAT
, isAncestor
);
103 return String
.format(
104 DATASTORE_INDEX_WITH_PROPERTIES_XML_FORMAT
,
105 index
.getEntityType(), ancestorAttribute
, source
, sb
.toString());
109 * Given a {@link IndexComponentsOnlyQuery}, return the {@link Index}
110 * needed to fulfill the query, or {@code null} if no index is needed.
112 * This code needs to remain in sync with its counterparts in other
113 * languages. If you modify this code please make sure you make the
114 * same update in the local datastore for other languages.
116 * @param indexOnlyQuery The query.
117 * @return The index that must be present in order to fulfill the query, or
118 * {@code null} if no index is needed.
120 protected Index
compositeIndexForQuery(final IndexComponentsOnlyQuery indexOnlyQuery
) {
121 DatastoreV3Pb
.Query query
= indexOnlyQuery
.getQuery();
123 boolean hasKind
= query
.hasKind();
124 boolean isAncestor
= query
.hasAncestor();
125 List
<Filter
> filters
= query
.filters();
126 List
<Order
> orders
= query
.orders();
128 if (filters
.isEmpty() && orders
.isEmpty()) {
132 List
<String
> eqProps
= indexOnlyQuery
.getPrefix();
133 List
<Property
> indexProperties
= getRecommendedIndexProps(indexOnlyQuery
);
135 if (hasKind
&& !eqProps
.isEmpty() &&
136 eqProps
.size() == filters
.size() &&
137 !indexOnlyQuery
.hasKeyProperty() &&
142 if (hasKind
&& !isAncestor
&& indexProperties
.size() <= 1 &&
143 (!indexOnlyQuery
.hasKeyProperty() ||
144 indexProperties
.get(0).getDirectionEnum() == Property
.Direction
.ASCENDING
)) {
148 Index index
= new Index();
149 index
.setEntityType(query
.getKind());
150 index
.setAncestor(isAncestor
);
151 index
.mutablePropertys().addAll(indexProperties
);
156 * We compare {@link Property Properties} by comparing their names.
158 private static final Comparator
<Property
> PROPERTY_NAME_COMPARATOR
= new Comparator
<Property
>() {
160 public int compare(Property o1
, Property o2
) {
161 return o1
.getName().compareTo(o2
.getName());
165 private List
<Property
> getRecommendedIndexProps(IndexComponentsOnlyQuery query
) {
166 List
<Property
> indexProps
= new ArrayList
<Property
>();
168 indexProps
.addAll(new UnorderedIndexComponent(Sets
.newHashSet(query
.getPrefix()))
169 .preferredIndexProperties());
171 for (IndexComponent component
: query
.getPostfix()) {
172 indexProps
.addAll(component
.preferredIndexProperties());
179 * Given a {@link IndexComponentsOnlyQuery} and a collection of existing
180 * {@link Index}s, return the minimum {@link Index} needed to fulfill
181 * the query, or {@code null} if no index is needed.
183 * This code needs to remain in sync with its counterparts in other
184 * languages. If you modify this code please make sure you make the
185 * same update in the local datastore for other languages.
187 * @param indexOnlyQuery The query.
188 * @param indexes The existing indexes.
189 * @return The minimum index that must be present in order to fulfill the
190 * query, or {@code null} if no index is needed.
192 protected Index
minimumCompositeIndexForQuery(IndexComponentsOnlyQuery indexOnlyQuery
,
193 Collection
<Index
> indexes
) {
195 Index suggestedIndex
= compositeIndexForQuery(indexOnlyQuery
);
196 if (suggestedIndex
== null) {
200 class EqPropsAndAncestorConstraint
{
201 final Set
<String
> equalityProperties
;
202 final boolean ancestorConstraint
;
204 EqPropsAndAncestorConstraint(Set
<String
> equalityProperties
, boolean ancestorConstraint
) {
205 this.equalityProperties
= equalityProperties
;
206 this.ancestorConstraint
= ancestorConstraint
;
210 Map
<List
<Property
>, EqPropsAndAncestorConstraint
> remainingMap
=
211 new HashMap
<List
<Property
>, EqPropsAndAncestorConstraint
>();
214 for (Index index
: indexes
) {
216 !indexOnlyQuery
.getQuery().getKind().equals(index
.getEntityType()) ||
217 (!indexOnlyQuery
.getQuery().hasAncestor() && index
.isAncestor())) {
221 int postfixSplit
= index
.propertySize();
222 for (IndexComponent component
: Lists
.reverse(indexOnlyQuery
.getPostfix())) {
223 if (!component
.matches(index
.propertys().subList(
224 Math
.max(postfixSplit
- component
.size(), 0), postfixSplit
))) {
227 postfixSplit
-= component
.size();
230 Set
<String
> indexEqProps
= Sets
.newHashSetWithExpectedSize(postfixSplit
);
231 for (Property prop
: index
.propertys().subList(0, postfixSplit
)) {
232 if (!indexOnlyQuery
.getPrefix().contains(prop
.getName())) {
235 indexEqProps
.add(prop
.getName());
238 List
<Property
> indexPostfix
= index
.propertys().subList(postfixSplit
, index
.propertySize());
240 Set
<String
> remainingEqProps
;
241 boolean remainingAncestor
;
243 EqPropsAndAncestorConstraint remaining
= remainingMap
.get(indexPostfix
);
244 if (remaining
== null) {
245 remainingEqProps
= Sets
.newHashSet(indexOnlyQuery
.getPrefix());
246 remainingAncestor
= indexOnlyQuery
.getQuery().hasAncestor();
248 remainingEqProps
= remaining
.equalityProperties
;
249 remainingAncestor
= remaining
.ancestorConstraint
;
253 boolean modified
= remainingEqProps
.removeAll(indexEqProps
);
254 if (remainingAncestor
&& index
.isAncestor()) {
256 remainingAncestor
= false;
259 if (remainingEqProps
.isEmpty() && !remainingAncestor
) {
267 remainingMap
.put(indexPostfix
,
268 new EqPropsAndAncestorConstraint(remainingEqProps
, remainingAncestor
));
271 if (remainingMap
.isEmpty()) {
272 return suggestedIndex
;
275 int minimumCost
= Integer
.MAX_VALUE
;
276 List
<Property
> minimumPostfix
= null;
277 EqPropsAndAncestorConstraint minimumRemaining
= null;
278 for (Map
.Entry
<List
<Property
>, EqPropsAndAncestorConstraint
> entry
: remainingMap
.entrySet()) {
279 int cost
= entry
.getValue().equalityProperties
.size();
280 if (entry
.getValue().ancestorConstraint
) {
283 if (cost
< minimumCost
) {
285 minimumPostfix
= entry
.getKey();
286 minimumRemaining
= entry
.getValue();
290 suggestedIndex
.clearProperty();
291 suggestedIndex
.setAncestor(minimumRemaining
.ancestorConstraint
);
292 for (String name
: minimumRemaining
.equalityProperties
) {
293 suggestedIndex
.addProperty().setName(name
).setDirection(Direction
.ASCENDING
);
295 Collections
.sort(suggestedIndex
.mutablePropertys(), PROPERTY_NAME_COMPARATOR
);
297 suggestedIndex
.mutablePropertys().addAll(minimumPostfix
);
298 return suggestedIndex
;
302 * Protected alias that allows us to make this class available to the local
303 * datastore without publicly exposing it in the api.
305 protected static class IndexComponentsOnlyQuery
306 extends com
.google
.appengine
.api
.datastore
.IndexComponentsOnlyQuery
{
307 public IndexComponentsOnlyQuery(DatastoreV3Pb
.Query query
) {
313 * Protected alias that allows us to make this class available to the local
314 * datastore without publicly exposing it in the api.
316 protected static class ValidatedQuery
317 extends com
.google
.appengine
.api
.datastore
.ValidatedQuery
{
318 public ValidatedQuery(DatastoreV3Pb
.Query query
) {
324 * Protected alias that allows us to make this class available to the local
325 * datastore without publicly exposing it in the api.
327 protected static class KeyTranslator
extends com
.google
.appengine
.api
.datastore
.KeyTranslator
{
328 protected KeyTranslator() { }