Skip to content

Commit

Permalink
Merge pull request #74 from azavea/isnull-values
Browse files Browse the repository at this point in the history
Add support for isnull on key/value pairs
  • Loading branch information
nemesifier committed Oct 5, 2014
2 parents 70bf83b + 436f867 commit 7bc1bc3
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 7 deletions.
1 change: 1 addition & 0 deletions django_hstore/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def south_field_triple(self):
HStoreField.register_lookup(HStoreLessThanOrEqual)
HStoreField.register_lookup(HStoreContains)
HStoreField.register_lookup(HStoreIContains)
HStoreField.register_lookup(HStoreIsNull)


class DictionaryField(HStoreField):
Expand Down
29 changes: 25 additions & 4 deletions django_hstore/lookups.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
LessThan,
LessThanOrEqual,
Contains,
IContains
IContains,
IsNull
)

from django_hstore.query import get_cast_for_param
from django_hstore.query import get_cast_for_param, get_value_annotations


__all__ = [
Expand All @@ -20,15 +21,16 @@
'HStoreLessThan',
'HStoreLessThanOrEqual',
'HStoreContains',
'HStoreIContains'
'HStoreIContains',
'HStoreIsNull'
]


class HStoreLookupMixin(object):
def __init__(self, lhs, rhs, *args, **kwargs):
# We need to record the types of the rhs parameters before they are converted to strings
if isinstance(rhs, dict):
self.value_annot = dict((key, type(subvalue)) for key, subvalue in six.iteritems(rhs))
self.value_annot = get_value_annotations(rhs)
super(HStoreLookupMixin, self).__init__(lhs, rhs)


Expand Down Expand Up @@ -118,3 +120,22 @@ def as_postgresql(self, qn, connection):

class HStoreIContains(IContains, HStoreContains):
pass


class HStoreIsNull(IsNull):

def as_postgresql(self, qn, connection):
lhs, lhs_params = self.process_lhs(qn, connection)

if isinstance(self.rhs, dict):
param = self.rhs
param_keys = list(param.keys())
conditions = []

for key in param_keys:
op = 'IS NULL' if param[key] else 'IS NOT NULL'
conditions.append('(%s->\'%s\') %s' % (lhs, key, op))

return (" AND ".join(conditions), lhs_params)

return super(HStoreIsNull, self).as_sql(qn, connection)
21 changes: 18 additions & 3 deletions django_hstore/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,18 @@ def get_cast_for_param(value_annot, key):
return '::float8'
elif issubclass(value_annot[key], Decimal):
return '::numeric'
elif issubclass(value_annot[key], bool):
elif value_annot[key] in (True, False):
return '::boolean'
else:
return ''


def get_value_annotations(param):
# We need to store the actual value for booleans, not just the type, for isnull
get_type = lambda v: v if isinstance(v, bool) else type(v)
return dict((key, get_type(subvalue)) for key, subvalue in six.iteritems(param))


class HStoreWhereNode(WhereNode):

def add(self, data, *args, **kwargs):
Expand All @@ -108,8 +114,8 @@ def add(self, data, *args, **kwargs):
if isinstance(original_value, dict):
len_children = len(self.children) if self.children else 0

value_annot = dict((key, type(subvalue))
for key, subvalue in six.iteritems(original_value))
value_annot = get_value_annotations(original_value)

# We should be able to get the normal child node here, but it is not returned in Django 1.5
super(HStoreWhereNode, self).add(data, *args, **kwargs)

Expand Down Expand Up @@ -202,6 +208,15 @@ def make_atom(self, child, qn, connection):
raise ValueError('invalid value')

elif lookup_type == 'isnull':
if isinstance(param, dict):
param_keys = list(param.keys())
conditions = []

for key in param_keys:
op = 'IS NULL' if value_annot[key] else 'IS NOT NULL'
conditions.append('(%s->\'%s\') %s' % (field, key, op))

return (" AND ".join(conditions), [])
# do not perform any special format
return super(HStoreWhereNode, self).make_atom(child, qn, connection)

Expand Down
7 changes: 7 additions & 0 deletions doc/doc.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,13 @@ Something.objects.filter(data__contains=['a', 'b'])

# subset by single key
Something.objects.filter(data__contains=['a'])

# filter by is null on individual key/value pairs
Something.objects.filter(data__isnull={'a': True})
Something.objects.filter(data__isnull={'a': True, 'b': False})

# filter by is null on the column works as normal
Something.objects.filter(data__isnull=True)
----
Expand Down
12 changes: 12 additions & 0 deletions tests/django_hstore_tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ def test_nullable_getitem(self):
with self.assertRaises(KeyError):
n.data['test']

def test_null_values(self):
null_v = DataBag.objects.create(name="test", data={"v": None})
nonnull_v = DataBag.objects.create(name="test", data={"v": "item"})

r = DataBag.objects.filter(data__isnull={"v": True})
self.assertEqual(len(r), 1)
self.assertEqual(r[0], null_v)

r = DataBag.objects.filter(data__isnull={"v": False})
self.assertEqual(len(r), 1)
self.assertEqual(r[0], nonnull_v)

def test_named_querying(self):
alpha, beta = self._create_bags()
self.assertEqual(DataBag.objects.get(name='alpha'), alpha)
Expand Down

0 comments on commit 7bc1bc3

Please sign in to comment.