Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements

From: Akshay Joshi <akshay(dot)joshi(at)enterprisedb(dot)com>
To: Rui Zhao <zhaorui126(at)gmail(dot)com>
Cc: Zsolt Parragi <zsolt(dot)parragi(at)percona(dot)com>, pgsql-hackers(at)lists(dot)postgresql(dot)org
Subject: Re: [PATCH] Add pg_get_table_ddl() to reconstruct CREATE TABLE statements
Date: 2026-07-06 10:57:56
Message-ID: CANxoLDdH-yWa_HT2hW_q5-5BPnfPiB=wmw+3x3BSvTRN3S-Y5Q@mail.gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Thanks for the review, Rui. I’ve addressed all the issues you raised.
The v17 patch is now ready for your review.

On Sat, Jul 4, 2026 at 10:05 PM Rui Zhao <zhaorui126(at)gmail(dot)com> wrote:

> Hi Akshay,
>
> Re-tested v16 on current master -- builds clean and the recent fixes hold
> up
> (self-ref FK after PK, no duplicate partition-child index, cross-schema
> partition child qualified, schema_qualified => true consistent). A few
> issues, in severity order; several are places where pg_dump already does
> the
> right thing.
>
> 1. Clause order: TABLESPACE is emitted before ON COMMIT (ddlutils.c:2042 vs
> 2054), but the grammar is "... OptWith OnCommitOption OptTableSpace" -- ON
> COMMIT must come first, so a temp table with both clauses is
> non-replayable:
>
> CREATE TABLESPACE ts1 LOCATION '/path/to/dir';
> CREATE TEMP TABLE tt (a int) ON COMMIT DROP TABLESPACE ts1;
> SELECT d FROM pg_get_table_ddl('tt'::regclass, owner => false) d;
> -- CREATE TEMPORARY TABLE tt (a integer) TABLESPACE ts1 ON COMMIT
> DROP;
>
> CREATE TEMPORARY TABLE tt (a integer) TABLESPACE ts1 ON COMMIT DROP;
> -- ERROR: syntax error at or near "ON"
>
> Swapping the two blocks so ON COMMIT precedes TABLESPACE fixes it.
>
> 2. Child-default override recurses (missing ONLY).
> emit_child_default_overrides
> emits the inherited-column default without ONLY (ddlutils.c:2118), so SET
> DEFAULT recurses into the table's children and reconstructing one table
> silently rewrites another:
>
> CREATE TABLE dpar (x int);
> CREATE TABLE dch () INHERITS (dpar);
> CREATE TABLE dgc () INHERITS (dch);
> ALTER TABLE ONLY dch ALTER COLUMN x SET DEFAULT 5;
> ALTER TABLE ONLY dgc ALTER COLUMN x SET DEFAULT 10;
>
> SELECT d FROM pg_get_table_ddl('dch'::regclass, owner => false) d;
> -- ALTER TABLE dch ALTER COLUMN x SET DEFAULT 5; -- no ONLY
>
> ALTER TABLE dch ALTER COLUMN x SET DEFAULT 5; -- replay this
> line
> -- => dgc's default is now 5, not 10
>
> pg_dump uses ALTER TABLE ONLY here for exactly this reason.
>
> Partitioned tables hit this especially easily -- any partitioned table
> with a
> column default emits a redundant, ONLY-less SET DEFAULT for every partition
> that merely inherits it:
>
> CREATE TABLE p (id int, amt int DEFAULT 5) PARTITION BY LIST (id);
> CREATE TABLE p_a PARTITION OF p FOR VALUES IN (1);
>
> SELECT d FROM pg_get_table_ddl('p'::regclass, owner => false) d;
> -- CREATE TABLE public.p (id integer, amt integer DEFAULT 5)
> PARTITION BY LIST (id);
> -- CREATE TABLE public.p_a PARTITION OF public.p FOR VALUES IN (1);
> -- ALTER TABLE public.p_a ALTER COLUMN amt SET DEFAULT 5; --
> redundant, no ONLY
>
> p_a already inherits amt's default from the PARTITION OF, so the third
> line is
> redundant; pg_dump instead keeps the default inline on the child and
> attaches
> with ALTER TABLE ONLY ... ATTACH PARTITION. A full-hierarchy replay
> self-corrects (each child's own SET DEFAULT runs last), but a partial
> replay or
> a lone emitted statement does not. The commit message lists "child-local
> DEFAULT overrides on inheritance/partition children" as supported, so this
> is
> in scope. (More generally the patch never emits ONLY anywhere; ADD
> CONSTRAINT,
> where CHECK / NOT NULL also recurse to children, is worth the same audit.)
>
> 3. Inherited-only NOT NULL emitted as local. When a child redeclares an
> inherited column (attislocal) without restating NOT NULL, the constraint is
> inherited-only (conislocal = false); collect_local_not_null skips it, but
> append_column_defs keys off att->attnotnull and emits a bare NOT NULL
> anyway:
>
> CREATE TABLE par (a int NOT NULL);
> CREATE TABLE chld (a int) INHERITS (par); -- 'a' redeclared, no NOT
> NULL
>
> SELECT d FROM pg_get_table_ddl('chld'::regclass, owner => false) d;
> -- CREATE TABLE public.chld (a integer NOT NULL) INHERITS
> (public.par);
>
> On replay the child now owns the constraint (conislocal flips false ->
> true,
> name regenerates par_a_not_null -> chld_a_not_null), so a later
> "ALTER TABLE par ALTER a DROP NOT NULL" cascades to the original child but
> not
> the reconstructed one. pg_dump emits "a integer" with no NOT NULL here,
> suppressing it via notnull_islocal (pg_dump.c ~9916). The docs describe
> this as
> the intended behavior -- "Inherited columns and constraints ... are not
> duplicated on inheritance children or partitions" -- so it reads as a
> documented contract the code doesn't quite meet. The inline CHECK path in
> this
> patch already filters on conislocal; the NOT NULL path could do the same.
>
> 4. Typed-table STORAGE / COMPRESSION not emitted -- intended?
> append_column_defs
> emits per-column STORAGE for ordinary tables, but the typed-table path
> (append_typed_column_overrides) only handles DEFAULT / NOT NULL / CHECK,
> so a
> storage override on a typed table is not reproduced:
>
> CREATE TYPE mytype AS (a int, b text);
> CREATE TABLE typed_t OF mytype;
> ALTER TABLE typed_t ALTER COLUMN b SET STORAGE external;
>
> SELECT d FROM pg_get_table_ddl('typed_t'::regclass, owner => false) d;
> -- CREATE TABLE public.typed_t OF public.mytype; -- STORAGE
> not emitted
>
> The docs scope the typed-table form to overrides for "defaults, NOT NULL,
> and
> CHECK", so this may well be deliberate. But STORAGE is listed in the
> general
> per-column coverage, and pg_dump does emit it (ALTER TABLE ONLY ... ALTER
> COLUMN
> b SET STORAGE EXTERNAL) -- so it seems worth confirming the omission is
> intentional rather than an oversight.
>
> 5. Minor: is_auto (ddlutils.c:1381) and the identity SEQUENCE NAME check
> (1681)
> rebuild the expected auto-name with snprintf, but the backend uses
> makeObjectName(), which truncates name1/name2 to fit NAMEDATALEN and never
> the
> label -- so for long names the "_not_null" / "_seq" suffix is dropped and
> the
> checks misfire, emitting a name a short-named table would omit:
>
> CREATE TABLE aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
> (bbbbbbbbbbbbbbbbbbbb int GENERATED ALWAYS AS IDENTITY);
>
> SELECT d FROM pg_get_table_ddl(
> 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'::regclass,
> owner => false) d;
> -- CREATE TABLE public.aaaa...(50) (bbbb...(20) integer
> -- GENERATED ALWAYS AS IDENTITY (SEQUENCE NAME public.aaaa..._seq)
> -- CONSTRAINT aaaa..._not_null NOT NULL);
>
> The default-omission convention in the commit message lists "the
> auto-generated
> identity sequence name" among the clauses meant to be dropped, which is
> exactly
> what misfires here for long names. It still replays (the names are real),
> so
> this one is cosmetic; comparing against makeObjectName(relname, colname,
> "not_null" / "seq") makes both sides agree.
>
> Everything else looks good.
>
> Thanks,
> Rui
>

Attachment Content-Type Size
v17-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch application/octet-stream 224.5 KB

In response to

Browse pgsql-hackers by date

  From Date Subject
Next Message Ayush Tiwari 2026-07-06 11:03:03 Re: REVOKE's CASCADE protection doesn't work with INHERITed table owners
Previous Message Ashutosh Sharma 2026-07-06 10:57:13 Re: Include sequences in publications created by pg_createsubscriber