Skip to content

Commit eb52af0

Browse files
committed
First attempt at supporting expressions in hstore
1 parent 69fc365 commit eb52af0

File tree

4 files changed

+119
-0
lines changed

4 files changed

+119
-0
lines changed

psqlextra/compiler.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from django.core.exceptions import SuspiciousOperation
22
from django.db.models.sql.compiler import SQLInsertCompiler, SQLUpdateCompiler
33

4+
from psqlextra.expressions import HStoreValue
5+
46

57
class PostgresReturningUpdateCompiler(SQLUpdateCompiler):
68
"""Compiler for SQL UPDATE statements that return
@@ -16,6 +18,31 @@ def execute_sql(self, _result_type):
1618

1719
return primary_keys
1820

21+
def as_sql(self):
22+
self._prepare_query_values()
23+
return super().as_sql()
24+
25+
def _prepare_query_values(self):
26+
"""Extra prep on query values by converting
27+
dictionaries into :see:HStoreValue expressions.
28+
29+
This allows putting expressions in a dictionary.
30+
The :see:HStoreValue will take care of resolving
31+
the expressions inside the dictionary."""
32+
33+
new_query_values = []
34+
for field, model, val in self.query.values:
35+
if isinstance(val, dict):
36+
val = HStoreValue(val)
37+
38+
new_query_values.append((
39+
field,
40+
model,
41+
val
42+
))
43+
44+
self.query.values = new_query_values
45+
1946
def _form_returning(self):
2047
"""Builds the RETURNING part of the query."""
2148

psqlextra/expressions.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,62 @@
11
from django.db.models import expressions, CharField
22

33

4+
class HStoreValue(expressions.Expression):
5+
"""Represents a HStore value.
6+
7+
The base PostgreSQL implementation Django provides,
8+
always represents HStore values as dictionaries,
9+
but this doesn't work if you want to use expressions
10+
inside hstore values."""
11+
12+
def __init__(self, value):
13+
"""Initializes a new instance."""
14+
15+
self.value = value
16+
17+
def resolve_expression(self, *args, **kwargs):
18+
"""Resolves expressions inside the dictionary."""
19+
20+
result = dict()
21+
for key, value in self.value.items():
22+
if hasattr(value, 'resolve_expression'):
23+
result[key] = value.resolve_expression(
24+
*args, **kwargs)
25+
else:
26+
result[key] = value
27+
28+
return HStoreValue(result)
29+
30+
def as_sql(self, compiler, connection):
31+
"""Compiles the HStore value into SQL.
32+
33+
Compiles expressions contained in the values
34+
of HStore entries as well.
35+
36+
Given a dictionary like:
37+
38+
dict(key1='val1', key2='val2')
39+
40+
The resulting SQL will be:
41+
42+
hstore(ARRAY['key1', 'val1'], ARRAY['key2', 'val2'])
43+
"""
44+
45+
result = []
46+
for key, value in self.value.items():
47+
if hasattr(value, 'as_sql'):
48+
sql, params = value.as_sql(compiler, connection)
49+
result.append('ARRAY[\'%s\', %s]' % (
50+
key, sql % params))
51+
elif value is not None:
52+
result.append('ARRAY[\'%s\', \'%s\']' % ((
53+
key, value)))
54+
else:
55+
result.append('ARRAY[\'%s\', NULL]' % key)
56+
57+
return 'hstore(%s)' % ','.join(result), []
58+
59+
460
class HStoreColumn(expressions.Col):
561
"""HStoreColumn expression.
662

psqlextra/fields/hstore_field.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from django.contrib.postgres.fields import HStoreField as DjangoHStoreField
44

5+
from psqlextra.expressions import HStoreValue
6+
57

68
class HStoreField(DjangoHStoreField):
79
"""Improved version of Django's :see:HStoreField that
@@ -22,6 +24,18 @@ def __init__(self, *args,
2224
self.uniqueness = uniqueness
2325
self.required = required
2426

27+
def get_db_prep_value(self, value, connection, prepared=False):
28+
"""Override the base class so it doesn't cast all values
29+
to strings.
30+
31+
psqlextra supports expressions in hstore fields, so casting
32+
all values to strings is a bad idea."""
33+
34+
if not value:
35+
return None
36+
37+
return value
38+
2539
def deconstruct(self):
2640
"""Gets the values to pass to :see:__init__ when
2741
re-creating this object."""

tests/test_query.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,25 @@ def test_annotate_hstore_key_ref():
3131
)
3232

3333
assert queryset['english_title'] == 'english'
34+
35+
36+
def test_hstore_f_ref():
37+
"""Tests whether F(..) expressions can be used in
38+
hstore values when performing update queries."""
39+
40+
model = get_fake_model({
41+
'name': models.CharField(max_length=255),
42+
'name_new': HStoreField()
43+
})
44+
45+
model.objects.create(
46+
name='waqas',
47+
name_new=dict(en='swen')
48+
)
49+
50+
model.objects.update(
51+
name_new=dict(en=models.F('name'))
52+
)
53+
54+
inst = model.objects.all().first()
55+
assert inst.name_new.get('en') == 'waqas'

0 commit comments

Comments
 (0)