Repository navigation
Declare unique constraints - #139
Merged
Merged
Conversation
jbylund
force-pushed
the
feat/declared-primary-key
branch
from
August 17, 2026 17:26
bc0c255 to
e5ce3b9
Compare
Table takes `unique`, any number of keys spelled the way primary_key is, each
normalised and validated the same way. Two identical keys are refused: they
would name two constraints the same thing and `\d` would render a duplicated
line. Postgres tolerates that and disambiguates with a counter, but here it is
almost certainly a copy-paste.
`\d` output is byte-identical to PostgreSQL 18 for the same declaration,
including the order -- psql sorts the footer `indisprimary DESC, relname`:
Indexes:
"u1_pkey" PRIMARY KEY, btree (id)
"u1_a_b_key" UNIQUE CONSTRAINT, btree (a, b)
"u1_email_key" UNIQUE CONSTRAINT, btree (email)
"u1_oddName_key" UNIQUE CONSTRAINT, btree ("oddName")
Most of the diff is that #127 assumed one key per table. Index and constraint
OIDs came from the *table's* position, and the row builder was a single `if
table.primary_key:` block, so a table with three unique constraints would have
issued three rows with one OID. Both are now driven by a running count over the
rows actually emitted, and the two kinds of key share one code path that differs
in exactly three places: contype and the PRIMARY KEY/UNIQUE rendering, the
indisprimary flag psql reads to choose between the two footer labels, and
whether the columns are implicitly NOT NULL. Only a primary key's are, which is
the one substantive difference between them.
Index names are relation names, so they are unique across the schema rather than
per table, and generated the way Postgres generates them: `t_pkey`, or
`t_<columns>_key` with the column names unquoted whatever they are -- Postgres
builds `u1_oddName_key` from `"oddName"` and quotes the whole name when it prints
it. Truncated to 63 bytes, which is NAMEDATALEN - 1 and reachable with an
ordinary long table name plus two columns, and a truncation that collides gets a
counter as ChooseIndexName does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jbylund
force-pushed
the
feat/declared-unique
branch
from
August 17, 2026 17:34
22b4781 to
ec71a48
Compare
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.
Closes #128. Stacked on #138, which is stacked on #137 — this PR targets #138's branch, so the diff here is only the unique-constraint work.
Tabletakesunique, any number of keys spelled the wayprimary_keyis:Output is byte-identical to PostgreSQL 18 for the same declaration, ordering included — psql sorts the footer
indisprimary DESC, relname:Most of the diff is undoing a one-key-per-table assumption
#128 was supposed to be small, and the declaration half was. The catalog half was not, because #127 had baked in "at most one key per table" in two places:
_FIRST_INDEX_OID + index), so a table with three unique constraints would have issued three rows sharing one OID.if table.primary_key:block.Both are now driven by a running count over the rows actually emitted. Worth knowing for #129, which adds a third kind of constraint to the same loop.
Where the two kinds of key actually differ
They share one code path that branches in exactly three places, which is the useful summary:
contype/ renderingp/PRIMARY KEY (...)u/UNIQUE (...)indisprimaryThe third is the one substantive difference, and it has a test asserting
\d u1saysnot nullexactly once.Naming
Index names are relation names, so they are unique across the schema rather than per table, and generated the way Postgres generates them:
t_pkey, ort_<columns>_keywith the column names unquoted whatever they are — Postgres buildsu1_oddName_keyfrom"oddName"and quotes the whole name only when printing it.Truncated to 63 bytes (NAMEDATALEN - 1), which is reachable with an ordinary long table name plus two columns, and a truncation that collides gets a counter as Postgres' own
ChooseIndexNamedoes. Identical keys are refused at declaration instead, since that is a copy-paste rather than a name clash.Tests
18 new.
test_declared.pyfor normalisation and the three refusals;test_pg_catalog.pyrunning psql's footer query verbatim and asserting the full ordered row set, plus the truncation and OID-uniqueness cases;test_psql.pyasserting all four rendered footer lines against the real binary.Full suite: 946 passed, 32 xfailed (up from 928).
ruff checkandruff format --checkclean.Not in scope
Foreign keys (#129) and declaring a constraint after construction (#130). Unique indexes that are not constraints —
CREATE UNIQUE INDEXwithoutADD CONSTRAINT, which psql prints asUNIQUE,rather thanUNIQUE CONSTRAINT,— have no declaration surface and nothing needs them.