Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3ef1b9d
Add all checks makefile command.
skv0zsneg Sep 12, 2025
b77f14d
Update pyproject toml.
skv0zsneg Sep 12, 2025
bf63de5
Update main dir name.
skv0zsneg Sep 12, 2025
763d831
Add msgpack for working with dict field.
skv0zsneg Sep 17, 2025
275f028
Rename param field to dict field.
skv0zsneg Sep 17, 2025
1dfaf99
Refactor serializer for using msgpack.
skv0zsneg Sep 17, 2025
432307b
Upd test model.
skv0zsneg Sep 17, 2025
eb90ed3
Update tests.
skv0zsneg Sep 17, 2025
1d097b6
Add bechmark script.
skv0zsneg Sep 20, 2025
13f125d
Add bechmark deps. Replace msgpack with orjson.
skv0zsneg Sep 20, 2025
d0a8c4a
Replace msgpack with orjson.
skv0zsneg Sep 20, 2025
3876034
Change naming: params field to dict field.
skv0zsneg Sep 20, 2025
566e31b
Add docker compose with postgres db for testing and bench.
skv0zsneg Oct 13, 2025
c3e6110
Add shortcuts with getting up db in Makefile.
skv0zsneg Oct 13, 2025
fd4196b
Add deps for postgresql db.
skv0zsneg Oct 13, 2025
714a7a8
Setup postgresql django settings.
skv0zsneg Oct 13, 2025
523b4bc
Improve bench module.
skv0zsneg Oct 13, 2025
6605c3b
Fix dirs to check.
skv0zsneg Oct 13, 2025
df55bc1
Format job.
skv0zsneg Oct 13, 2025
abce424
Add up and down db.
skv0zsneg Oct 13, 2025
f5d18f2
Merge branch 'dev' into efficient-crud-for-dict-field
skv0zsneg Oct 13, 2025
06569f2
Add benchmark image. Minor README rewrite.
skv0zsneg Dec 4, 2025
b409a85
Replace orjson with msgspec.
skv0zsneg Dec 4, 2025
ac2f004
Replace orjson with msgspec.
skv0zsneg Dec 4, 2025
cbf252c
Upd readme.
skv0zsneg Dec 4, 2025
dc6b627
Increases and improve tests.
skv0zsneg Dec 4, 2025
6ddc2df
Fix typehinting.
skv0zsneg Dec 4, 2025
a7486d2
Fix typehinting.
skv0zsneg Dec 4, 2025
fd068d4
Format job.
skv0zsneg Dec 4, 2025
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
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,9 @@ jobs:
poetry config virtualenvs.in-project true
poetry run pip install -U pip
poetry install
- name: Pull up db
run: make db-up
- name: Run tests
run: make test
- name: Pull down db
run: make db-down
25 changes: 20 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,22 +1,37 @@
SHELL:=/usr/bin/env bash

.PHONY: db-up
db-up:
docker compose up -d

.PHONY: db-down
db-down:
docker compose down -v

.PHONY: unit
unit:
poetry run pytest

.PHONY: benchmark
benchmark:
poetry run python3 benchmarks/jsonfield_benchmark.py

.PHONY: typing
typing:
poetry run mypy src

.PHONY: lint
lint:
poetry run ruff check --select I
poetry run ruff format --check
poetry run ruff check --select I src
poetry run ruff format --check src

.PHONY: format
format:
poetry run ruff check --select I --fix
poetry run ruff format
poetry run ruff check --select I --fix src
poetry run ruff format src

.PHONY: test
test: unit
test: unit

.PHONY: all-checks
all-checks: lint typing test
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

> ⚠️ This project under active developing.

Django extension for efficient and easy storing `dict` data in django field.
Django extension for storing dict data in relation database field with fast performance and flex schema.

## 📍 Purpose

Sometimes we need to store some key value data in storages. In Django we can use `JSONField` with `json` tool to convert or `BinaryField` with some customs transformations. Often it also need to be efficient for work with big data or to have some validation and another features.
Sometimes we need to store some key value data in storages. Often it also need to be efficient for work with big data and have some validation and another features.

`django-dict-field` is a tool build around amazing [msgspec](https://github.com/jcrist/msgspec) serialization and validation library for solving this problems like a charm ✨

**DictField vs JSONField Benchmark**

![DictField vs JSONField Benchmark](docs/media/bechmark.png)

`django-dict-field` here is to solve this problems like a charm ✨

## 🚀 Quick start

Expand Down
File renamed without changes.
105 changes: 105 additions & 0 deletions benchmarks/jsonfield_benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import os
import random
import sys
import timeit

import django
from django.db import connection
from django.test.utils import setup_test_environment, teardown_test_environment
from tabulate import tabulate

project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
django_app_path = os.path.join(project_root, "tests", "django_app")

sys.path.insert(0, project_root)
sys.path.insert(0, django_app_path)

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_app.settings")
django.setup()

setup_test_environment()
db_name = connection.creation.create_test_db(verbosity=1, autoclobber=True)

from dict_field.models import ModelForTest

DICT_SIZE = 100_000
BIG_DICT = {str(i): i for i in range(DICT_SIZE)}


def get_big_dict_field(test_model: ModelForTest, field_name: str) -> None:
rand_index = str(random.randint(0, DICT_SIZE - 1))
test_model = ModelForTest.objects.get(pk=test_model.pk)
test_model.__getattribute__(field_name)[rand_index]


def delete_big_dict_field(test_model: ModelForTest, field_name: str) -> None:
rand_index = str(random.randint(0, DICT_SIZE - 1))
val = test_model.__getattribute__(field_name)[rand_index]
del test_model.__getattribute__(field_name)[rand_index]
test_model.save()
# NOTE: Here is overhead because of saving deleted value. Must to figure
# out how to test delete correctly.
test_model.__getattribute__(field_name)[rand_index] = val
test_model.save()


def update_big_dict_field(test_model: ModelForTest, field_name: str) -> None:
rand_index = str(random.randint(0, DICT_SIZE - 1))
test_model.__getattribute__(field_name)[rand_index] = "restored"
test_model.save()


def run_bench():
# NOTE: Can take a while. Be careful.
number = 1000
test_model_jsonfield = ModelForTest.objects.create(
default_json_field=BIG_DICT, default_dict_field={}
)
test_model_dictfield = ModelForTest.objects.create(
default_json_field={}, default_dict_field=BIG_DICT
)

# JSONField
time_to_get = timeit.timeit(
lambda: get_big_dict_field(test_model_jsonfield, "default_json_field"),
number=number,
)
time_to_delete = timeit.timeit(
lambda: delete_big_dict_field(test_model_jsonfield, "default_json_field"),
number=number,
)
time_to_update = timeit.timeit(
lambda: update_big_dict_field(test_model_jsonfield, "default_json_field"),
number=number,
)
json_field_time = ("JSONField", time_to_get, time_to_update, time_to_delete)

# DictField
time_to_get = timeit.timeit(
lambda: get_big_dict_field(test_model_dictfield, "default_dict_field"),
number=number,
)
time_to_delete = timeit.timeit(
lambda: delete_big_dict_field(test_model_dictfield, "default_dict_field"),
number=number,
)
time_to_update = timeit.timeit(
lambda: update_big_dict_field(test_model_dictfield, "default_dict_field"),
number=number,
)
dict_field_time = ("DictField", time_to_get, time_to_update, time_to_delete)

report_table = tabulate(
(json_field_time, dict_field_time),
headers=("Field", "Get", "Delete", "Update"),
tablefmt="grid",
)
print(report_table)


if __name__ == "__main__":
try:
run_bench()
finally:
teardown_test_environment()
connection.creation.destroy_test_db(db_name, verbosity=1)
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
postgres:
image: postgres:15.14-alpine3.21
ports:
- "8888:5432"
environment:
POSTGRES_PASSWORD: postgrespassword
Binary file added docs/media/bechmark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading