Skip to main content

A drop-in Django model field for storing sortable, time-encoded ULIDs as 26-character strings.

Project description

django-ulidfield

A drop-in Django model field for storing sortable, time-encoded ULIDs as 26-character strings.

What are ULIDs?

ULIDs (Universally Unique Lexicographically Sortable Identifiers) are a modern alternative to UUIDs that combine the benefits of both sequential integers and random UUIDs. They consist of:

  • 48-bit timestamp (milliseconds since Unix epoch)
  • 80-bit randomness
    01AN4Z07BY             79KA1307SR9X4MV3
|----------------|    |------------------------|
       time                   randomness
      48bits                    80bits

ULIDs are encoded in base-32 (Crockford's Base32) resulting in 26-character strings that are:

  • Sortable by creation time
  • URL-safe (no special characters)
  • Case-insensitive
  • Compatible with UUID storage (128-bit)

Why ULIDs over UUIDs?

As explained in Brandur Leach's article "Identity Crisis: Sequence v. UUID as Primary Key", ULIDs solve several problems with traditional UUID v4:

Problems with Random UUIDs

  • Poor database performance: Random UUIDs cause index fragmentation and cache misses
  • High WAL overhead: More write-ahead log data due to scattered page updates
  • No temporal ordering: Can't sort by creation time

ULID Advantages

  • Time-ordered: ULIDs sort naturally by creation time
  • Better database performance: Sequential timestamp prefix reduces index fragmentation
  • Distributed generation: No single point of failure like auto-incrementing integers
  • Opaque to users: Prevents enumeration attacks and business intelligence leakage
  • UUID compatible: Can be stored in UUID columns when needed

Installation

pip install django-ulidfield

Or with Poetry:

poetry add django-ulidfield

Usage

Basic Usage

from django.db import models
from django_ulidfield import ULIDField

class Article(models.Model):
    id = ULIDField(primary_key=True)
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)

# ULIDs are automatically generated
article = Article.objects.create(title="Hello World", content="...")
print(article.id)  # Output: 01AN4Z07BY79KA1307SR9X4MV3

Non-Primary Key Usage

class Order(models.Model):
    id = models.AutoField(primary_key=True)
    order_id = ULIDField()  # Unique by default
    customer_email = models.EmailField()
    total = models.DecimalField(max_digits=10, decimal_places=2)

Custom Configuration

class Document(models.Model):
    # Allow null values
    doc_id = ULIDField(null=True, blank=True)

    # Custom default function
    tracking_id = ULIDField(default=None, null=True)

    # Allow duplicates (not recommended)
    reference_id = ULIDField(unique=False)

Field Options

ULIDField inherits from Django's CharField and accepts all the same options, with these defaults:

  • max_length=26 (ULIDs are always 26 characters)
  • unique=True (ULIDs should be unique)
  • editable=False (ULIDs are typically auto-generated)
  • default=generate_ulid (automatically generates new ULIDs)
  • blank=False (ULIDs are required by default)

Validation

The field automatically validates that values are proper ULIDs:

# This will raise a ValidationError
invalid_article = Article(id=\"invalid-ulid\")
invalid_article.full_clean()  # ValidationError: 'invalid-ulid' is not a valid ULID

Database Considerations

Index Performance

ULIDs provide better database performance than random UUIDs because:

  • The timestamp prefix keeps new insertions clustered together
  • Reduces index page splits and cache misses
  • Minimizes write-ahead log (WAL) overhead

Storage

  • Database storage: 26 characters (can be optimized to 16 bytes in UUID columns)
  • Memory/JSON: 26-character string
  • URL-safe: Can be used directly in URLs

Migration from UUIDs

If you're migrating from UUIDs, you can:

  1. Direct replacement (new records only):
# Change this:
id = models.UUIDField(primary_key=True, default=uuid.uuid4)

# To this:
id = ULIDField(primary_key=True)
  1. Gradual migration (with a new field):
class MyModel(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4)  # Keep existing
    ulid = ULIDField(null=True, blank=True)  # Add new field

Time Extraction

You can extract the timestamp from a ULID:

from ulid import ULID

# Get timestamp from ULID
ulid_obj = ULID.from_str(article.id)
timestamp = ulid_obj.timestamp()
datetime_obj = ulid_obj.datetime()

Development

Setup

git clone https://github.com/your-username/django-ulidfield
cd django-ulidfield
poetry install
poetry run pre-commit install

Running Tests

poetry run pytest

Code Quality

This project uses:

  • Ruff for linting and formatting
  • pytest for testing
  • pre-commit for code quality checks

Requirements

  • Python 3.9+
  • Django 4.2+
  • python-ulid 3.0.0+

License

MIT License - see LICENSE file for details.

Related Resources

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_ulidfield-0.1.0.tar.gz (5.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_ulidfield-0.1.0-py3-none-any.whl (5.7 kB view details)

Uploaded Python 3

File details

Details for the file django_ulidfield-0.1.0.tar.gz.

File metadata

  • Download URL: django_ulidfield-0.1.0.tar.gz
  • Upload date:
  • Size: 5.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.3 CPython/3.12.3 Linux/6.11.0-1018-azure

File hashes

Hashes for django_ulidfield-0.1.0.tar.gz
Algorithm Hash digest
SHA256 890d3143d604ff7f67647bff91482c5e6d8c8deaa77b272da37d9b1ae98f2418
MD5 2800cf1685cf8b7b36a08a8dbade01e1
BLAKE2b-256 5f452006f76ed1d09b0a0a5ad9f2fb8823f8da0fa17de9ba8295a390d8150f9e

See more details on using hashes here.

File details

Details for the file django_ulidfield-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: django_ulidfield-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 5.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.3 CPython/3.12.3 Linux/6.11.0-1018-azure

File hashes

Hashes for django_ulidfield-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ad03f9c8c2677a56da66c8211d2834fa4ef54fe1b453916e9400061a7d173f03
MD5 eeffed23b1806710b392a7fc32fd7e64
BLAKE2b-256 e9c39a69c26e00ce1d8f46bc6ccb9153f6e5468fbd4deacfc83001ee09b9948e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page