Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Syntactic sugar classes for ancestor queries #38

Open
wants to merge 28 commits into
base: feature/ancestor-query-1.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fbcecf5
git ignore
aburgel Nov 26, 2011
f265731
appengine key and ancestor support
aburgel Nov 26, 2011
decfc12
fixes for non-primary GAEKeys
aburgel Nov 26, 2011
87a9f85
use id_or_name as string value
aburgel Nov 29, 2011
fd234e6
better value conversion to match str from GAEKey
aburgel Nov 29, 2011
150c915
test cases for different kinds of pk representations
aburgel Nov 29, 2011
29a1c62
fix cmp for GAEKey
aburgel Dec 6, 2011
1a1456a
merge upstream/develop
aburgel Dec 27, 2011
091b12c
use methods instead of properties for GAEKey fields
aburgel Dec 29, 2011
b5f126a
Merge branch 'develop' of git://github.com/django-nonrel/djangoappeng…
aburgel Dec 29, 2011
902c363
remove missed @property, update unit tests
aburgel Dec 29, 2011
6aa66b7
use real_key for serialization
aburgel Jan 25, 2012
216aa48
modify SqlInsertCompiler to handle bulk inserts
aburgel Mar 17, 2012
7df916e
remove usage of XMLField
aburgel Mar 17, 2012
8208067
repr method for GAEKey
aburgel Mar 17, 2012
2fc14a5
do not encode cursor if it doesnt exist
aburgel Mar 17, 2012
b23cc69
do not allow null primary keys, check for empty strings when creating…
aburgel Mar 18, 2012
cc73be7
merge develop branch
aburgel Mar 23, 2012
60e62a7
merge develop branch
aburgel Mar 23, 2012
6a10a9d
rewrite ancestor queries for type-conversion-refactor
aburgel Mar 23, 2012
d38742e
django 1.4 updates
aburgel Mar 23, 2012
7407854
DBKeyField is a special case for conversion
aburgel Apr 20, 2012
b77c183
add missing import
aburgel Apr 20, 2012
9be9b5f
dbkeyfield should not be forced to nullable
aburgel Apr 20, 2012
4c6ebdf
added make_key function to simplify created DbKeys from models
aburgel Apr 20, 2012
32d2e17
Merge commit '257e3390ab4768abbb6d82af6c16438cb9ede3cb' into django-1.4
May 22, 2012
13c8b64
Merge branch 'feature/ancestor-query-1.4' of git://github.com/django-…
May 23, 2012
a417ac0
Add some syntactic sugar classes for doing ancestor queries.
May 25, 2012
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def setup_env():
else:
if name == 'webapp2':
extra_paths.append(root)
elif name == 'webob_0_9':
extra_paths.append(root)
sys.path = extra_paths + sys.path
from google.appengine.api import apiproxy_stub_map

Expand Down
3 changes: 2 additions & 1 deletion db/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ def _value_for_db(self, value, field, field_kind, db_type, lookup):
if db_type == 'key':
# value = self._value_for_db_key(value, field_kind)
try:
value = key_from_path(field.model._meta.db_table, value)
if not isinstance(value, Key):
value = key_from_path(field.model._meta.db_table, value)
except (BadArgumentError, BadValueError,):
raise DatabaseError("Only strings and positive integers "
"may be used as keys on GAE.")
Expand Down
70 changes: 43 additions & 27 deletions db/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from .db_settings import get_model_indexes
from .expressions import ExpressionEvaluator
from .utils import commit_locked
from .utils import AncestorKey, commit_locked


# Valid query types (a dictionary is used for speedy lookups).
Expand Down Expand Up @@ -86,6 +86,7 @@ def __init__(self, compiler, fields):
self.included_pks = None
self.excluded_pks = ()
self.has_negated_exact_filter = False
self.ancestor_key = None
self.ordering = []
self.db_table = self.query.get_meta().db_table
self.pks_only = (len(fields) == 1 and fields[0].primary_key)
Expand Down Expand Up @@ -190,6 +191,14 @@ def add_filter(self, field, lookup_type, negated, value):
# Optimization: batch-get by key; this is only suitable for
# primary keys, not for anything that uses the key type.
if field.primary_key and lookup_type in ('exact', 'in'):
if lookup_type == 'exact' and isinstance(value, AncestorKey):
if negated:
raise DatabaseError("You can't negate an ancestor operator.")
if self.ancestor_key is not None:
raise DatabaseError("You can't use more than one ancestor operator.")
self.ancestor_key = value.key
return

if self.included_pks is not None:
raise DatabaseError("You can't apply multiple AND "
"filters on the primary key. "
Expand Down Expand Up @@ -311,6 +320,8 @@ def _make_entity(self, entity):
def _build_query(self):
for query in self.gae_query:
query.Order(*self.ordering)
if self.ancestor_key:
query.Ancestor(self.ancestor_key)
if len(self.gae_query) > 1:
return MultiQuery(self.gae_query, self.ordering)
return self.gae_query[0]
Expand Down Expand Up @@ -356,36 +367,41 @@ class SQLCompiler(NonrelCompiler):
class SQLInsertCompiler(NonrelInsertCompiler, SQLCompiler):

@safe_call
def insert(self, data, return_id=False):
def insert(self, data_list, return_id=False):
opts = self.query.get_meta()
unindexed_fields = get_model_indexes(self.query.model)['unindexed']
kwds = {'unindexed_properties': []}
properties = {}
for field, value in data.iteritems():

# The value will already be a db.Key, but the Entity
# constructor takes a name or id of the key, and will
# automatically create a new key if neither is given.
if field.primary_key:
if value is not None:
kwds['id'] = value.id()
kwds['name'] = value.name()

# GAE does not store empty lists (and even does not allow
# passing empty lists to Entity.update) so skip them.
elif isinstance(value, (tuple, list)) and not len(value):
continue

# Use column names as property names.
else:
properties[field.column] = value
unindexed_cols = [opts.get_field(name).column
for name in unindexed_fields]

entity_list = []
for data in data_list:
properties = {}
kwds = {'unindexed_properties': unindexed_cols}
for column, value in data.items():
# The value will already be a db.Key, but the Entity
# constructor takes a name or id of the key, and will
# automatically create a new key if neither is given.
if column == opts.pk.column:
if value is not None:
kwds['id'] = value.id()
kwds['name'] = value.name()
kwds['parent'] = value.parent()

# GAE does not store empty lists (and even does not allow
# passing empty lists to Entity.update) so skip them.
elif isinstance(value, (tuple, list)) and not len(value):
continue

# Use column names as property names.
else:
properties[column] = value

if field in unindexed_fields:
kwds['unindexed_properties'].append(field.column)
entity = Entity(opts.db_table, **kwds)
entity.update(properties)
entity_list.append(entity)

entity = Entity(opts.db_table, **kwds)
entity.update(properties)
return Put(entity)
keys = Put(entity_list)
return keys[0] if isinstance(keys, list) else keys


class SQLUpdateCompiler(NonrelUpdateCompiler, SQLCompiler):
Expand Down
16 changes: 12 additions & 4 deletions db/creation.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ def db_type(self, field):
field is to be indexed, and the "text" db_type (db.Text) if
it's registered as unindexed.
"""
from djangoappengine.fields import DbKeyField

# DBKeyField reads/stores db.Key objects directly
# so its treated as a special case
if isinstance(field, DbKeyField):
return field.db_type(connection=self.connection)

if self.connection.settings_dict.get('STORE_RELATIONS_AS_DB_KEYS'):
if field.primary_key or field.rel is not None:
return 'key'
Expand Down Expand Up @@ -70,7 +77,8 @@ def _create_test_db(self, *args, **kw):
stub_manager.activate_test_stubs()

def _destroy_test_db(self, *args, **kw):
if self._had_test_stubs:
stub_manager.deactivate_test_stubs()
stub_manager.setup_stubs(self.connection)
del self._had_test_stubs
if hasattr(self, '_had_test_stubs'):
if self._had_test_stubs:
stub_manager.deactivate_test_stubs()
stub_manager.setup_stubs(self.connection)
del self._had_test_stubs
Empty file added db/models/__init__.py
Empty file.
14 changes: 14 additions & 0 deletions db/models/manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django.db.models import Manager as _baseManager
from djangoappengine.db.utils import as_ancestor
from djangoappengine.db.models.query import QuerySet

class Manager(_baseManager):

def get_query_set(self):
"""Returns a new QuerySet object. Subclasses can override this method
to easily customize the behavior of the Manager.
"""
return QuerySet(self.model, using=self._db)

def ancestor(self, ancestor):
return self.get_query_set().ancestor(ancestor)
17 changes: 17 additions & 0 deletions db/models/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.db.models.query import QuerySet as _baseQuerySet
from djangoappengine.db.utils import as_ancestor

class QuerySet(_baseQuerySet):
def ancestor(self, ancestor):
"""
Returns a new QuerySet instance with the args ANDed to the existing
set.
"""
return self._filter_or_exclude(False, pk=as_ancestor(ancestor))

class EmptyQuerySet(QuerySet):
def ancestor(self, *args, **kwargs):
"""
Always returns EmptyQuerySet.
"""
return self
36 changes: 35 additions & 1 deletion db/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from google.appengine.api.datastore import Key
from google.appengine.datastore.datastore_query import Cursor

from django.db import models, DEFAULT_DB_ALIAS

try:
Expand All @@ -20,7 +22,7 @@ def get_cursor(queryset):
# Evaluate QuerySet.
len(queryset)
cursor = getattr(queryset.query, '_gae_cursor', None)
return Cursor.to_websafe_string(cursor)
return Cursor.to_websafe_string(cursor) if cursor else None


def set_cursor(queryset, start=None, end=None):
Expand Down Expand Up @@ -57,3 +59,35 @@ def _commit_locked(*args, **kw):
if callable(func_or_using):
return inner_commit_locked(func_or_using, DEFAULT_DB_ALIAS)
return lambda func: inner_commit_locked(func, func_or_using)

class AncestorKey(object):
def __init__(self, key):
self.key = key

def as_ancestor(key_or_model):
if key_or_model is None:
raise ValueError("key_or_model must not be None")

if isinstance(key_or_model, models.Model):
key_or_model = Key.from_path(key_or_model._meta.db_table, key_or_model.pk)

return AncestorKey(key_or_model)

def make_key(*args, **kwargs):
parent = kwargs.pop('parent', None)

if kwargs:
raise AssertionError('Excess keyword arguments; received %s' % kwargs)

if not args or len(args) % 2:
raise AssertionError('A non-zero even number of positional arguments is required; received %s' % args)

if isinstance(parent, models.Model):
parent = Key.from_path(parent._meta.db_table, parent.pk)

converted_args = []
for i in xrange(0, len(args), 2):
model, id_or_name = args[i:i+2]
converted_args.extend((model._meta.db_table, id_or_name))

return Key.from_path(*converted_args, parent=parent)
87 changes: 87 additions & 0 deletions fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.encoding import smart_unicode

from djangoappengine.db.utils import AncestorKey

from google.appengine.api.datastore import Key, datastore_errors

import logging

class DbKeyField(models.Field):
description = "A field for native database key objects"
__metaclass__ = models.SubfieldBase

def __init__(self, *args, **kwargs):
kwargs['blank'] = True

self.parent_key_attname = kwargs.pop('parent_key_name', None)

if self.parent_key_attname is not None and kwargs.get('primary_key', None) is None:
raise ValueError("Primary key must be true to use parent_key_name")

super(DbKeyField, self).__init__(*args, **kwargs)

def contribute_to_class(self, cls, name):
if self.primary_key:
assert not cls._meta.has_auto_field, "A model can't have more than one auto field."
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 set_parent_key(instance, value):
if instance is None:
raise AttributeError("Attribute must be accessed via instance")

if not isinstance(value, Key):
raise ValueError("'%s' must be a Key" % self.parent_key_attname)

instance.__dict__[self.parent_key_attname] = value

setattr(cls, self.parent_key_attname, property(get_parent_key, set_parent_key))

super(DbKeyField, self).contribute_to_class(cls, name)

def to_python(self, value):
if value is None:
return None
if isinstance(value, Key):
return value
if isinstance(value, basestring):
if len(value) == 0:
return None

try:
return Key(encoded=value)
except datastore_errors.BadKeyError:
return Key.from_path(self.model._meta.db_table, long(value))
if isinstance(value, (int, long)):
return Key.from_path(self.model._meta.db_table, value)

raise ValidationError("DbKeyField does not accept %s" % type(value))

def get_prep_value(self, value):
if isinstance(value, AncestorKey):
return value
return self.to_python(value)

def pre_save(self, model_instance, add):
value = super(DbKeyField, self).pre_save(model_instance, add)

if add and value is None and self.parent_key_attname is not None and hasattr(model_instance, self.parent_key_attname):
stashed_parent = getattr(model_instance, self.parent_key_attname)
value = Key.from_path(self.model._meta.db_table, 0, parent=stashed_parent)

return value

def formfield(self, **kwargs):
return None

def value_to_string(self, obj):
return smart_unicode(self._get_val_from_obj(obj))
32 changes: 32 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from django.test import TestCase

from google.appengine.datastore import datastore_stub_util

from db.stubs import stub_manager

class GAETestCase(TestCase):
def _pre_setup(self):
"""Performs any pre-test setup.
* Set the dev_appserver consistency state.
"""
super(GAETestCase,self)._pre_setup()

if hasattr(self, 'consistency_probability'):
datastore = stub_manager.testbed.get_stub('datastore_v3')
self._orig_policy = datastore._consistency_policy

datastore.SetConsistencyPolicy(datastore_stub_util.PseudoRandomHRConsistencyPolicy(probability=self.consistency_probability))


def _post_teardown(self):
""" Performs any post-test things. This includes:

* Putting back the original ROOT_URLCONF if it was changed.
* Force closing the connection, so that the next test gets
a clean cursor.
"""
if hasattr(self, '_orig_policy'):
datastore = stub_manager.testbed.get_stub('datastore_v3')
datastore.SetConsistencyPolicy(self._orig_policy)

super(GAETestCase,self)._post_teardown()
4 changes: 3 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from .backend import BackendTest
#from .decimals import DecimalTest
from .field_db_conversion import FieldDBConversionTest
from .field_options import FieldOptionsTest
from .filter import FilterTest
from .keys import KeysTest
from .keys import KeysTest, DbKeyFieldTest, AncestorQueryTest, ParentKeyTest
from .not_return_sets import NonReturnSetsTest
from .order import OrderTest
from .transactions import TransactionTest
from .ancestor import AncestorTest
Loading