All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.appengine.api.datastore.CompositeIndexManager Maven / Gradle / Ivy

Go to download

API for Google App Engine standard environment with some of the dependencies shaded (repackaged)

There is a newer version: 2.0.27
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.appengine.api.datastore;

import static java.util.Objects.requireNonNull;

import com.google.apphosting.datastore.DatastoreV3Pb;
import com.google.apphosting.datastore.DatastoreV3Pb.Query.Filter;
import com.google.apphosting.datastore.DatastoreV3Pb.Query.Order;
import com.google.common.base.Function;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.storage.onestore.v3.OnestoreEntity.Index;
import com.google.storage.onestore.v3.OnestoreEntity.Index.Property;
import com.google.storage.onestore.v3.OnestoreEntity.Index.Property.Direction;
import com.google.storage.onestore.v3.OnestoreEntity.Index.Property.Mode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.Nullable;

// CAUTION: this is one of several files that implement parsing and
// validation of the index definition schema; they all must be kept in
// sync.  Please refer to java/com/google/appengine/tools/development/datastore-indexes.xsd
// for the list of these files.

/** Composite index management operations needed by the datastore api. */
// This class is public because the dev appserver needs access to it.
public class CompositeIndexManager {

  // Apologies for the lowercase literals, but the whole point of these enums
  // is to represent constants in an xml document, and it's silly to have
  // the code literals not match the xml literals - you end up with a bunch
  // of case conversion just support the java naming conversion.

  /**
   * The source of an index in the index file. These are used as literals in an xml document that we
   * read and write.
   */
  protected enum IndexSource {
    auto,
    manual
  }

  /**
   * Generate an xml representation of the provided {@link Index}.
   *
   * 

* * * * @param index The index for which we want an xml representation. * @param source The source of the provided index. * @return The xml representation of the provided index. */ protected String generateXmlForIndex(Index index, IndexSource source) { return CompositeIndexUtils.generateXmlForIndex(index, source); } /** * Given a {@link IndexComponentsOnlyQuery}, return the {@link Index} needed to fulfill the query, * or {@code null} if no index is needed. * *

This code needs to remain in sync with its counterparts in other languages. If you modify * this code please make sure you make the same update in the local datastore for other languages. * * @param indexOnlyQuery The query. * @return The index that must be present in order to fulfill the query, or {@code null} if no * index is needed. */ protected @Nullable Index compositeIndexForQuery(final IndexComponentsOnlyQuery indexOnlyQuery) { DatastoreV3Pb.Query query = indexOnlyQuery.getQuery(); boolean hasKind = query.hasKind(); boolean isAncestor = query.hasAncestor(); List filters = query.filters(); List orders = query.orders(); if (filters.isEmpty() && orders.isEmpty()) { // If there are no filters or sorts no composite index is needed; the // built-in primary key or kind index can handle this. return null; } // Group the filters by operator. List eqProps = indexOnlyQuery.getPrefix(); List indexProperties = indexOnlyQuery.isGeo() ? getNeededSearchProps(eqProps, indexOnlyQuery.getGeoProperties()) : getRecommendedIndexProps(indexOnlyQuery); if (hasKind && !eqProps.isEmpty() && eqProps.size() == filters.size() && !indexOnlyQuery.hasKeyProperty() && orders.isEmpty()) { // No sort orders, all filters (if any) are equals filters, and none of // the filters are on the key - the // datastore can merge join this, with or without an ancestor. No index // needed. Non-empty equality filters is a critical part of this check // because without it we would capture queries with only kind and ancestor // specified, and those queries can _not_ be satisfied by merge-join. return null; } if (hasKind && !isAncestor && indexProperties.size() <= 1 && !indexOnlyQuery.isGeo() && (!indexOnlyQuery.hasKeyProperty() || indexProperties.get(0).getDirectionEnum() == Property.Direction.ASCENDING)) { // For traditional indexes, we never need kind-only or // single-property composite indexes unless it's a single // property, descending index on key. The built-in primary key // and single property indexes are good enough. (But for geo // (a.k.a. Search) indexes, we might.) return null; } Index index = new Index(); index.setEntityType(query.getKind()); index.setAncestor(isAncestor); index.mutablePropertys().addAll(indexProperties); return index; } /** We compare {@link Property Properties} by comparing their names. */ private static final Comparator PROPERTY_NAME_COMPARATOR = new Comparator() { @Override public int compare(Property o1, Property o2) { return o1.getName().compareTo(o2.getName()); } }; private List getRecommendedIndexProps(IndexComponentsOnlyQuery query) { // Construct the list of index properties List indexProps = new ArrayList(); // This converts our Set prefix to be a list of {@link Property}s with ascending // direction. The list will be ordered lexicographically. indexProps.addAll( new UnorderedIndexComponent(Sets.newHashSet(query.getPrefix())).preferredIndexProperties()); for (IndexComponent component : query.getPostfix()) { indexProps.addAll(component.preferredIndexProperties()); } return indexProps; } /** * Function which can transform a property name into a Property pb object, optionally including a * Mode setting, suitable for use in defining a Search index in normalized order. */ static class SearchPropertyTransform implements Function { private final @Nullable Mode mode; SearchPropertyTransform(@Nullable Mode mode) { this.mode = mode; } @Override public Property apply(String name) { Property p = new Property(); p.setName(name); if (mode != null) { p.setMode(mode); } return p; } } private static final SearchPropertyTransform TO_MODELESS_PROPERTY = new SearchPropertyTransform(null); private static final SearchPropertyTransform TO_GEOSPATIAL_PROPERTY = new SearchPropertyTransform(Mode.GEOSPATIAL); /** * Produces the list of Property objects needed for a Search index, properly normalized: all * pre-intersection (i.e., modeless) properties come first, followed by all geo-spatial * properties. Within type the properties appear in lexicographical order by name. */ private List getNeededSearchProps(List eqProps, List searchProps) { List result = new ArrayList<>(); result.addAll( FluentIterable.from(eqProps) .transform(TO_MODELESS_PROPERTY) .toSortedList(PROPERTY_NAME_COMPARATOR)); result.addAll( FluentIterable.from(searchProps) .transform(TO_GEOSPATIAL_PROPERTY) .toSortedList(PROPERTY_NAME_COMPARATOR)); return result; } /** * Given a {@link IndexComponentsOnlyQuery} and a collection of existing {@link Index}s, return * the minimum {@link Index} needed to fulfill the query, or {@code null} if no index is needed. * *

This code needs to remain in sync with its counterparts in other languages. If you modify * this code please make sure you make the same update in the local datastore for other languages. * * @param indexOnlyQuery The query. * @param indexes The existing indexes. * @return The minimum index that must be present in order to fulfill the query, or {@code null} * if no index is needed. */ protected @Nullable Index minimumCompositeIndexForQuery( IndexComponentsOnlyQuery indexOnlyQuery, Collection indexes) { Index suggestedIndex = compositeIndexForQuery(indexOnlyQuery); if (suggestedIndex == null) { return null; } if (indexOnlyQuery.isGeo()) { // None of the shortcuts/optimizations below are applicable for Search indexes. return suggestedIndex; } class EqPropsAndAncestorConstraint { final Set equalityProperties; final boolean ancestorConstraint; EqPropsAndAncestorConstraint(Set equalityProperties, boolean ancestorConstraint) { this.equalityProperties = equalityProperties; this.ancestorConstraint = ancestorConstraint; } } // Map from postfix to the remaining equality properties and ancestor constraints. Map, EqPropsAndAncestorConstraint> remainingMap = new HashMap, EqPropsAndAncestorConstraint>(); index_for: for (Index index : indexes) { if ( // Kind must match. !indexOnlyQuery.getQuery().getKind().equals(index.getEntityType()) || // Ancestor indexes can only be used on ancestor queries. (!indexOnlyQuery.getQuery().hasAncestor() && index.isAncestor())) { continue; } // Matching the postfix. int postfixSplit = index.propertySize(); for (IndexComponent component : Lists.reverse(indexOnlyQuery.getPostfix())) { if (!component.matches( index .propertys() .subList(Math.max(postfixSplit - component.size(), 0), postfixSplit))) { continue index_for; } postfixSplit -= component.size(); } // Postfix matches! Now checking the prefix. Set indexEqProps = Sets.newHashSetWithExpectedSize(postfixSplit); for (Property prop : index.propertys().subList(0, postfixSplit)) { // Index must not contain extra properties in the prefix. if (!indexOnlyQuery.getPrefix().contains(prop.getName())) { continue index_for; } indexEqProps.add(prop.getName()); } // Index matches! // Find the matching remaining requirements. List indexPostfix = index.propertys().subList(postfixSplit, index.propertySize()); Set remainingEqProps; boolean remainingAncestor; { EqPropsAndAncestorConstraint remaining = remainingMap.get(indexPostfix); if (remaining == null) { remainingEqProps = Sets.newHashSet(indexOnlyQuery.getPrefix()); remainingAncestor = indexOnlyQuery.getQuery().hasAncestor(); } else { remainingEqProps = remaining.equalityProperties; remainingAncestor = remaining.ancestorConstraint; } } // Remove any remaining requirements handled by this index. boolean modified = remainingEqProps.removeAll(indexEqProps); if (remainingAncestor && index.isAncestor()) { modified = true; remainingAncestor = false; } if (remainingEqProps.isEmpty() && !remainingAncestor) { return null; // No new index needed! } if (!modified) { // Index made no contribution, don't update the map. continue; } // Save indexes contribution remainingMap.put( indexPostfix, new EqPropsAndAncestorConstraint(remainingEqProps, remainingAncestor)); } if (remainingMap.isEmpty()) { return suggestedIndex; // suggested index is the minimum index } int minimumCost = Integer.MAX_VALUE; List minimumPostfix = null; EqPropsAndAncestorConstraint minimumRemaining = null; for (Map.Entry, EqPropsAndAncestorConstraint> entry : remainingMap.entrySet()) { int cost = entry.getValue().equalityProperties.size(); if (entry.getValue().ancestorConstraint) { cost += 2; // Arbitrary value picked because ancestor are multi-valued. } if (cost < minimumCost) { minimumCost = cost; minimumPostfix = entry.getKey(); minimumRemaining = entry.getValue(); } } requireNonNull(minimumRemaining); // map not empty so we should have found cost < MAX_VALUE. requireNonNull(minimumPostfix); // Populating suggesting the minimal index instead. suggestedIndex.clearProperty(); suggestedIndex.setAncestor(minimumRemaining.ancestorConstraint); for (String name : minimumRemaining.equalityProperties) { suggestedIndex.addProperty().setName(name).setDirection(Direction.ASCENDING); } Collections.sort(suggestedIndex.mutablePropertys(), PROPERTY_NAME_COMPARATOR); suggestedIndex.mutablePropertys().addAll(minimumPostfix); return suggestedIndex; } /** * Protected alias that allows us to make this class available to the local datastore without * publicly exposing it in the api. */ protected static class IndexComponentsOnlyQuery extends com.google.appengine.api.datastore.IndexComponentsOnlyQuery { public IndexComponentsOnlyQuery(DatastoreV3Pb.Query query) { super(query); } } /** * Protected alias that allows us to make this class available to the local datastore without * publicly exposing it in the api. */ protected static class ValidatedQuery extends com.google.appengine.api.datastore.ValidatedQuery { public ValidatedQuery(DatastoreV3Pb.Query query) { super(query); } } /** * Protected alias that allows us to make this class available to the local datastore without * publicly exposing it in the api. */ protected static class KeyTranslator extends com.google.appengine.api.datastore.KeyTranslator { protected KeyTranslator() {} } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy