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.96) (envelope-from ) id 1wRkn0-002esF-1k for pgsql-hackers@arkaria.postgresql.org; Tue, 26 May 2026 05:57:18 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wRkmy-003G9V-1p for pgsql-hackers@arkaria.postgresql.org; Tue, 26 May 2026 05:57:17 +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.96) (envelope-from ) id 1wRkmy-003G9N-0p for pgsql-hackers@lists.postgresql.org; Tue, 26 May 2026 05:57:17 +0000 Received: from mail-qt1-x82a.google.com ([2607:f8b0:4864:20::82a]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1wRkmx-00000001TF7-0Kyz for pgsql-hackers@lists.postgresql.org; Tue, 26 May 2026 05:57:16 +0000 Received: by mail-qt1-x82a.google.com with SMTP id d75a77b69052e-5164dafcf97so103780521cf.2 for ; Mon, 25 May 2026 22:57:14 -0700 (PDT) ARC-Seal: i=1; a=rsa-sha256; t=1779775033; cv=none; d=google.com; s=arc-20240605; b=bqZa2QBQvErf12QEDfab4CNmVa77ccOgxqR+5D//R/bZrNQQL6SylXJfeviK3j1oda leQoYmX1oSx8L8XPhlPxumk1pQgDBP5NKJRO0FNaRPJF/ONCpnsLB707MMzsShlacKNf xOz2u+WEt3QAkoelpUR5xzummoWr8VoyxLRFLD8/8niAvryL5+M+3eYx++Zh9Jr3l10h Wtk6IO5lklio+ZzTV4e03b2ZhKchprQ5Bhsx7aGuec0i8XdLJ6r0CyFCCL3y7JyXnuYv GsxIyOPwJf2XO0W+cZ39PvIxaw/7OyNi8I4E0ldCHWjQsnjuPJGkKxyawyZXphc9YJvz DNHQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:dkim-signature; bh=IWMkLueJ0739i3Ydj6oUG69d7nA15HlAiiCkAhFIpN4=; fh=YaeB3tcUtDzsK4hS0vsDSrD6xR8+Z/EVZgEiwfJ3LBg=; b=HR/1pKkwuS9R8Sxk15TG/eL9WkArocHTcEGV8HEbGlg2Xae5smh9Sho+fruEP363CH wd2Zzj5kuJwRbH0s25xFMX+xIQeNNgXOqh/PF6MEt1HEUW/mk9PBS4YJVb57tPqip8j2 37pietVPkJFrlbPdQS0R9ZoUwtS4TVfCuOE7DOpUqK66fQUgMbtM9dW9ZRATWGvC7Ny2 V9pr7SUErQ6zd9PR5t8rwMcF+W+Gj/sVGqtZVQZbmrXDVWCYOslBy+JPsCWkFlzN9O6L WDinS3u54ZVeSgI0aQaagVMH1IMQhrQUXJ18UWq0CqI1dwaHp3hrjvcrEbKFKvu/S8ob SfgQ==; darn=lists.postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1779775033; x=1780379833; darn=lists.postgresql.org; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=IWMkLueJ0739i3Ydj6oUG69d7nA15HlAiiCkAhFIpN4=; b=akNnUC+ATThjJ2SPjAOVfcs+TuNTQOLP3PestgJEvlqE+3QQ19p7Jge3Wl9PIfnOxD vlqQqdfzocRZu40Q+/jVoNCGlGisx7n1T0lZn8Ovt2YKl4V5LzDp+rbtoPGhJlaKC6cY kjmerVCcJ2LgL/Yogl3MqLS+IHYYZv4iYmi61EwnCKGQF1WvDXn7gtkMA0GFBYJdf1wr KW+E+uCP0BerfLAjYiXFO0oBl14TQGmJ7kVp3L4XaBoiSjuIXkTFFym81DLm05HDMs+K U/cCvu8DEI30l9A2gZSl9yP0KiHYkSniv0QcVg3u71qYmf/3SSjtzLzkVV0ddH9fCqqL KwQw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1779775033; x=1780379833; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=IWMkLueJ0739i3Ydj6oUG69d7nA15HlAiiCkAhFIpN4=; b=gb7aVADwYldMCbU8wzOH5nTUepJx5VEdDPI/VTj5KZ1yTvGsVPK43R3LCa15ZOD9Hw eVZbMwHSpoWWblrfw8ZknplvsUhVITVyWKumfe4czTaOn91uuvfkSSOIF/pef4336YpC BVej4d8iE31UktFqXCuluSWIs+on5u0mqGG6QXrlEED3AE+CEqnkId56B8jlsJnlwBIz qF1l7Nz4Hd6GNVp4LLnehwYD6S/xkPERIsz0JptHZ7oMoo+Edo+LNzwaTWJiXqCqhrXB NInkEMUVeJl/WTb1BoyAsHIHLCvuhGrBS4ZPADwuENs+pm3Di+WAPJ0Q8ystxszuNQvz qh5Q== X-Forwarded-Encrypted: i=1; AFNElJ9n9KIzCd7PdIxZf83Q+dMhUZfU63PxX1dp7KSYYCxyXgyojvb6RK9+JwxsvJ/IHErV6MkYL6gEvE/VFMwV@lists.postgresql.org X-Gm-Message-State: AOJu0YzqpUyIKZaxdoI8qIjOGAjhDn7Kt8pKWA/+7zLRD/qiOUlhABqU w/VWNn26Ta/57paPCYBCR/7aNX00Ksg/UYu+PdYutc4iUsxtPsDhYyztfrsdLwwSU8RxPm8eC3v PSapssIiRKTGUThu49nbGH10BUmFtePk= X-Gm-Gg: Acq92OFHEBoW7OgZs4dpcyHksO+3Le5SzedZaBrhIjXBS2nKHJ8fXY2sj+pqm8yyO9Z fwqtvzewGv4vz1YVcIx7G3xxb9Gzgk52uEsFE3MzMVoCTivD2wTDiDOu7Ao97CITppO347XIZii lL7fwlg6JQr+OsRnsdcSPA1FjB0FSX30ZNl4TgDgmZGO6SoebPWswenQfB/8stnbVU014KcNNTC G4DXa1pzmKbxyYh2amSydIBEwB/Hb2VUcaUQOrO0W10TBSf7NQsYf7Dv1jjrGr3mAMXzcPkDxQo unNLHfo= X-Received: by 2002:ac8:750a:0:b0:509:2527:d789 with SMTP id d75a77b69052e-516d4428762mr170715531cf.6.1779775033314; Mon, 25 May 2026 22:57:13 -0700 (PDT) MIME-Version: 1.0 References: In-Reply-To: From: Peter Smith Date: Tue, 26 May 2026 15:56:45 +1000 X-Gm-Features: AVHnY4InbyuqzkdW-z-WuHZCp4HkpsKGuENNNCNLhB7LBIq2vr6AWDpSLnKfl3M Message-ID: Subject: Re: Support EXCEPT for TABLES IN SCHEMA publications To: Nisha Moond Cc: shveta malik , Amit Kapila , PostgreSQL Hackers Content-Type: text/plain; charset="UTF-8" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk Hi Nisha. Some review comments for patch v6-0003. ====== Commit Message 1. Extend AlterPublicationExceptTables() with the AP_SetObjects case, which redefine the publication and replaces the entire EXCEPT list. ~ /redefine/redefines/ ====== doc/src/sgml/ref/alter_publication.sgml 2. - -and except_table_object is: - - [ ONLY ] table_name [ * ] IIUC this is just removing some duplicate entry in the synopsis that was not supposed to be there in the first place. ~~~ 3. The EXCEPT clause can be used with - ADD TABLES IN SCHEMA to exclude specific tables from a + ADD TABLES IN SCHEMA and + SET TABLES IN SCHEMA to exclude specific tables from a schema-level publication. EXCEPT is not supported with DROP TABLES IN SCHEMA; instead, dropping a schema from the publication automatically removes all of its associated 3a. This whole description section seems arranged in a confusing way IMO. Anyway, it is not all the fault of the current patch. But I don't think it should be talking about "SET TABLES IN SCHEMA" here because that was all mentioned already in the earlier "third variant" paragraph. ~ 3b. That last sentence all about EXCEPT with DROP TABLES IN SCHEMA seems redundant to me. It is not allowed by the synopsis, so there is no possible confusion about it being supported. Isn't it better to just say nothing? ~~~ EXCEPT 4. Specifies tables to be excluded from a schema-level publication entry. This clause may be used with ADD TABLES IN SCHEMA - and not with DROP TABLES IN SCHEMA. Each named - table must belong to the schema specified in the same - TABLES IN SCHEMA clause. Table names may be - schema-qualified or unqualified; unqualified names are implicitly - qualified with the schema named in the same clause. See - for further details on the + and SET TABLES IN SCHEMA, and not with + DROP TABLES IN SCHEMA. Each named table must belong + to the schema specified in the same TABLES IN SCHEMA + clause. Table names may be schema-qualified or unqualified; unqualified + names are implicitly qualified with the schema named in the same clause. + See for further details on the semantics of EXCEPT. 4a. IMO there is no reason to mention the "DROP TABLES IN SCHEMA" has no EXCEPT. That is not possible just by looking at the synopsis, so there is no ambiguity. Why say anything at all? ~ 4b. This description about EXCEPT is missing talking about FOR ALL TABLES EXCEPT, but IIRC I already reported that in a previous review. ~~~ EXAMPLES 5. + Replace the schema list of sales_publication with + sales, excluding only + sales.drafts (any previously excluded tables for + that schema are replaced, and schemas previously in the publication are + removed): BEFORE (any previously excluded tables for that schema are replaced, and schemas previously in the publication are removed): SUGGESTION Other than sales.drafts, any previously excluded tables for schema sales are no longer excluded. Any schemas previously in the sales_publication are removed. ~~~ 6. + +ALTER PUBLICATION sales_publication SET TABLES IN SCHEMA sales EXCEPT (TABLE sales.drafts); + Don't fully qualify that table in the SQL example. ====== src/backend/commands/publicationcmds.c AlterPublicationExceptTables: 7. - * Nothing to do if no EXCEPT entries. + * Nothing to do if no EXCEPT entries, except in SET: for that it is quite + * possible that the user has removed all exceptions, in which case we + * need to drop any existing ones. Maybe reword this because it is a bit odd to have the word "except" with the keyword "EXCEPT". SUGGESTION Nothing to do if there are no EXCEPT entries, unless handling the SET command, because if the user has removed all exceptions we need to drop any existing ones. ~~~ 8. + { + List *oldexceptrelids = NIL; + List *newexceptrelids = NIL; + List *delrelids = NIL; + List *rels; + List *explicitrelids; + ListCell *lc; + + rels = OpenTableList(exceptrelations); + + /* Collect OIDs of the desired new except list. */ + foreach(lc, rels) There are multiple foreach() loops which can probably be simplified all by using foreach_ptr(...) instead. ~~~ 9. + /* Collect OIDs of the desired new except list. */ + foreach(lc, rels) /except/EXCEPT/ ~~~ 10. + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("table \"%s.%s\" cannot appear in both the table list and the EXCEPT clause", + get_namespace_name(relns), + RelationGetRelationName(pri->relation))); Use the new function to get a fully qualified name and just substitute \"%s\" instead of \"%s.%s\". ~~~ 11. + /* + * Get the current set of except entries. Only FOR ALL TABLES and + * schema-level publications can have except entries; for any other + * publication type oldexceptrelids stays NIL. + * + * Note: we check is_schema_publication() against the current catalog + * state (before AlterPublicationSchemas has run), so if the caller is + * doing SET TABLE t1 to convert a schema publication into a plain + * table publication, is_schema_publication() still returns true here. + * That is intentional: it lets us discover and clean up any stale + * except entries that belong to the old schema definition. + */ + if (GetPublication(pubid)->alltables || is_schema_publication(pubid)) + oldexceptrelids = GetExcludedPublicationTables(pubid, + PUBLICATION_PART_ROOT); + + /* Build a list of old except entries not present in the new list. */ + foreach(lc, oldexceptrelids) + { + Oid oldrelid = lfirst_oid(lc); + + if (!list_member_oid(newexceptrelids, oldrelid)) + delrelids = lappend_oid(delrelids, oldrelid); + } + + /* Drop old except entries not present in the new list. */ + foreach(lc, delrelids) There are multiple comments here mentioning "except entries", but I think they should say "EXCEPT entries". ~~~ PublicationDropSchemas: 12. + /* + * Collect prexcept rows for tables belonging to this schema before + * removing the schema entry. GetExcludedPublicationTables relies on + * is_schema_publication(), which scans pg_publication_namespace; if + * this is the last schema in the publication, performDeletion() below + * would remove that row and make is_schema_publication() return + * false, tripping the assertion. + */ What assertion? ~~~ 13. + foreach(elc, exceptoids) + { + Oid relid = lfirst_oid(elc); + Oid proid; Maybe this can be changed to be a foreach_oid(...) loop. ====== src/bin/psql/tab-complete.in.c 14. + else if (Matches("ALTER", "PUBLICATION", MatchAny, "SET", "TABLES", "IN", "SCHEMA", MatchAny, "EXCEPT", "(", "TABLE", MatchAnyN) && ends_with(prev_wd, ',')) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables); Not sure if this also ought to be 'Query_for_list_of_tables_in_schema', instead of 'Query_for_list_of_tables'. ====== src/test/regress/sql/publication.sql 15. +-- error: EXCEPT is not allowed with DROP I think we should keep all the SET tests together. The DROP test seems to be between other SET tests. ~~~ 16. Perhaps there should be some more tests -- eg. a test case to hit this new error "table \"%s.%s\" cannot appear in both the table list and the EXCEPT clause" ====== src/test/subscription/t/037_except.pl 17. +$node_publisher->safe_psql( + 'postgres', qq( + INSERT INTO sch1.tab_published VALUES (7); + INSERT INTO sch1.tab_excluded VALUES (7); +)); +$node_publisher->wait_for_catchup('sch_sub'); + +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM sch1.tab_excluded"); +is($result, qq(7), + 'ALTER ... SET TABLES IN SCHEMA EXCEPT: newly included table is replicated' +); +$result = + $node_subscriber->safe_psql('postgres', + "SELECT count(*) FROM sch1.tab_published"); +is($result, qq(6), + 'ALTER ... SET TABLES IN SCHEMA EXCEPT: now-excluded table is not replicated' +); Instead of having to keep track of the running totals IMO it might be simpler if you just did "SELECT ... WHERE a = 7;" then the answer would be just 1 or 0 rows. ~~~ 18. +# SET without EXCEPT: clears the except list; both tables are now published. +# tab_published will be re-synced because REFRESH removed its entry when it was +# excluded. Truncate the subscriber copy beforehand so the re-sync produces +# a predictable count: publisher has 7 rows (6 original + INSERT(7)), so the +# subscriber ends up with 7 after re-sync, then 8 after INSERT(8). This is similar to my previous comment. There is lots of tricky commentary here because you are trying to keep track of running totals of rows. I think most of this might not be needed if you changed the checks to do ""SELECT ... WHERE a = 8;", so then you are just expecting row count to be 1 for both tables. ====== Kind Regards, Peter Smith. Fujitsu Australia