Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1ruYTe-0065tK-4r for pgsql-hackers@arkaria.postgresql.org; Wed, 10 Apr 2024 13:59:03 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.94.2) (envelope-from ) id 1ruYTd-0031R1-AE for pgsql-hackers@arkaria.postgresql.org; Wed, 10 Apr 2024 13:59:01 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1ruYTc-0031Qe-6I for pgsql-hackers@lists.postgresql.org; Wed, 10 Apr 2024 13:59:01 +0000 Received: from wfout2-smtp.messagingengine.com ([64.147.123.145]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.94.2) (envelope-from ) id 1ruYTX-000Cn9-Hw for pgsql-hackers@lists.postgresql.org; Wed, 10 Apr 2024 13:58:59 +0000 Received: from compute1.internal (compute1.nyi.internal [10.202.2.41]) by mailfout.west.internal (Postfix) with ESMTP id 254561C000F1; Wed, 10 Apr 2024 09:58:52 -0400 (EDT) Received: from mailfrontend1 ([10.202.2.162]) by compute1.internal (MEProxy); Wed, 10 Apr 2024 09:58:52 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-transfer-encoding :content-type:content-type:date:date:feedback-id:feedback-id :from:from:in-reply-to:in-reply-to:message-id:mime-version :reply-to:subject:subject:to:to:x-me-proxy:x-me-proxy :x-me-sender:x-me-sender:x-sasl-enc; s=fm2; t=1712757531; x= 1712843931; bh=jIfRVXI7IFFs1vrOLFS0QlCsB6fEdxMs7ZcU6Zh8OdE=; b=L gAyV9QFEPPJYuS/gdFj7wGQH4WWxUcaCMhqgXjcUMWNTOK/HKHUR21hHYJ461/v0 cirfhPERqCqnt/nua27C4HKlaNWU9F7Jw+hoB8JliI3mA+PHtRnYNRvCOY+OXwVD ocE3LkLfLCkXX3lG16HAUczmpxarendxM8Ppsoh8iAiYO2Q2XwevDOWFt0HUIhUd YYku+v2n+PZPC9ywJ72iFQ4Wz6JLf7uGA8dVcpN8fYheN27WveNPk2DEoO9LKWfU HsEPQTYUObK0Hqxyg9YGr7i/ujLESY49GhySzPAHX8czZi3irxtiPkd3LVtHktpq eYtOOtO2fraH1p1NqC9Uw== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedvledrudehiedgieekucetufdoteggodetrfdotf fvucfrrhhofhhilhgvmecuhfgrshhtofgrihhlpdfqfgfvpdfurfetoffkrfgpnffqhgen uceurghilhhouhhtmecufedttdenucesvcftvggtihhpihgvnhhtshculddquddttddmne cujfgurhepfffhvfevuffkgggtugfgjgesmhekreertddtjeenucfhrhhomheptehlvhgr rhhoucfjvghrrhgvrhgruceorghlvhhhvghrrhgvsegrlhhvhhdrnhhoqdhiphdrohhrgh eqnecuggftrfgrthhtvghrnhepudelkeekgfdttdettdekgfduvdeiffelteegjeeihfet heekveeftedtuddvhfeunecuffhomhgrihhnpegvnhhtvghrphhrihhsvggusgdrtghomh enucevlhhushhtvghrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpegrlhhv hhgvrhhrvgesrghlvhhhrdhnohdqihhprdhorhhg X-ME-Proxy: Feedback-ID: ia2694551:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA; Wed, 10 Apr 2024 09:58:51 -0400 (EDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=alvh.no-ip.org; s=schmee; t=1712757529; bh=HZ1IwaBbZIXpj/FKU2yGOdJ1UAh373ObOapX8bfSIV0=; h=Date:From:To:Cc:Subject:In-Reply-To:From; b=HetPNSOkQSt6OokglTd869gRi3Y26pCvh7dyngue9MDKe7hJaOKjUuWdL5a9cnXxZ eUTq5Q74GklkJYHvBGzb0rk8DYHy+syBqEusm1IQZ6mdjwpbxOOsjifH6nQ2+F57cQ p9dZkpDMUNXZwCABWaRG/suY5FCOCp49tqFbqosVIFrCE/rBi+gD7+ZEjlMHr9O1Fb jesOAI8lu2rs2m2OlxUqCXrf6jgbbg0Sg5nS/W0XvQGmx8e7ZtFEUJaCq57jJBKavx /DbC/Mv9D7jmGOEjOKjCY8fX2QGCyGO1LnnGPJvjjSksaEbArxvbDqb4qPASAWQAeA exwjN8luskbaA== Received: by schmee.alvh.no-ip.org (Postfix, from userid 1000) id 1AEA5160; Wed, 10 Apr 2024 15:58:49 +0200 (CEST) Date: Wed, 10 Apr 2024 15:58:49 +0200 From: Alvaro Herrera To: Tender Wang Cc: jian he , PostgreSQL Hackers Subject: Re: Can't find not null constraint, but \d+ shows that Message-ID: <202404101358.iet3euazs5zf@alvherre.pgsql> MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="ujvasav73b4rary6" Content-Disposition: inline Content-Transfer-Encoding: 8bit In-Reply-To: <202404101054.c2qhyi62by5n@alvherre.pgsql> List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --ujvasav73b4rary6 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: 8bit It turns out that trying to close all holes that lead to columns marked not-null without a pg_constraint row is not possible within the ALTER TABLE framework, because it can happen outside it also. Consider this CREATE DOMAIN dom1 AS integer; CREATE TABLE notnull_tbl (a dom1, b int, PRIMARY KEY (a, b)); DROP DOMAIN dom1 CASCADE; In this case you'll end up with b having attnotnull=true and no constraint; and no amount of messing with tablecmds.c will fix it. So I propose to instead allow those constraints, and treat them as second-class citizens. We allow dropping them with ALTER TABLE DROP NOT NULL, and we allow to create a backing full-fledged constraint with SET NOT NULL or ADD CONSTRAINT. So here's a partial crude initial patch to do that. One thing missing here is pg_dump support. If you just dump this table, it'll end up with no constraint at all. That's obviously bad, so I propose we have pg_dump add a regular NOT NULL constraint for those, to avoid perpetuating the weird situation further. Another thing I wonder if whether I should use the existing set_attnotnull() instead of adding drop_orphaned_notnull(). Or we could just inline the code in ATExecDropNotNull, since it's small and self-contained. -- Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/ "Postgres is bloatware by design: it was built to house PhD theses." (Joey Hellerstein, SIGMOD annual conference 2002) --ujvasav73b4rary6 Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="0001-Handle-ALTER-.-DROP-NOT-NULL-when-no-pg_constraint-r.patch" From 3a00358847f1792c01e608909dc8a4a0edb8739c Mon Sep 17 00:00:00 2001 From: Alvaro Herrera Date: Wed, 10 Apr 2024 13:02:35 +0200 Subject: [PATCH] Handle ALTER .. DROP NOT NULL when no pg_constraint row exists --- src/backend/commands/tablecmds.c | 67 +++++++++++++++++++---- src/test/regress/expected/constraints.out | 55 +++++++++++++++++++ src/test/regress/sql/constraints.sql | 29 ++++++++++ 3 files changed, 139 insertions(+), 12 deletions(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 865c6331c1..f692001e55 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -448,6 +448,7 @@ static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool r LOCKMODE lockmode); static bool set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, bool recurse, LOCKMODE lockmode); +static void drop_orphaned_notnull(Oid relationOid, AttrNumber attnum); static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel, char *constrname, char *colName, bool recurse, bool recursing, @@ -7678,17 +7679,23 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse, } /* - * Find the constraint that makes this column NOT NULL. + * Find the constraint that makes this column NOT NULL, and drop it if we + * see one. dropconstraint_internal() will do necessary consistency + * checking. If there isn't one, there are two possibilities: either the + * column is marked attnotnull because it's part of the primary key, and + * then we just throw an appropriate error; or it's a leftover marking that + * we can remove. However, before doing the latter, to avoid breaking + * consistency any further, prevent this if the column is part of the + * replica identity. */ conTup = findNotNullConstraint(RelationGetRelid(rel), colName); if (conTup == NULL) { Bitmapset *pkcols; + Bitmapset *ircols; /* - * There's no not-null constraint, so throw an error. If the column - * is in a primary key, we can throw a specific error. Otherwise, - * this is unexpected. + * If the column is in a primary key, throw a specific error message. */ pkcols = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_PRIMARY_KEY); if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, @@ -7697,16 +7704,25 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse, errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("column \"%s\" is in a primary key", colName)); - /* this shouldn't happen */ - elog(ERROR, "could not find not-null constraint on column \"%s\", relation \"%s\"", - colName, RelationGetRelationName(rel)); + /* Also throw an error if the column is in the replica identity */ + ircols = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY); + if (bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, ircols)) + ereport(ERROR, + errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("column \"%s\" is in index used as replica identity", + get_attname(RelationGetRelid(rel), attnum, false))); + + /* Otherwise, just remove the attnotnull marking and do nothing else. */ + drop_orphaned_notnull(RelationGetRelid(rel), attnum); } + else + { + readyRels = NIL; + dropconstraint_internal(rel, conTup, DROP_RESTRICT, recurse, false, + false, &readyRels, lockmode); - readyRels = NIL; - dropconstraint_internal(rel, conTup, DROP_RESTRICT, recurse, false, - false, &readyRels, lockmode); - - heap_freetuple(conTup); + heap_freetuple(conTup); + } InvokeObjectPostAlterHook(RelationRelationId, RelationGetRelid(rel), attnum); @@ -7796,6 +7812,33 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum, bool recurse, return retval; } +/* + * In rare cases, a column can end up with an "orphaned" attnotnull marking + * with no corresponding pg_constraint row. If the user then does ALTER TABLE + * DROP NOT NULL, this takes care of resetting that. + */ +static void +drop_orphaned_notnull(Oid relationOid, AttrNumber attnum) +{ + Relation attr_rel; + HeapTuple tuple; + Form_pg_attribute attForm; + + attr_rel = table_open(AttributeRelationId, RowExclusiveLock); + + tuple = SearchSysCacheCopyAttNum(relationOid, attnum); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for attribute %d of relation %u", + attnum, relationOid); + attForm = (Form_pg_attribute) GETSTRUCT(tuple); + + attForm->attnotnull = false; + CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple); + + table_close(attr_rel, RowExclusiveLock); +} + + /* * ALTER TABLE ALTER COLUMN SET NOT NULL * diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out index d9d8408e86..f0c166dc4f 100644 --- a/src/test/regress/expected/constraints.out +++ b/src/test/regress/expected/constraints.out @@ -866,6 +866,61 @@ select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl foobar | n | {1} (1 row) +DROP TABLE notnull_tbl1; +-- make sure attnotnull is reset correctly when a PK is dropped indirectly +CREATE TABLE notnull_tbl1 (c0 int, c1 int, PRIMARY KEY (c0, c1)); +ALTER TABLE notnull_tbl1 DROP c1; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + c0 | integer | | not null | | plain | | + +ALTER TABLE notnull_tbl1 ALTER c0 DROP NOT NULL; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + c0 | integer | | | | plain | | + +DROP TABLE notnull_tbl1; +-- again +CREATE DOMAIN notnull_dom1 AS INTEGER; +CREATE TABLE notnull_tbl1 (c0 notnull_dom1, c1 int, PRIMARY KEY (c0, c1)); +DROP DOMAIN notnull_dom1 CASCADE; +NOTICE: drop cascades to column c0 of table notnull_tbl1 +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + c1 | integer | | not null | | plain | | + +ALTER TABLE notnull_tbl1 ALTER c1 SET NOT NULL; +\d+ notnull_tbl1 + Table "public.notnull_tbl1" + Column | Type | Collation | Nullable | Default | Storage | Stats target | Description +--------+---------+-----------+----------+---------+---------+--------------+------------- + c1 | integer | | not null | | plain | | +Not-null constraints: + "notnull_tbl1_c1_not_null" NOT NULL "c1" + +DROP TABLE notnull_tbl1; +-- with a REPLICA IDENTITY column +CREATE DOMAIN notnull_dom1 AS INTEGER; +CREATE TABLE notnull_tbl1 (c0 notnull_dom1, c1 int UNIQUE, PRIMARY KEY (c0, c1)); +ALTER TABLE notnull_tbl1 REPLICA IDENTITY USING INDEX notnull_tbl1_c1_key; +DROP DOMAIN notnull_dom1 CASCADE; +NOTICE: drop cascades to column c0 of table notnull_tbl1 +ALTER TABLE notnull_tbl1 ALTER c1 DROP NOT NULL; +ERROR: column "c1" is in index used as replica identity +DROP TABLE notnull_tbl1; +-- with an identity generated column +CREATE DOMAIN notnull_dom1 AS INTEGER; +CREATE TABLE notnull_tbl1 (c0 notnull_dom1, c1 int generated by default as identity, PRIMARY KEY (c0, c1)); +DROP DOMAIN notnull_dom1 CASCADE; +NOTICE: drop cascades to column c0 of table notnull_tbl1 +ALTER TABLE notnull_tbl1 ALTER c1 DROP NOT NULL; +ERROR: column "c1" of relation "notnull_tbl1" is an identity column DROP TABLE notnull_tbl1; -- nope CREATE TABLE notnull_tbl2 (a INTEGER CONSTRAINT blah NOT NULL, b INTEGER CONSTRAINT blah NOT NULL); diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql index 87d685ae39..e946c29f18 100644 --- a/src/test/regress/sql/constraints.sql +++ b/src/test/regress/sql/constraints.sql @@ -599,6 +599,35 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a; select conname, contype, conkey from pg_constraint where conrelid = 'notnull_tbl1'::regclass; DROP TABLE notnull_tbl1; +-- make sure attnotnull is reset correctly when a PK is dropped indirectly +CREATE TABLE notnull_tbl1 (c0 int, c1 int, PRIMARY KEY (c0, c1)); +ALTER TABLE notnull_tbl1 DROP c1; +\d+ notnull_tbl1 +ALTER TABLE notnull_tbl1 ALTER c0 DROP NOT NULL; +\d+ notnull_tbl1 +DROP TABLE notnull_tbl1; +-- again +CREATE DOMAIN notnull_dom1 AS INTEGER; +CREATE TABLE notnull_tbl1 (c0 notnull_dom1, c1 int, PRIMARY KEY (c0, c1)); +DROP DOMAIN notnull_dom1 CASCADE; +\d+ notnull_tbl1 +ALTER TABLE notnull_tbl1 ALTER c1 SET NOT NULL; +\d+ notnull_tbl1 +DROP TABLE notnull_tbl1; +-- with a REPLICA IDENTITY column +CREATE DOMAIN notnull_dom1 AS INTEGER; +CREATE TABLE notnull_tbl1 (c0 notnull_dom1, c1 int UNIQUE, PRIMARY KEY (c0, c1)); +ALTER TABLE notnull_tbl1 REPLICA IDENTITY USING INDEX notnull_tbl1_c1_key; +DROP DOMAIN notnull_dom1 CASCADE; +ALTER TABLE notnull_tbl1 ALTER c1 DROP NOT NULL; +DROP TABLE notnull_tbl1; +-- with an identity generated column +CREATE DOMAIN notnull_dom1 AS INTEGER; +CREATE TABLE notnull_tbl1 (c0 notnull_dom1, c1 int generated by default as identity, PRIMARY KEY (c0, c1)); +DROP DOMAIN notnull_dom1 CASCADE; +ALTER TABLE notnull_tbl1 ALTER c1 DROP NOT NULL; +DROP TABLE notnull_tbl1; + -- nope CREATE TABLE notnull_tbl2 (a INTEGER CONSTRAINT blah NOT NULL, b INTEGER CONSTRAINT blah NOT NULL); -- 2.39.2 --ujvasav73b4rary6--