Skip to content

Commit 5373be2

Browse files
committed
JSON field increment with F expressions
1 parent 029a692 commit 5373be2

File tree

7 files changed

+97
-1
lines changed

7 files changed

+97
-1
lines changed

requirements/local.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ boto3-stubs==1.26.81
1919

2020
flake8==6.0.0
2121
isort==5.12.0
22-
black==23.1.0
22+
black==23.3.0
2323
pre-commit==3.2.2

styleguide_example/blog_examples/f_expressions/__init__.py

Whitespace-only changes.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from django.db.models import CharField, F, IntegerField, JSONField, Value
2+
from django.db.models.expressions import Func
3+
from django.db.models.functions import Cast
4+
5+
"""
6+
An F() object represents the value of a model field, transformed value of a model field, or annotated column.
7+
It makes it possible to refer to model field values and perform database operations using them without actually
8+
having to pull them out of the database into Python memory.
9+
"""
10+
11+
12+
class JSONIncrement(Func):
13+
function = "jsonb_set"
14+
15+
def __init__(self, full_path, value=1, **extra):
16+
field_name, *key_path_parts = full_path.split("__")
17+
18+
if not field_name:
19+
raise ValueError("`full_path` can not be blank.")
20+
21+
if len(key_path_parts) < 1:
22+
raise ValueError("`full_path` must contain at least one key.")
23+
24+
key_path = ",".join(key_path_parts)
25+
26+
new_value_expr = Cast(Cast(F(full_path), IntegerField()) + value, CharField())
27+
28+
expressions = [F(field_name), Value(f"{{{key_path}}}"), Cast(new_value_expr, JSONField())]
29+
30+
super().__init__(*expressions, output_field=JSONField(), **extra)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.1.6 on 2023-05-09 11:10
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('blog_examples', '0001_initial'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='SomeDataModel',
15+
fields=[
16+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('name', models.CharField(blank=True, max_length=255)),
18+
('stored_field', models.JSONField(blank=True)),
19+
],
20+
),
21+
]

styleguide_example/blog_examples/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,13 @@ class TimestampsOpinionated(models.Model):
2929

3030
created_at = models.DateTimeField(default=timezone.now)
3131
updated_at = models.DateTimeField(blank=True, null=True)
32+
33+
34+
class SomeDataModel(models.Model):
35+
name = models.CharField(
36+
max_length=255,
37+
blank=True,
38+
)
39+
stored_field = models.JSONField(
40+
blank=True,
41+
)

styleguide_example/blog_examples/tests/services/__init__.py

Whitespace-only changes.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from django.test import TestCase
2+
3+
from styleguide_example.blog_examples.f_expressions.service import JSONIncrement
4+
from styleguide_example.blog_examples.models import SomeDataModel
5+
6+
7+
class JSONIncrementTests(TestCase):
8+
def setUp(self) -> None:
9+
self.first_entity_stored_field = {"first_key": 1, "second_key": 2, "third_key": 3}
10+
self.second_entity_stored_field = {"first_key": 4, "second_key": 5, "third_key": 6}
11+
self.increment_value = 10
12+
13+
def save_model_entities(self):
14+
entities = SomeDataModel.objects.bulk_create(
15+
[
16+
SomeDataModel(name="First name", stored_field=self.first_entity_stored_field),
17+
SomeDataModel(name="Second name", stored_field=self.second_entity_stored_field),
18+
]
19+
)
20+
return entities
21+
22+
def test_json_increment_in_model(self):
23+
self.save_model_entities()
24+
25+
SomeDataModel.objects.filter(name="First name").update(
26+
stored_field=JSONIncrement("stored_field__first_key", self.increment_value)
27+
)
28+
29+
changed_entity = SomeDataModel.objects.first()
30+
second_entity = SomeDataModel.objects.last()
31+
32+
self.assertEqual(
33+
self.first_entity_stored_field["first_key"] + self.increment_value, changed_entity.stored_field["first_key"]
34+
)
35+
self.assertEqual(self.second_entity_stored_field["first_key"], second_entity.stored_field["first_key"])

0 commit comments

Comments
 (0)