Skip to content

Add NULLS NOT DISTINCT index support (PostgreSQL)#36

Open
ebrake wants to merge 1 commit into
mr-fatalyst:mainfrom
ebrake:feature/postgres-nulls-not-distinct
Open

Add NULLS NOT DISTINCT index support (PostgreSQL)#36
ebrake wants to merge 1 commit into
mr-fatalyst:mainfrom
ebrake:feature/postgres-nulls-not-distinct

Conversation

@ebrake

@ebrake ebrake commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds PostgreSQL NULLS NOT DISTINCT support for table-level Oxyde indexes:

Index(("tenant_id", "email"), unique=True, nulls_not_distinct=True)

This lets unique indexes treat NULL values as equal on PostgreSQL 15+.

Changes

  • Adds nulls_not_distinct to the Python Index(...) API.
  • Validates that nulls_not_distinct=True requires unique=True.
  • Preserves the flag through schema extraction, migration diffing, generated migration operations, and replay.
  • Extends Rust IndexDef with a backward-compatible serde default.
  • Emits NULLS NOT DISTINCT in PostgreSQL index DDL.
  • Rejects unsupported dialects with a clear migration error.
  • Documents usage in the performance/indexing guide.

Tests

  • cargo test -p oxyde-migrate
  • uv run --project python --extra dev --no-sync pytest python/oxyde/tests/unit/test_migrations_detection.py python/oxyde/tests/unit/
    test_migrations_pipeline.py python/oxyde/tests/unit/test_migrations_execution.py
  • make lint

@ebrake

ebrake commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Hey @mr-fatalyst this is just a proposal - there are many postgres features that other dbs don't have, and I totally understand if this is going too far!

For dedupe/idempotency in a task queue, I had to write a custom migration to support NULLS NOT DISTINCT

basically:

Index(
      ("queue", "task_type", "dedupe_key"),
      unique=True,
      nulls_not_distinct=True,
      where="state IN ('queued', 'running')",
  )

That enforces: “only one active task for this queue/type/key.” With NULLS NOT DISTINCT, dedupe_key = NULL is also treated as a real bucket, so you cannot accidentally enqueue unlimited “same task but no dedupe key” rows. (Without it, PostgreSQL unique indexes allow multiple NULLs, so this would not dedupe rows where dedupe_key is NULL)

Useful for one active sync per tenant/resource.. one active scheduled job per logical slot.. singleton queue jobs where optional scope fields can be NULL.. idempotency keys where missing key should still mean “same default key”.. things like that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant