Commits

Alex Burgel committed 144b506

support for appengine Keys as primary key and ancestor queries

Comments (0)

Files changed (4)

     NonrelDatabaseOperations, NonrelDatabaseWrapper, NonrelDatabaseClient, \
     NonrelDatabaseValidation, NonrelDatabaseIntrospection
 from google.appengine.ext.db.metadata import get_kinds, get_namespaces
-from google.appengine.api.datastore import Query, Delete
+from google.appengine.api.datastore import Query, Delete, Key
 from google.appengine.api.namespace_manager import set_namespace
 import logging
 import os
 
     DEFAULT_MAX_DIGITS = 16
 
+    def value_to_db_auto(self, value):
+        logging.info("value_to_db_auto: %s %s" % (value, type(value)))
+        if isinstance(value, Key):
+            return value
+        return super(DatabaseOperations, self).value_to_db_auto(value)
+
     def value_to_db_decimal(self, value, max_digits, decimal_places):
         if value is None:
             return None
 from .db_settings import get_model_indexes
 from .utils import commit_locked
 from .expressions import ExpressionEvaluator
+from ..fields import GAEKeyField
 
 import datetime
 import sys
 import cPickle as pickle
 
 import decimal
+import logging
 
 # Valid query types (a dictionary is used for speedy lookups).
 OPERATORS_MAP = {
         self.pk_filters = None
         self.excluded_pks = ()
         self.has_negated_exact_filter = False
+        self.ancestor_key = None
         self.ordering = ()
         self.gae_ordering = []
         pks_only = False
     # This function is used by the default add_filters() implementation
     @safe_call
     def add_filter(self, column, lookup_type, negated, db_type, value):
+        logging.info("add_filter: %s %s %s %s %s %s" % (column, lookup_type, negated, db_type, value, type(value)))
+
         if value in ([], ()):
             self.pk_filters = []
             return
                 else:
                     self.pk_filters = pks
                 return
+            elif lookup_type == 'startswith' and isinstance(value, Key):
+                if self.ancestor_key is not None:
+                    raise DatabaseError("You can't use more than one ancestor operator.")
+                self.ancestor_key = value
+                return
             else:
                 # XXX: set db_type to 'gae_key' in order to allow
                 # convert_value_for_db to recognize the value to be a Key and
     def _build_query(self):
         for query in self.gae_query:
             query.Order(*self.gae_ordering)
+            if self.ancestor_key:
+                query.Ancestor(self.ancestor_key)
         if len(self.gae_query) > 1:
             return MultiQuery(self.gae_query, self.gae_ordering)
         return self.gae_query[0]
     query_class = GAEQuery
 
     def convert_value_from_db(self, db_type, value):
+        logging.info("convert_value_from_db %s %s %s" % (db_type, value, type(value)))
+
         if isinstance(value, (list, tuple, set)) and \
                 db_type.startswith(('ListField:', 'SetField:')):
             db_sub_type = db_type.split(':', 1)[1]
             # contain non unicode strings, nevertheless work with unicode ones)
             value = value.decode('utf-8')
         elif isinstance(value, Key):
-            # for now we do not support KeyFields thus a Key has to be the own
-            # primary key
-            # TODO: GAE: support parents via GAEKeyField
-            assert value.parent() is None, "Parents are not yet supported!"
-            if db_type == 'integer':
-                if value.id() is None:
-                    raise DatabaseError('Wrong type for Key. Expected integer, found'
-                        'None')
+            if db_type == 'gae_key':
+                # gae_keys pass through as-is
+                pass
+            else:
+                assert value.parent() is None, "Use GAEKeyField to enable parent keys!"
+                if db_type == 'integer':
+                    if value.id() is None:
+                        raise DatabaseError('Wrong type for Key. Expected integer, found'
+                            'None')
+                    else:
+                        value = value.id()
+                elif db_type == 'text':
+                    if value.name() is None:
+                        raise DatabaseError('Wrong type for Key. Expected string, found'
+                            'None')
+                    else:
+                        value = value.name()
                 else:
-                    value = value.id()
-            elif db_type == 'text':
-                if value.name() is None:
-                    raise DatabaseError('Wrong type for Key. Expected string, found'
-                        'None')
-                else:
-                    value = value.name()
-            else:
-                raise DatabaseError("%s fields cannot be keys on GAE" % db_type)
+                    raise DatabaseError("%s fields cannot be keys on GAE" % db_type)
         elif db_type == 'date' and isinstance(value, datetime.datetime):
             value = value.date()
         elif db_type == 'time' and isinstance(value, datetime.datetime):
         return value
 
     def convert_value_for_db(self, db_type, value):
+        logging.info("convert_value_for_db %s %s %s" % (db_type, value, type(value)))
+
         if isinstance(value, unicode):
             value = unicode(value)
         elif isinstance(value, str):
         kwds = {'unindexed_properties': unindexed_cols}
         for column, value in data.items():
             if column == opts.pk.column:
-                if isinstance(value, basestring):
+                if isinstance(value, Key):
+                    if value.parent():
+                        kwds['parent'] = value.parent()
+                    if value.name():
+                        kwds['name'] = value.name()
+                    else:
+                        kwds['id'] = value.id()
+                elif isinstance(value, basestring):
                     kwds['name'] = value
                 else:
                     kwds['id'] = value
             elif isinstance(value, (tuple, list)) and not len(value):
-                # gae does not store emty lists (and even does not allow passing empty
+                # gae does not store empty lists (and even does not allow passing empty
                 # lists to Entity.update) so skip them
                 continue
             else:
                 gae_data[column] = value
 
-        entity = Entity(self.query.get_meta().db_table, **kwds)
+        entity = Entity(opts.db_table, **kwds)
         entity.update(gae_data)
         key = Put(entity)
-        return key.id_or_name()
+        
+        if not isinstance(opts.pk, GAEKeyField):
+            if key.id() is not None:
+                key = key.id()
+            else:
+                key = key.name()
+        
+        return key
 
 class SQLUpdateCompiler(NonrelUpdateCompiler, SQLCompiler):
     def execute_sql(self, result_type=MULTI):
             value.second, value.microsecond)
 
 def create_key(db_table, value):
+    if isinstance(value, Key):
+        return value
     if isinstance(value, (int, long)) and value < 1:
         return None
     return Key.from_path(db_table, value)
         return self.internal_type
 
 def get_data_types():
-    # TODO: Add GAEKeyField and a corresponding db_type
     string_types = ('text', 'longtext')
     data_types = NonrelDatabaseCreation.data_types.copy()
     for name, field_type in data_types.items():
         if field_type in string_types:
             data_types[name] = StringType(field_type)
+    
+    data_types['GAEKeyField'] = 'gae_key'
+    
     return data_types
 
 class DatabaseCreation(NonrelDatabaseCreation):
+from django.db import models
+from google.appengine.api.datastore import Key
+import logging
+
+class GAEKeyField(models.AutoField):
+    description = "A field for Google AppEngine Key objects"
+    __metaclass__ = models.SubfieldBase
+
+    def __init__(self, *args, **kwargs):
+        kwargs['null'] = True
+        super(GAEKeyField, self).__init__(*args, **kwargs)
+
+    def get_internal_type(self):
+        return self.__class__.__name__
+
+    def get_related_internal_type(self):
+        return self.get_internal_type()
+
+    def to_python(self, value):
+        logging.info("to python value: %s %s" % (value, type(value)))
+        if isinstance(value, Key):
+            return value
+        if value is None or len(value) < 1:
+            return
+        return Key(value)
+
+    def get_prep_value(self, value):
+        logging.info("prep value: %s %s" % (value, type(value)))
+        
+        if isinstance(value, Key):
+            return value
+
+        if isinstance(value, (str, basestring)):
+            return Key(value)
+
+        return None
+
+    def get_db_prep_lookup(self, lookup_type, value, connection, prepared=False):
+        if not prepared:
+            value = self.get_prep_lookup(lookup_type, value)
+        
+        return value
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.