Pg upgrade bug with NOT NULL NOT VALID

First seen: 2026-05-21 12:14:07+00:00 · Messages: 2 · Participants: 2

Latest Update

2026-05-22 · claude-opus-4-6

Pg_upgrade Bug with NOT NULL NOT VALID Constraints

Core Problem

This thread identifies a pg_upgrade compatibility bug introduced by PostgreSQL 18's treatment of NOT NULL constraints as first-class catalog objects (rather than just column-level attnotnull flags). The issue arises from a specific interaction between constraint naming conventions across major versions.

The Scenario

In PostgreSQL 17, a user creates a table with an inline NOT NULL constraint:

CREATE TABLE t(i int NOT NULL);

This sets attnotnull = true on column i but does not create a named constraint in pg_constraint. The user then creates a CHECK constraint that happens to use the same naming pattern that PG18 would auto-generate for the NOT NULL constraint:

ALTER TABLE t ADD CONSTRAINT t_i_not_null CHECK ((i IS NOT NULL)) NOT VALID;

When upgrading to PG18 (current master), pg_dump --binary-upgrade attempts to recreate the NOT NULL constraint as a proper catalog entry with the auto-generated name t_i_not_null. This collides with the pre-existing CHECK constraint of the same name, causing pg_restore to fail with:

ERROR: constraint "t_i_not_null" for relation "t" already exists

Why This Matters Architecturally

PostgreSQL 18 changed NOT NULL constraints from being implicit column metadata (pg_attribute.attnotnull) to explicit entries in pg_constraint, giving them proper constraint names following the {table}_{column}_not_null pattern. This was part of the broader work to make NOT NULL constraints fully manageable objects (supporting NOT VALID, inheritance, etc.). The upgrade path must handle the case where a user already occupied that namespace in the source database.

This is conceptually similar to commit 3db61db48ef5 which addressed naming conflicts during pg_upgrade for other auto-generated constraint names.

Proposed Solutions

1. Change the Auto-Generated Name Pattern (Proposer: Kirill Reshke)

Make the mechanically-generated name for NOT NULL constraints less obvious/predictable during CREATE TABLE processing. This would affect all new databases, not just upgrades.

Rejected by Álvaro Herrera because:

2. pg_dump --binary-upgrade Name Collision Avoidance (Álvaro Herrera)

Have pg_dump in binary-upgrade mode generate NOT NULL constraint names with full knowledge of all existing constraint names in the target relation, guaranteeing a non-conflicting name.

Tradeoffs:

3. pg_upgrade --check Warning (Álvaro Herrera, preferred)

Have pg_upgrade -c (the pre-upgrade check mode) detect and warn about CHECK constraints whose names would conflict with the auto-generated NOT NULL constraint names. The user would then manually rename the conflicting constraint before upgrading.

Tradeoffs:

Key Technical Context

The ChooseConstraintName() function in PostgreSQL generates constraint names by concatenating table name, column name, and a suffix (like _not_null), then truncating to NAMEDATALEN - 1 (typically 63 bytes). When names exceed this limit, truncation can produce non-obvious results. The pg_upgrade check would need to replicate this logic precisely to avoid false negatives (missing a real collision) or false positives (blocking upgrades unnecessarily).

Assessment

This is a real upgrade-blocking bug that affects any PG17 database where a user has created a CHECK constraint named {table}_{column}_not_null on a column that also has an inline NOT NULL constraint. While likely rare in practice, it's a correctness issue that must be fixed before PG18 release. The pg_upgrade --check approach (option 3) appears most likely to be adopted given its simplicity and alignment with existing pg_upgrade patterns.