Commits

Alex Burgel committed c6ebfdc

new implementation of ancestor query and GAEKeys

  • Participants
  • Parent commits 2dd50cb

Comments (0)

Files changed (3)

File db/compiler.py

 from .utils import commit_locked
 from .expressions import ExpressionEvaluator
 from ..fields import GAEKeyField
+from ..models import GAEKey, GAEAncestorKey
 
 import datetime
 import sys
 import cPickle as pickle
 
 import decimal
+import logging
 
 # Valid query types (a dictionary is used for speedy lookups).
 OPERATORS_MAP = {
     # 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
         if column == self.query.get_meta().pk.column:
             column = '__key__'
             db_table = self.query.get_meta().db_table
+            
+            if lookup_type == 'exact' and isinstance(value, GAEAncestorKey):
+                if negated:
+                    raise DatabaseError("You can't negate an ancestor operation.")
+                if self.ancestor_key is not None:
+                    raise DatabaseError("You can't use more than one ancestor operation.")
+                self.ancestor_key = value.key()
+                return
             if lookup_type in ('exact', 'in'):
                 # Optimization: batch-get by key
                 if self.pk_filters is not None:
                 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
     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]
             value = value.decode('utf-8')
         elif isinstance(value, Key):
             if db_type == 'gae_key':
-                # gae_keys pass through as-is
-                pass
+                value = GAEKey(real_key=value)
             else:
                 assert value.parent() is None, "Use GAEKeyField to enable parent keys!"
                 if db_type == 'integer':
         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, Key):
-                    if value.parent():
-                        kwds['parent'] = value.parent()
-                    if value.name():
-                        kwds['name'] = value.name()
-                    else:
-                        kwds['id'] = value.id()
+                if isinstance(value, GAEKey):
+                    if value.parent_key:
+                        kwds['parent'] = value.parent_key.real_key
+                    if isinstance(value.id_or_name, basestring):
+                        kwds['name'] = value.id_or_name
+                    elif value.id_or_name is not None:
+                        kwds['id'] = value.id_or_name
                 elif isinstance(value, basestring):
                     kwds['name'] = value
                 else:
             value.second, value.microsecond)
 
 def create_key(db_table, value):
-    if isinstance(value, Key):
-        return value
+    if isinstance(value, GAEKey):
+        parent = None
+        if value.parent_key is not None:
+            parent = value.parent.real_key
+        return Key.from_path(db_table, value.id_or_name, parent=parent)
     if isinstance(value, (int, long)) and value < 1:
         return None
     return Key.from_path(db_table, value)
 from django.db import models
 from google.appengine.api.datastore import Key
+from .models import GAEKey, GAEAncestorKey
 
-class GAEKeyField(models.AutoField):
+class GAEKeyField(models.Field):
     description = "A field for Google AppEngine Key objects"
     __metaclass__ = models.SubfieldBase
 
     def __init__(self, *args, **kwargs):
+        assert kwargs.get('primary_key', False) is True, "%ss must have primary_key=True." % self.__class__.__name__
         kwargs['null'] = True
+        kwargs['blank'] = True
+        self.parent_key_attname = kwargs.pop('parent_key_name', None)
+
         super(GAEKeyField, self).__init__(*args, **kwargs)
 
-    def get_internal_type(self):
-        return self.__class__.__name__
+    def contribute_to_class(self, cls, name):
+        assert not cls._meta.has_auto_field, "A model can't have more than one auto field."
+        super(GAEKeyField, self).contribute_to_class(cls, name)
+        cls._meta.has_auto_field = True
+        cls._meta.auto_field = self
+        
+        if self.parent_key_attname is not None:
+            def get_parent_key(instance, instance_type=None):
+                if instance is None:
+                    return self
+                return instance.__dict__.get(self.parent_key_attname)
 
-    def get_related_internal_type(self):
-        return self.get_internal_type()
+            def set_parent_key(instance, value):
+                if instance is None:
+                    raise AttributeError("Attribute must be accessed via instance")
+
+                if not isinstance(value, GAEKey):
+                    raise ValueError("parent must be a GAEKey")
+
+                instance.__dict__[self.parent_key_attname] = value
+
+            setattr(cls, self.parent_key_attname, property(get_parent_key, set_parent_key))
 
     def to_python(self, value):
+        if isinstance(value, GAEKey):
+            return value
         if isinstance(value, Key):
-            return value
+            return GAEKey(real_key=value)
         if value is None or len(value) < 1:
-            return
-        return Key(value)
+            return None
+        return GAEKey(id_or_name=value)
 
     def get_prep_value(self, value):
-        if isinstance(value, Key):
-            return value
+        if value is None:
+            return None
+        if not isinstance(value, (GAEKey, GAEAncestorKey)):
+            raise ValueError('must by type GAEKey or GAEAncestorKey, not <%s>' % type(value))
+        return value
 
-        if isinstance(value, (str, basestring)):
-            return Key(value)
-
+    def formfield(self, **kwargs):
         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
+    def pre_save(self, model_instance, add):
+        if add and self.parent_key_attname is not None:
+            parent_key = getattr(model_instance, self.parent_key_attname)
+            if parent_key is not None:
+                key = GAEKey(parent_key=parent_key)
+                setattr(model_instance, self.attname, key)
+                return key
+
+        return super(GAEKeyField, self).pre_save(model_instance, add)
+from django.db import models
+from google.appengine.api.datastore import Key
+
+# TODO: look for better exceptions to raise
+
+class GAEAncestorKey(object):
+    def __init__(self, key):
+        if key is None:
+            raise ValueError('key must not be None')
+        if not isinstance(key, Key):
+            raise ValueError('key must be of type Key')
+
+        self._key = key
+
+    def key(self):
+        return self._key
+
+class GAEKey(object):
+    def __init__(self, id_or_name=None, parent_key=None, real_key=None):
+        self._id_or_name = id_or_name
+        self._parent_key = parent_key
+        self._real_key = None
+
+        if real_key is not None:
+            if id_or_name is not None or parent_key is not None:
+                raise ValueError("You can't set both a real_key and an id_or_name or parent_key")
+
+            self._real_key = real_key
+            if real_key.parent():
+                self._parent_key = GAEKey(real_key=real_key.parent())
+            self._id_or_name = real_key.id_or_name()
+
+    def _get_id_or_name(self):
+        return self._id_or_name
+    id_or_name = property(_get_id_or_name)
+
+    def _get_parent_key(self):
+        if self._parent_key is None:
+            raise ValueError("This is a root entity, there is no parent.")
+        return self._parent_key
+    parent_key = property(_get_parent_key)
+
+    def _get_real_key(self):
+        if self._real_key is None:
+            raise ValueError("Incomplete key, please save the entity first.")
+        return self._real_key
+    real_key = property(_get_real_key)
+
+    def as_ancestor(self):
+        return GAEAncestorKey(self._get_real_key())
+
+    def __cmp__(self, other):
+        if not isinstance(other, GAEKey):
+            return 1
+        if self._real_key is None or other._real_key is None:
+            raise ValueError("You can't compare unsaved keys.")
+
+        return cmp(self._real_key, other._real_key)
+
+    def __hash__(self):
+        if self._real_key is None:
+            raise ValueError("You can't hash an unsaved key.")
+
+        return hash(self._real_key)
+
+    def __str__(self):
+        return "<%s %s %s>" % (self._id_or_name, self._parent_key, self._real_key)