# Django HStore Project A monorepo for the Django HStore ecosystem providing a human-friendly, Lit-based key-value editor for PostgreSQL hstore fields in Django admin. - **Python**: 3.10+ - **Django**: 5.0+ - **PostgreSQL**: 14+ # Packages ## django-hstore-widget Lower-level package providing the custom hstore editor widget for Django admin. - **HStoreFormWidget** — Lit-based key-value editor replacing the default textarea - **HStoreFormField** — Form field wired to the custom widget with JSON parsing - **check_database_backend_is_postgres** — System check that warns when the default database is not PostgreSQL ## django-hstore-field Higher-level package providing a drop-in HStoreField replacement. - **HStoreField** — Overrides ``formfield()`` so the admin and ModelForms automatically use ``HStoreFormField`` and ``HStoreFormWidget`` with no manual form configuration required. # Installation Installation ============ Get up and running with django-hstore-project in minutes. Requirements ------------ - :iconify:`mdi:python` **Python 3.10+** - :iconify:`logos:django` **Django 5.0+** - :iconify:`logos:postgresql` **PostgreSQL 14+** - Modern browsers (Chrome 112+, Firefox 117+, Safari 16.5+) Install django-hstore-field --------------------------- .. tabs:: .. tab:: :iconify:`logos:pypi` pip .. termynal:: $ pip install django-hstore-field --> Collecting django-hstore-field Collecting django-hstore-widget Installing collected packages... Successfully installed django-hstore-field django-hstore-widget .. tab:: :iconify:`simple-icons:astral` uv .. termynal:: $ uv pip install django-hstore-field --> Collecting django-hstore-field Collecting django-hstore-widget Installing collected packages... Successfully installed django-hstore-field django-hstore-widget .. tab:: :iconify:`simple-icons:poetry` Poetry .. termynal:: $ poetry add django-hstore-field --> Creating virtualenv: django-hstore-project Package django-hstore-field added. Package django-hstore-widget added (dependency). .. tab:: :iconify:`simple-icons:pdm` PDM .. termynal:: $ pdm add django-hstore-field --> Adding packages to workspace: django-hstore-field Package django-hstore-widget added (dependency). All packages are installed. .. tab:: :iconify:`mdi:package-variant` pip-tools .. termynal:: $ echo "django-hstore-field" >> requirements.in $ pip-compile requirements.in --> # # This file is autogenerated by pip-compile # django-hstore-field==1.0.0 django-hstore-widget==1.0.0 $ pip-sync requirements.txt Configure --------- Add both packages to your ``INSTALLED_APPS``: .. code-block:: python # settings.py INSTALLED_APPS = [ ..., 'django_hstore_widget', 'django_hstore_field', ..., ] Run Migrations -------------- .. termynal:: $ python manage.py migrate --> Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK All migrations completed. This enables the PostgreSQL hstore extension automatically. Next ---- Head over to the :doc:`quickstart` to use HStore fields in your models. # Quick Start Quick Start =========== Get up and running with django-hstore-project in under a minute. Create a Model -------------- Add an HStore field to any model: .. code-block:: python # models.py from django.db import models from django_hstore_field import HStoreField class Product(models.Model): name = models.CharField(max_length=100) metadata = HStoreField() That's it. The widget is auto-wired, no form configuration needed. Run Migrations -------------- .. code-block:: bash python manage.py makemigrations python manage.py migrate This enables the PostgreSQL hstore extension automatically. Register in Admin ----------------- .. code-block:: python # admin.py from django.contrib import admin from .models import Product @admin.register(Product) class ProductAdmin(admin.ModelAdmin): pass Visit your admin page and you'll see the HStore widget with a clean key-value interface. Each key-value pair is editable inline. Add, remove, and modify pairs as needed. With Custom Keys ---------------- Define explicit keys to get labeled form fields instead of the free-form editor: .. code-block:: python from django import forms from django_hstore_field import HStoreField class Product(models.Model): name = models.CharField(max_length=100) metadata = HStoreField( keys=[ ('color', {'widget': forms.TextInput}), ('size', {'widget': forms.Select, 'choices': SIZE_CHOICES}), ('material', {'widget': forms.TextInput}), ], ) Each key becomes a proper form field with validation, widgets, and labels. What's Next ----------- - :doc:`../user-guide/installation` - Detailed installation options - :doc:`../technical/hstore-vs-jsonb` - When to use HStore vs JSONB - :doc:`../user-guide/best-practices` - Tips for production use # django-hstore-widget API ## django_hstore_widget.widgets.HStoreFormWidget Django admin widget that renders the custom hstore editor. Replaces the default textarea with a Lit-based key-value editor component. Loads the widget JavaScript as an ES module via the ``Media`` inner class. ```python from django_hstore_widget.widgets import HStoreFormWidget widget = HStoreFormWidget() ``` ### render(name, value, attrs=None, renderer=None) Render the widget HTML for a given form field. - ``name`` — HTML name attribute for the field - ``value`` — Current hstore data as a key-value mapping - ``attrs`` — Additional HTML attributes passed to the template ### Media.js ``['admin/js/django_hstore_widget/django-hstore-widget.js']`` ## django_hstore_widget.forms.HStoreFormField Form field that uses ``HStoreFormWidget``. Extends Django's built-in ``HStoreField`` to inject the custom widget and override ``clean()`` so incoming values are parsed through ``json.loads``. ```python from django_hstore_widget.forms import HStoreFormField field = HStoreFormField() ``` ### clean(value) Parse the raw form value into a Python dict using ``json.loads``. ## django_hstore_widget.checks.check_database_backend_is_postgres System check that warns when the default DB backend is not PostgreSQL. Runs during ``manage.py check`` and ``manage.py migrate``. # django-hstore-field API ## django_hstore_field.HStoreField Drop-in replacement for Django's ``HStoreField`` with the custom widget. Overrides ``formfield()`` so that the admin and ModelForms automatically use ``HStoreFormField`` and ``HStoreFormWidget``. ```python from django.db import models from django_hstore_field import HStoreField class Product(models.Model): name = models.CharField(max_length=100) metadata = HStoreField() class Meta: app_label = "example" ``` ### formfield(**kwargs) Return a form field class pre-configured with the custom widget. Pass ``form_class`` or ``widget`` to override the defaults. # Frontend Architecture Architecture ============ Overview -------- .. mermaid:: graph LR A[django_hstore_field] --> B[django_hstore_widget] B --> C[Django runtime] B --> D[Lit frontend] django-hstore-field ------------------- Drop-in HStoreField that auto-wires the widget. No form configuration needed. django-hstore-widget -------------------- Lit-based frontend component with Django admin integration. Handles rendering, validation, and media registration. Frontend Build Pipeline ======================= Overview -------- The widget frontend is built with Vite and TypeScript, producing a single IIFE bundle loaded by Django admin. Build Steps ----------- 1. Vite bundles ``src/frontend/index.ts`` into ``dist/components/django-hstore-widget.js`` 2. ``scripts/copy.ts`` copies the bundle to ``src/django_hstore_widget/static/admin/js/django_hstore_widget/`` 3. Django serves the bundle via static files Configuration ------------- - ``vite.config.ts`` - Entry point, output, aliases, dev server - ``tsconfig.json`` - TypeScript compiler options, path aliases Path Aliases ------------ .. grid:: 2 :gutter: 1 .. grid-item:: :columns: 12 12 6 6 .. code-block:: text $lib/* Resolves to ``src/frontend/lib/*`` .. grid-item:: :columns: 12 12 6 6 .. code-block:: text $store/* Resolves to ``src/frontend/stores/*`` .. grid-item:: :columns: 12 12 6 6 .. code-block:: text $mappping/* Resolves to ``src/frontend/mappings/*`` .. grid-item:: :columns: 12 12 6 6 .. code-block:: text $components/* Resolves to ``src/frontend/components/*`` Development ----------- .. code-block:: bash # Start dev server npm run dev # The dev server runs on port 9100 with HMR # Best Practices Best Practices ============== Tips and recommendations for using HStore fields effectively in production. Keep Keys Consistent -------------------- Use a naming convention for keys across your application. This makes querying and maintenance easier. .. code-block:: python # Good: consistent snake_case keys metadata = HStoreField(keys=[ ('product_color', {'widget': forms.TextInput}), ('product_size', {'widget': forms.TextInput}), ]) # Avoid: mixing conventions metadata = HStoreField(keys=[ ('product_color', {'widget': forms.TextInput}), ('Size', {'widget': forms.TextInput}), ]) Use Explicit Keys When Possible ------------------------------- Defining explicit keys gives you: - Labeled form fields in the admin - Built-in validation per key - Widget customization per field - IDE autocomplete for key names .. code-block:: python class Product(models.Model): metadata = HStoreField( keys=[ ('color', {'widget': forms.TextInput}), ('size', {'widget': forms.Select, 'choices': SIZE_CHOICES}), ], ) Index Frequently Queried Keys ----------------------------- Add GIN indexes on HStore columns you query often: .. code-block:: python from django.db import models class Product(models.Model): attributes = HStoreField() class Meta: indexes = [ models.GinIndex(fields=['attributes'], name='product_attrs_gin'), ] Handle Missing Keys Gracefully ------------------------------ HStore keys may not exist in all rows. Use ``get()`` with defaults: .. code-block:: python product = Product.objects.first() color = product.attributes.get('color', 'default_color') Validate Key Values ------------------- HStore values are strings. Validate and convert them where needed: .. code-block:: python from django.core.exceptions import ValidationError def clean(self): if 'price' in self.attributes: try: float(self.attributes['price']) except (ValueError, TypeError): raise ValidationError({'attributes': 'Price must be a number.'}) Consider Migration Paths ------------------------ If you anticipate needing nested data in the future, plan your migration from HStore to JSONB early. The data formats are not directly compatible. # HStore vs JSONB HStore vs JSONB =============== PostgreSQL offers two flexible data types for key-value and document storage. This page compares them to help you choose the right one. Quick Decision -------------- Use **HStore** when your data is flat key-value pairs, which is most metadata, tags, and product attributes. Use **JSONB** when you need nested structures, mixed value types, or containment queries. Feature Matrix -------------- .. grid:: 3 :gutter: 1 .. grid-item:: Feature :columns: 12 :child-align: center +------------------+----------+---------+ | Feature | HStore | JSONB | +==================+==========+=========+ | Flat key-value | Yes | Yes | +------------------+----------+---------+ | Nested structs | No | Yes | +------------------+----------+---------+ | Non-string vals | No | Yes | +------------------+----------+---------+ | Simple admin UI | Yes | No | +------------------+----------+---------+ | Easy validation | Yes | No | +------------------+----------+---------+ | Containment | No | Yes | +------------------+----------+---------+ | Smaller storage | Yes | No | +------------------+----------+---------+ | GIN indexes | Yes | Yes | +------------------+----------+---------+ Storage ------- HStore is more compact because it stores flat key-value pairs without structural overhead: .. note:: - HStore: ~200 bytes for 10 key-value pairs - JSONB: ~350 bytes for the same data (structure overhead) Query Performance ----------------- HStore ~~~~~~ .. tabs:: .. tab:: :iconify:`logos:django` Django ORM .. code-block:: python # Find all red products Product.objects.filter(attributes__color='red') # Products with a size attribute Product.objects.filter(attributes__has_key='size') .. tab:: :iconify:`mdi:database` SQL .. code-block:: sql -- Check if a key exists SELECT * FROM products WHERE attributes ? 'size'; -- Get a specific value SELECT attributes->'color' FROM products; -- Query by key-value pair SELECT * FROM products WHERE attributes->'color' = 'red'; JSONB ~~~~~ .. tabs:: .. tab:: :iconify:`logos:django` Django ORM .. code-block:: python # Nested value access Config.objects.filter(settings__cache__ttl__gt=300) # Containment query Config.objects.filter(settings__contains={"enabled": True}) .. tab:: :iconify:`mdi:database` SQL .. code-block:: sql -- Nested value access SELECT * FROM configs WHERE settings->'cache'->>'ttl' > 300; -- Containment query SELECT * FROM configs WHERE settings @> '{"enabled": true}'; Indexing -------- Both HStore and JSONB support GIN indexes for fast lookups: .. code-block:: sql CREATE INDEX products_attrs_gin ON products USING GIN (attributes); CREATE INDEX configs_settings_gin ON configs USING GIN (settings); Summary ------- .. tip:: Choose HStore for simple metadata, tags, and product attributes. Choose JSONB for nested configurations, API responses, and document storage. # Contributing Contributing ============ Step-by-step guide to setting up your development environment and contributing to django-hstore-project. Prerequisites ------------- Make sure you have the following installed: - :iconify:`mdi:python` **Python 3.10+** with ``uv`` package manager - :iconify:`logos:nodejs` **Node.js 18+** with ``npm`` - :iconify:`logos:postgresql` **PostgreSQL 14+** running locally Clone the Repository -------------------- .. termynal:: $ git clone https://github.com/baseplate-admin/django-hstore-project.git Cloning into 'django-hstore-project'... $ cd django-hstore-project Install Python Dependencies ---------------------------- .. tabs:: .. tab:: :iconify:`simple-icons:astral` uv .. termynal:: $ uv sync --group test --> Resolved 69 packages in 12ms Preparing 5 packages in 3.2s Installed 5 packages in 8ms .. tab:: :iconify:`simple-icons:poetry` Poetry .. termynal:: $ poetry install --with test --> Updating dependencies Resolving dependencies... Installing 69 packages in 3.2s .. tab:: :iconify:`simple-icons:pdm` PDM .. termynal:: $ pdm install --with test --> Adding revisions of 69 packages Installing 69 packages in 3.2s .. tab:: :iconify:`mdi:package-variant` pip-tools .. termynal:: $ pip-compile requirements-dev.in --> # This file is autogenerated by pip-compile django-hstore-field==1.0.0 pytest==7.4.0 $ pip-sync requirements-dev.txt This installs all Python packages including test dependencies. Install Frontend Dependencies ----------------------------- .. termynal:: $ cd packages/django_hstore_widget $ npm install --> added 412 packages in 18s 45 packages are looking for funding run `npm fund` for details This installs Lit, Vite, and all frontend tooling. Set Up the Database ------------------- Start a local PostgreSQL instance with the hstore extension enabled: .. tabs:: .. tab:: :iconify:`logos:django` Django ORM .. termynal:: $ python manage.py migrate --> Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK All migrations completed. .. tab:: :iconify:`mdi:database` SQL .. termynal:: $ psql -c "CREATE DATABASE django_hstore_dev;" --> CREATE DATABASE $ psql -d django_hstore_dev -c "CREATE EXTENSION hstore;" --> CREATE EXTENSION Run the Tests ------------- Frontend Tests ~~~~~~~~~~~~~~ .. termynal:: $ npm run test:jest --> PASS tests/widget.test.ts PASS tests/parser.test.ts Test Suites: 2 passed, 2 total Tests: 14 passed, 14 total $ npm test --> Running 2 tests using 2 workers ✓ e2e/widget.spec.ts (1.2s) 2 passed (1.5s) Backend Tests ~~~~~~~~~~~~~ .. termynal:: $ uv run pytest packages/django_hstore_widget/tests/ -v --> test_widget_render.py::test_widget_media PASSED test_widget_render.py::test_widget_value PASSED test_widget_render.py::test_widget_init PASSED ================= 3 passed in 0.12s ================= $ uv run pytest packages/django_hstore_field/tests/ -v --> test_field.py::test_hstore_field_formfield PASSED test_field.py::test_hstore_field_deconstruct PASSED ================= 2 passed in 0.08s ================= Type Checking ~~~~~~~~~~~~~ .. termynal:: $ npm run typecheck --> Found 0 errors. Build the Project ----------------- .. termynal:: $ npm run build --> vite v5.4.19 building for production... transforming... 12 modules transformed. rendering chunks... computed gzip sizes: admin/js/django_hstore_widget/django-hstore-widget.js 24.12 kB │ gzip: 8.34 kB ✓ built in 342ms Start the Dev Server -------------------- .. termynal:: $ npm run dev --> ➜ Local: http://localhost:9100/ ➜ press h + enter to show help The dev server runs on port 9100 with hot module replacement enabled. Code Style ---------- - **Python**: Ruff (linter + formatter) - **TypeScript**: Prettier - Pre-commit hooks are configured for automated formatting Before submitting a pull request, run: .. termynal:: $ uv run ruff check --fix --> All checks passed! $ uv run ruff format --> 12 files left unchanged. $ npm run format --> Checked 12 files.