django-enum.IntFlag-field — A model field that takes an enum.IntFlag class, stores flag combinations as an IntegerField, and allows convenient bitwise containment predicates through the Django ORM.
Project description
django-enum.IntFlag-field
This gives you a model field that takes a Python stdlib enum.IntFlag class, stores flag combinations as an IntegerField, and allows convenient bitwise containment predicates through the Django ORM.
In other words: a bitfield in your DB, with all the conveniences of enum.IntFlag.
What is a bitfield, even
What's a bitfield? A bitfield is a field of bits :-). Like 0110
.
You can use it to represent a bunch of booleans that in turn represent presence or absence of flags, or anything you want — perhaps sandwich toppings!
Let's say these 4 field positions represent Butter, Gouda, Rocket and Tomato. Then 0110
means we have a sandwich without Butter, but with Gouda and Rocket, but no Tomato.
A bitfield is thus a string of bits. And it just so happens that an integer is also a string of bits! 0110
is the number 6, look:
>>> 0b0110
6
And it just so happens that both Python and databases have facilities to work with numbers-as-bitfields or bitfields-as-numbers.
A database 64-bit BigIntegerField can accommodate a Python enum.IntFlag class
with up to 62 members (Yes, not 64, as we can only use the positive half, and also not 63, since we need space to express the other flags: 2⁰ + 2¹ + 2² + … + 2⁶² = 2⁶³-1
). When you use this ModelField, a Django check will make sure your enum.IntFlag will not contain more than 62 members.
Installing
pip install django-enum.IntFlag-field
. Or equivalent.
There's no need to add anything to your settings.INSTALLED_APPS
.
How do I even
Example model and parafernalia, inspired by the test models.py:
from enum import IntFlag, auto
from django.db import models
from enum_intflagfield import IntFlagField
class SandwichTopping(IntFlag):
BUTTER = auto() # becomes 1 (2**0)
GOUDA = auto() # becomes 2 (2**1)
ROCKET = auto() # becomes 4 (2**2)
TOMATO = auto() # becomes 8 (2**3)
HUMMUS = auto() # becomes 16 (2**4)
class Sandwich(models.Model):
class Meta:
constraints = [
models.CheckConstraint(
check=models.Q(toppings__contains_noneof=IntFlagField.complement(SandwichTopping)),
name='forbid_spurious_bits',
violation_error_message='Field contains spurious bits (bits not representing any Topping member)',
)
]
name = models.CharField(primary_key=True)
toppings = IntFlagField(choices=SandwichTopping, unique=True)
And then you'd use it like so — example adapted from the tests:
>>> Sandwich.objects.create(
name='Healthy',
toppings=SandwichTopping.ROCKET | SandwichTopping.TOMATO | SandwichTopping.HUMMUS,
)
<Sandwich: Sandwich object (Healthy)>
# gives you the sandwich you just saved — on readback, the integer value is neatly converted into an IntFlag combo.
# 28 is the numeric value that was actually stored in the database — as with all Enums, you can get at it with `.value`.
>>> Sandwich.objects.get(name='Healthy').toppings
<SandwichTopping.ROCKET|TOMATO|HUMMUS: 28>
# gives you all sandwiches that have *ONE OR MORE* of (Butter, Hummus) toppings
>>> Sandwich.objects.filter(toppings__contains_anyof=SandwichTopping.BUTTER | SandwichTopping.HUMMUS)
<QuerySet [<Sandwich: Sandwich object (Healthy)>]>
# gives you all sandwiches that have *AT LEAST ALL OF* (Rocket, Tomato) toppings
>>> Sandwich.objects.filter(toppings__contains=SandwichTopping.ROCKET | SandwichTopping.TOMATO)
<QuerySet [<Sandwich: Sandwich object (Healthy)>]>
# gives you all sandwiches that have *NONE OF* (Butter, Gouda) toppings
>>> Sandwich.objects.filter(toppings__contains=SandwichTopping.Butter | SandwichTopping.Gouda)
<QuerySet [<Sandwich: Sandwich object (Healthy)>]>
This is wrong and not even in 1NF
True! It's a field with a set of values. You can set indices on it, but it's quite an art to match index expressions to query patterns. And you'll need to manage your Enum class wisely; deleting a member means changing over from auto()
to hardcoded powers-of-2 as you don't want your semantics to shift, presumably! There's advantages too, in particular CHECK
constraints are straightforward.
Alternatives:
- add a boolean column for every flag. Cons: Lots of columns, thus lots of schema changes when your set-of-booleans definition change often. Performance & efficiency suffer. Pros: Much easier to design indices for.
- use a many2many design. Cons: performance & efficiency suffer. Some queries are harder.
CHECK
constraints won't allow you to express all you would be able to express on a bitfield-containing row. Pros: You'll be doing it by the book.
Yet sometimes, you really just want a blessed bitfield. I couldn't find any ergonomic ones on PyPI (2024), so I made this one.
Contributing
You may want to discuss your idea on the mailinglist first. Or just send a patch straight away, see https://git-send-email.io/ to learn how.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for django_enum_intflag_field-0.0.1.tar.gz
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2fee87629b9207a2e4650f0583bb4d0c5099bf0b97a49084e55a4c80b9734e01 |
|
MD5 | 32622a1389dc48bfbb162ba767ab8d88 |
|
BLAKE2b-256 | 3346e07734ad7eb0042387080982e7d0f0253beb235371b0cd64f9582fdd119c |
Hashes for django_enum.IntFlag_field-0.0.1-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | af206cbdfe5187a0fd34d096e9de5cd44c27c8b470b95130dbac13ba3196d883 |
|
MD5 | ee025f2dd19bf6fc0fe62dbcc9b1396d |
|
BLAKE2b-256 | 1715b5615e539282e741d91baabe2fb4fff9b03024fb75c1669dcef1797a0ffa |