thirdparty/google_appengine/google/appengine/datastore/datastore_index.py
changeset 109 620f9b141567
child 686 df109be0567c
equal deleted inserted replaced
108:261778de26ff 109:620f9b141567
       
     1 #!/usr/bin/env python
       
     2 #
       
     3 # Copyright 2007 Google Inc.
       
     4 #
       
     5 # Licensed under the Apache License, Version 2.0 (the "License");
       
     6 # you may not use this file except in compliance with the License.
       
     7 # You may obtain a copy of the License at
       
     8 #
       
     9 #     http://www.apache.org/licenses/LICENSE-2.0
       
    10 #
       
    11 # Unless required by applicable law or agreed to in writing, software
       
    12 # distributed under the License is distributed on an "AS IS" BASIS,
       
    13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       
    14 # See the License for the specific language governing permissions and
       
    15 # limitations under the License.
       
    16 #
       
    17 
       
    18 """Primitives for dealing with datastore indexes.
       
    19 
       
    20 Example index.yaml file:
       
    21 ------------------------
       
    22 
       
    23 indexes:
       
    24 
       
    25 - kind: Cat
       
    26   ancestor: no
       
    27   properties:
       
    28   - name: name
       
    29   - name: age
       
    30     direction: desc
       
    31 
       
    32 - kind: Cat
       
    33   properties:
       
    34   - name: name
       
    35     direction: ascending
       
    36   - name: whiskers
       
    37     direction: descending
       
    38 
       
    39 - kind: Store
       
    40   ancestor: yes
       
    41   properties:
       
    42   - name: business
       
    43     direction: asc
       
    44   - name: owner
       
    45     direction: asc
       
    46 """
       
    47 
       
    48 
       
    49 
       
    50 
       
    51 
       
    52 from google.appengine.api import validation
       
    53 from google.appengine.api import yaml_errors
       
    54 from google.appengine.api import yaml_object
       
    55 from google.appengine.datastore import datastore_pb
       
    56 
       
    57 
       
    58 class Property(validation.Validated):
       
    59   """Representation for an individual property of an index.
       
    60 
       
    61   Attributes:
       
    62     name: Name of attribute to sort by.
       
    63     direction: Direction of sort.
       
    64   """
       
    65 
       
    66   ATTRIBUTES = {
       
    67       'name': validation.TYPE_STR,
       
    68       'direction': validation.Options(('asc', ('ascending',)),
       
    69                                       ('desc', ('descending',)),
       
    70                                       default='asc'),
       
    71       }
       
    72 
       
    73 
       
    74 class Index(validation.Validated):
       
    75   """Individual index definition.
       
    76 
       
    77   Order of the properties properties determins a given indixes sort priority.
       
    78 
       
    79   Attributes:
       
    80     kind: Datastore kind that index belongs to.
       
    81     ancestors: Include ancestors in index.
       
    82     properties: Properties to sort on.
       
    83   """
       
    84 
       
    85   ATTRIBUTES = {
       
    86       'kind': validation.TYPE_STR,
       
    87       'ancestor': validation.Type(bool, default=False),
       
    88       'properties': validation.Optional(validation.Repeated(Property)),
       
    89       }
       
    90 
       
    91 
       
    92 class IndexDefinitions(validation.Validated):
       
    93   """Top level for index definition file.
       
    94 
       
    95   Attributes:
       
    96     indexes: List of Index definitions.
       
    97   """
       
    98 
       
    99   ATTRIBUTES = {
       
   100       'indexes': validation.Optional(validation.Repeated(Index)),
       
   101       }
       
   102 
       
   103 
       
   104 def ParseIndexDefinitions(document):
       
   105   """Parse an individual index definitions document from string or stream.
       
   106 
       
   107   Args:
       
   108     document: Yaml document as a string or file-like stream.
       
   109 
       
   110   Raises:
       
   111     EmptyConfigurationFile when the configuration file is empty.
       
   112     MultipleConfigurationFile when the configuration file contains more than
       
   113     one document.
       
   114 
       
   115   Returns:
       
   116     Single parsed yaml file if one is defined, else None.
       
   117   """
       
   118   try:
       
   119     return yaml_object.BuildSingleObject(IndexDefinitions, document)
       
   120   except yaml_errors.EmptyConfigurationFile:
       
   121     return None
       
   122 
       
   123 
       
   124 def ParseMultipleIndexDefinitions(document):
       
   125   """Parse multiple index definitions documents from a string or stream.
       
   126 
       
   127   Args:
       
   128     document: Yaml document as a string or file-like stream.
       
   129 
       
   130   Returns:
       
   131     A list of datstore_index.IndexDefinitions objects, one for each document.
       
   132   """
       
   133   return yaml_object.BuildObjects(IndexDefinitions, document)
       
   134 
       
   135 
       
   136 def IndexDefinitionsToKeys(indexes):
       
   137   """Convert IndexDefinitions to set of keys.
       
   138 
       
   139   Args:
       
   140     indexes: A datastore_index.IndexDefinitions instance, or None.
       
   141 
       
   142   Returns:
       
   143     A set of keys constructed from the argument, each key being a
       
   144     tuple of the form (kind, ancestor, properties) where properties is
       
   145     a tuple of (name, direction) pairs, direction being ASCENDING or
       
   146     DESCENDING (the enums).
       
   147   """
       
   148   keyset = set()
       
   149   if indexes is not None:
       
   150     if indexes.indexes:
       
   151       for index in indexes.indexes:
       
   152         keyset.add(IndexToKey(index))
       
   153   return keyset
       
   154 
       
   155 
       
   156 def IndexToKey(index):
       
   157   """Convert Index to key.
       
   158 
       
   159   Args:
       
   160     index: A datastore_index.Index instance (not None!).
       
   161 
       
   162   Returns:
       
   163     A tuple of the form (kind, ancestor, properties) where properties
       
   164     is a tuple of (name, direction) pairs, direction being ASCENDING
       
   165     or DESCENDING (the enums).
       
   166   """
       
   167   props = []
       
   168   if index.properties is not None:
       
   169     for prop in index.properties:
       
   170       if prop.direction == 'asc':
       
   171         direction = ASCENDING
       
   172       else:
       
   173         direction = DESCENDING
       
   174       props.append((prop.name, direction))
       
   175   return index.kind, index.ancestor, tuple(props)
       
   176 
       
   177 
       
   178 
       
   179 
       
   180 ASCENDING = datastore_pb.Query_Order.ASCENDING
       
   181 DESCENDING = datastore_pb.Query_Order.DESCENDING
       
   182 
       
   183 EQUALITY_OPERATORS = set((datastore_pb.Query_Filter.EQUAL,
       
   184                           ))
       
   185 INEQUALITY_OPERATORS = set((datastore_pb.Query_Filter.LESS_THAN,
       
   186                             datastore_pb.Query_Filter.LESS_THAN_OR_EQUAL,
       
   187                             datastore_pb.Query_Filter.GREATER_THAN,
       
   188                             datastore_pb.Query_Filter.GREATER_THAN_OR_EQUAL,
       
   189                             ))
       
   190 EXISTS_OPERATORS = set((datastore_pb.Query_Filter.EXISTS,
       
   191                         ))
       
   192 
       
   193 
       
   194 def CompositeIndexForQuery(query):
       
   195   """Return the composite index needed for a query.
       
   196 
       
   197   A query is translated into a tuple, as follows:
       
   198 
       
   199   - The first item is the kind string, or None if we're not filtering
       
   200     on kind (see below).
       
   201 
       
   202   - The second item is a bool giving whether the query specifies an
       
   203     ancestor.
       
   204 
       
   205   - After that come (property, ASCENDING) pairs for those Filter
       
   206     entries whose operator is EQUAL or IN.  Since the order of these
       
   207     doesn't matter, they are sorted by property name to normalize them
       
   208     in order to avoid duplicates.
       
   209 
       
   210   - After that comes at most one (property, ASCENDING) pair for a
       
   211     Filter entry whose operator is on of the four inequalities.  There
       
   212     can be at most one of these.
       
   213 
       
   214   - After that come all the (property, direction) pairs for the Order
       
   215     entries, in the order given in the query.  Exceptions: (a) if
       
   216     there is a Filter entry with an inequality operator that matches
       
   217     the first Order entry, the first order pair is omitted (or,
       
   218     equivalently, in this case the inequality pair is omitted); (b) if
       
   219     an Order entry corresponds to an equality filter, it is ignored
       
   220     (since there will only ever be one value returned).
       
   221 
       
   222   - Finally, if there are Filter entries whose operator is EXISTS, and
       
   223     whose property names are not already listed, they are added, with
       
   224     the direction set to ASCENDING.
       
   225 
       
   226   This algorithm should consume all Filter and Order entries.
       
   227 
       
   228   Additional notes:
       
   229 
       
   230   - The low-level implementation allows queries that don't specify a
       
   231     kind; but the Python API doesn't support this yet.
       
   232 
       
   233   - If there's an inequality filter and one or more sort orders, the
       
   234     first sort order *must* match the inequality filter.
       
   235 
       
   236   - The following indexes are always built in and should be suppressed:
       
   237     - query on kind only;
       
   238     - query on kind and one filter *or* one order;
       
   239     - query on ancestor only, without kind (not exposed in Python yet);
       
   240     - query on kind and equality filters only, no order (with or without
       
   241       ancestor).
       
   242 
       
   243   - While the protocol buffer allows a Filter to contain multiple
       
   244     properties, we don't use this.  It is only needed for the IN operator
       
   245     but this is (currently) handled on the client side, so in practice
       
   246     each Filter is expected to have exactly one property.
       
   247 
       
   248   Args:
       
   249     query: A datastore_pb.Query instance.
       
   250 
       
   251   Returns:
       
   252     None if no composite index is needed for this query.  Otherwise,
       
   253     a tuple of the form (kind, ancestor, (prop1, prop2, ...), neq) where:
       
   254       kind: the kind or None;
       
   255       ancestor: True if this is an ancestor query;
       
   256       prop1, prop2, ...: tuples of the form (name, direction) where:
       
   257         name: a property name;
       
   258         direction: datastore_pb.Query_Order.ASCENDING or ...DESCENDING;
       
   259       neq: the number of prop tuples corresponding to equality filters.
       
   260   """
       
   261   kind = query.kind()
       
   262   ancestor = query.has_ancestor()
       
   263   filters = query.filter_list()
       
   264   orders = query.order_list()
       
   265 
       
   266   for filter in filters:
       
   267     assert filter.op() != datastore_pb.Query_Filter.IN, 'Filter.op()==IN'
       
   268     nprops = len(filter.property_list())
       
   269     assert nprops == 1, 'Filter has %s properties, expected 1' % nprops
       
   270 
       
   271   if ancestor and not kind and not filters and not orders:
       
   272     return None
       
   273 
       
   274   eq_filters = [f for f in filters if f.op() in EQUALITY_OPERATORS]
       
   275   ineq_filters = [f for f in filters if f.op() in INEQUALITY_OPERATORS]
       
   276   exists_filters = [f for f in filters if f.op() in EXISTS_OPERATORS]
       
   277   assert (len(eq_filters) + len(ineq_filters) +
       
   278           len(exists_filters)) == len(filters), 'Not all filters used'
       
   279 
       
   280   if (kind and eq_filters and not ineq_filters and not exists_filters and
       
   281       not orders):
       
   282     return None
       
   283 
       
   284   ineq_property = None
       
   285   if ineq_filters:
       
   286     ineq_property = ineq_filters[0].property(0).name()
       
   287     for filter in ineq_filters:
       
   288       assert filter.property(0).name() == ineq_property
       
   289 
       
   290   new_orders = []
       
   291   for order in orders:
       
   292     name = order.property()
       
   293     for filter in eq_filters:
       
   294       if filter.property(0).name() == name:
       
   295         break
       
   296     else:
       
   297       new_orders.append(order)
       
   298   orders = new_orders
       
   299 
       
   300   props = []
       
   301 
       
   302   for f in eq_filters:
       
   303     prop = f.property(0)
       
   304     props.append((prop.name(), ASCENDING))
       
   305 
       
   306   props.sort()
       
   307 
       
   308   if ineq_property:
       
   309     if orders:
       
   310       assert ineq_property == orders[0].property()
       
   311     else:
       
   312       props.append((ineq_property, ASCENDING))
       
   313 
       
   314   for order in orders:
       
   315     props.append((order.property(), order.direction()))
       
   316 
       
   317   for filter in exists_filters:
       
   318     prop = filter.property(0)
       
   319     prop_name = prop.name()
       
   320     for name, direction in props:
       
   321       if name == prop_name:
       
   322         break
       
   323     else:
       
   324       props.append((prop_name, ASCENDING))
       
   325 
       
   326   if (kind and not ancestor and
       
   327       (not props or (len(props) == 1 and props[0][1] == ASCENDING))):
       
   328     return None
       
   329 
       
   330   unique_names = set(name for name, dir in props)
       
   331   if len(props) > 1 and len(unique_names) == 1:
       
   332     return None
       
   333 
       
   334   return (kind, ancestor, tuple(props), len(eq_filters))
       
   335 
       
   336 
       
   337 def IndexYamlForQuery(kind, ancestor, props):
       
   338   """Return the composite index definition YAML needed for a query.
       
   339 
       
   340   The arguments are the same as the tuples returned by CompositeIndexForQuery,
       
   341   without the last neq element.
       
   342 
       
   343   Args:
       
   344     kind: the kind or None
       
   345     ancestor: True if this is an ancestor query, False otherwise
       
   346     prop1, prop2, ...: tuples of the form (name, direction) where:
       
   347         name: a property name;
       
   348         direction: datastore_pb.Query_Order.ASCENDING or ...DESCENDING;
       
   349 
       
   350   Returns:
       
   351     A string with the YAML for the composite index needed by the query.
       
   352   """
       
   353   yaml = []
       
   354   yaml.append('- kind: %s' % kind)
       
   355   if ancestor:
       
   356     yaml.append('  ancestor: yes')
       
   357   if props:
       
   358     yaml.append('  properties:')
       
   359     for name, direction in props:
       
   360       yaml.append('  - name: %s' % name)
       
   361       if direction == DESCENDING:
       
   362         yaml.append('    direction: desc')
       
   363   return '\n'.join(yaml)