feat(#19): composite uniqueness via Models.UniqueConstraint (add-only) - #160
Merged
Conversation
Add Django-style multi-column uniqueness (unique_together), spelled as named UniqueConstraint objects declared via the model-level `constraints=` kwarg on Model(...). Each constraint is materialized as a CREATE UNIQUE INDEX at table creation (idempotent, byte-identical on PostgreSQL and SQLite), generalizing the existing ManyToManyField auto-index pipeline: model.cache -> planner -> Dialect.create_unique_index. - Models: UniqueConstraint type + validation (fields must exist and be concrete columns, not M2M; no duplicate or blank names); constraints= on both the named and the idiomatic no-positional-name Model forms; round-trip via Model_to_str. - Planner: _add_unique_constraints emits the index in _add_new_table and guards against colliding index names. - Django importer: maps Meta.unique_together (resolving the FK _id suffix), leniently warning and skipping a malformed declaration instead of aborting. - Tests: DB-free unit coverage (construction/validation, planner rendering on both backends, Model_to_str round-trip, importer) plus an isolated integration phase proving the index rejects a duplicate on PostgreSQL and SQLite. - Docs: models.md (incl. the 63-char identifier-limit note), import_django.md, fields.md, UPGRADING.md. Add-only: a constraint is created with its table (same lifecycle as the M2M auto-index); diffing add/drop/change on an already-migrated table is deferred (needs composite-unique introspection on both backends). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Django-style composite/multi-column uniqueness (
unique_together) for regular models — issue #19. Single-columnunique=truewas already supported; this adds uniqueness spanning two or more columns.Spelled as named
Models.UniqueConstraintobjects (the Django 2.2+ / SQLAlchemy form) declared via a model-levelconstraints=kwarg:At migration time each constraint becomes a
CREATE UNIQUE INDEX— byte-identical on PostgreSQL and SQLite:Design
ManyToManyFieldpipeline —model.cache[…]→ planner →Dialect.create_unique_index— instead of new machinery. No new SQL rendering.This PR references #19 but does not fully close it — the migration-diff requirement (#3 in the issue) is the deferred follow-up.
What changed
src/Models.jl—UniqueConstrainttype + validation (fields must exist and be concrete columns, notManyToManyField; no duplicate or blank names);constraints=on both the namedModel("t"; …)and the idiomatic no-positional-nameModel(; …)forms; round-trip emission inModel_to_str.src/migrations/planner.jl—_add_unique_constraints(sibling of_add_many_to_many_auto_constraints), called from_add_new_table; resolves field names to physical columns viamodel_column(honorsdb_column); guards against colliding index names.src/migrations/importers.jl— the Django importer mapsMeta.unique_togethertoUniqueConstraint(resolving the FK_idsuffix), leniently warning and skipping a malformed declaration rather than aborting the import.models.md(with the 63-char identifier-limit note),import_django.md,fields.md,UPGRADING.md.Testing
test/unit/test_unique_constraints.jl, DB-free): construction/validation, planner rendering on both backends,Model_to_strround-trip, importer mapping + leniency. Full unit suite 2619/2619.test_migration_bootstrap.jl, isolated temp DB): declare aUniqueConstraint→ migrate → assert the composite index exists → assert a duplicate insert is rejected and a different pair is accepted. Runs on both backends.UniqueViolation: duplicate key value violates unique constraint "uniqtest_season_round_uniq".UNIQUE constraint failed: uniqtest.season, uniqtest.round.Follow-up (deferred)
Full add/drop/change diff across migrations (Django's
AlterUniqueTogether): needs a new composite-unique introspection query returning(index_name, columns, is_unique)for both PostgreSQL and SQLite (today PG discards composite-unique column sets and the SQLite diff doesn't read indexes at all), plus a table-level diff pass and the SQLite table-rebuild interaction.🤖 Generated with Claude Code