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

Поиск
Список
Период
Сортировка
Искать

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

От:
Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Дата:
Hello.

At Thu, 11 Jun 2026 13:18:07 +0530, Akshay Joshi  wrote in 
> Fixed the issue above. The v5 patch is ready for review/testing.

I have not looked at the patch in detail, but I noticed that some
comments in the patch seem to contain non-ASCII characters.

> * * re-emitting it as ALTER TABLE … ADD CONSTRAINT ― the column-emit

I don't think that is recommended in PostgreSQL source comments, so
these should probably be replaced with plain ASCII equivalents.

https://www.postgresql.org/message-id/E1pnhhu-003D6z-Ki%40gemulon.postgresql.org

For reference, I have attached the result of a quick search below.

Regards,

-- 
Kyotaro Horiguchi
NTT Open Source Software Center


Quick search result:

=========
20 matches in 18 lines for "[^[:ascii:]]" in buffer: v5-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch
    565:+ * re-emitting it as ALTER TABLE … ADD CONSTRAINT ― the column-emit
    877:+				 * defaults ― mirroring pg_get_database_ddl's pattern of
    934:+						 * SEQUENCE NAME ― omit when it matches the implicit
   1042:+	 * Table-level CHECK constraints ― emitted inline in the CREATE TABLE
   1055:+ *		applied per-column overrides ― DEFAULT, NOT NULL, and any locally
   1155:+ * pg_get_ruledef, pg_get_statisticsobjdef_string) ― when
   1383:+ *		ALTER TABLE qualname ALTER COLUMN col SET DEFAULT expr ― one per
   1422:+ *		ALTER TABLE qualname ALTER COLUMN col SET (...) ― one per column
   1468:+ *		out-of-line by emit_local_constraints (the ALTER TABLE … ADD
   1500:+ *		ALTER TABLE … ADD CONSTRAINT for each locally-defined constraint
   1617:+ *		ALTER TABLE qualname REPLICA IDENTITY … ― emitted only when the
   1749:+ * (#if 0) ― they will become a single helper call once the standalone
   1830:+	 * Pre-compute "." too ― the always-qualified helpers
   1884:+	 * Triggers and row-level security policies ― disabled until the
   2680:+-- quoting (its prefix starts with ").  Both forms of the prefix ―
   2681:+-- bare-lowercase and quoted ― must be stripped from outer
   3197:+-- quoting (its prefix starts with ").  Both forms of the prefix ―
   3198:+-- bare-lowercase and quoted ― must be stripped from outer
=========


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

От:
Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Дата:
At Mon, 15 Jun 2026 14:25:06 +0530, Akshay Joshi  wrote in 
> Hi Kyotaro and Zsolt,
> 
> I have incorporated the feedback provided by both of you.
> The v6 patch is updated and ready for your review.
> 
> On Fri, Jun 12, 2026 at 6:40 AM Kyotaro Horiguchi 
> wrote:
> >
> > I have not looked at the patch in detail, but I noticed that some
> > comments in the patch seem to contain non-ASCII characters.

Thanks for the update.

That issue appears to be resolved in this version.

Regards,
-- 
Kyotaro Horiguchi
NTT Open Source Software Center

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

От:
Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Дата:
At Mon, 22 Jun 2026 18:10:53 +0530, Akshay Joshi  wrote in 
> The v9 patch is ready for review.

I have not looked closely at the DDL generation logic itself, but I
have a few comments on how pg_get_table_ddl handles its options.

Since pg_get_table_ddl_internal() appears to copy these values into
TableDdlContext almost immediately, I wonder whether TableDdlContext
could be initialized by the caller instead.

Using positional boolean arguments is probably fine when there are
only a handful of options, but with around fifteen of them the current
approach seems somewhat error-prone.

It might also be clearer to initialize the default values first, and
then override only the fields corresponding to explicitly specified
options, rather than folding the default handling and option lookup
into the same expression.

Regards,

-- 
Kyotaro Horiguchi
NTT Open Source Software Center


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

От:
Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Дата:
Hello,

Looking at this, one thing that concerns me is the large amount of
overlap with dumpTableSchema() in pg_dump.

I wonder if it would make sense to separate the SQL generation logic
into frontend/backend-shared code so that it could also be used by
pg_dump. The catalog lookup would naturally remain separate, but
sharing the DDL generation itself would significantly reduce the
duplication.

By the way, a couple of comments use a Unicode RIGHTWARDS ARROW
(U+2192). Please use an ASCII equivalent instead.

Regards,

-- 
Kyotaro Horiguchi
NTT Open Source Software Center


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

От:
Chao Li <li.evan.chao@gmail.com>
Дата:


> On Jun 22, 2026, at 22:11, Akshay Joshi  wrote:
> 
> 
> 
> On Mon, Jun 22, 2026 at 7:30 PM Chao Li  wrote:
> 
> 
> > On Jun 22, 2026, at 20:40, Akshay Joshi  wrote:
> > 
> > You're right, and thanks for spotting this. The existing pattern in pg_proc.dat for variadic-text functions (e.g., jsonb_delete, json_extract_path) uses _text at the variadic position in both proargtypes and proallargtypes, with provariadic => 'text'. That is the convention documented by the sanity check in src/test/regress/sql/opr_sanity.sql.
> > 
> > The same issue applies to pg_get_role_ddl, pg_get_tablespace_ddl (both variants), and pg_get_database_ddl, but that will require a separate patch.
> > 
> 
> Thanks for confirming. Then I will file a patch tomorrow to fix those.
> 
>    I started working on it, but if you want to take the lead, just let me know and I won't send my version over 
> 

The changes to pg_proc.dat have been in my local tree for some time. Today I also fixed the sanity check in opr_sanity so that it can now report this mismatch. Could you please take a look at my patch [1]?

[1] https://www.postgresql.org/message-id/D41A334E-ED9E-42EE-830D-28D4D36E9317%40gmail.com

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/






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

От:
Chao Li <li.evan.chao@gmail.com>
Дата:


> On Jun 22, 2026, at 14:26, Akshay Joshi  wrote:
> 
> Thanks for the review; you're right, `includes_foreign_keys=false` on its own is a half-measure. Re-running with the default to add FKs back collides with the existing CREATE TABLE, UNIQUE indexes, etc.
> 
> I've added an only_foreign_keys option (boolean, default false) as the natural complement of includes_foreign_keys=false. When set to true, the function emits only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY statements and suppresses everything else (CREATE TABLE, owner, indexes, non-FK constraints, rules, statistics, replica identity, RLS toggles). Partition-child recursion still runs so child FKs are reached too. Combining `only_foreign_keys=true` with `includes_foreign_keys=false` is rejected upfront since it would produce no output.
> 
> The documentation paragraph for `includes_foreign_keys` now directs users to `only_foreign_keys` as the intended second pass. Regression coverage adds three cases: the FK-only emission for your cons example, the zero-row result for a table without FKs, and the error path. 
> 
> The v8 patch is ready for review.
> 
> On Sat, Jun 20, 2026 at 1:15 AM Zsolt Parragi  wrote:
> The previous features all look good to me, I only have one question
> for the new flag.
> 
> > Calling
> > pg_get_table_ddl(t, 'includes_foreign_keys', 'false') now emits everything
> > except FOREIGN KEY constraints. This covers the multi-tenant clone
> > workflow: create tables first without cross-table references, then re-run
> > with the default to add the constraints once all targets exist.
> 
> I think this feature needs a bit more documentation, an
> "only_foreign_keys" flag, or both.
> 
> CREATE TABLE refd (id int PRIMARY KEY);
> CREATE TABLE cons (a int CHECK(a>0), b int UNIQUE, c int REFERENCES refd(id));
> 
> -- pass 1: running without foreign keys
> SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','false');
> -- execute everything
> 
> -- loading data
> 
> -- pass 2: running with everything
> SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','true');
> -- ERROR: relation "cons" already exists (and the unique constraint
> also collides)
> 
> I could do a "grep FOREIGN KEY" before executing (unless it's a tricky
> schema where that phrase appears elsewhere), or since psql continues
> on error, it will simply work if I accept a significant error noise,
> but then the documentation should be clear about this limitation.
> Following the documented approach and getting a bunch of unexpected
> errors could be confusing for users.
> 
> 
> 

I have a comment, or maybe a question:
```
+{ oid => '8215', descr => 'get DDL to recreate a table',
+  proname => 'pg_get_table_ddl', prorows => '50', provariadic => 'text',
+  proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r',
+  pronargdefaults => '1', prorettype => 'text',
+  proargtypes => 'regclass text', proallargtypes => '{regclass,text}',
+  proargmodes => '{i,v}', proargdefaults => '{NULL}',
+  prosrc => 'pg_get_table_ddl' },
```

Since provariadic is text, I wonder if proallargtypes should be {regclass,_text}, with _text meaning an array of text.

I’m asking because I have had this suspicion for some time. I saw a few other procs using the same pattern, for example:
```
{ oid => '6501', descr => 'get DDL to recreate a role',
  proname => 'pg_get_role_ddl', prorows => '10', provariadic => 'text',
  proisstrict => 'f', proretset => 't', provolatile => 's',
  pronargdefaults => '1', prorettype => 'text', proargtypes => 'regrole text',
  proallargtypes => '{regrole,text}', proargmodes => '{i,v}',
  proargdefaults => '{NULL}', prosrc => 'pg_get_role_ddl' },
```

But for jsonb_delete etc procs, _text is used:
```
{ oid => '3343',
  proname => 'jsonb_delete', provariadic => 'text', prorettype => 'jsonb',
  proargtypes => 'jsonb _text', proallargtypes => '{jsonb,_text}',
  proargmodes => '{i,v}', proargnames => '{from_json,path_elems}',
  prosrc => 'jsonb_delete_array' },
```

So I wonder whether “text” rather than “_text" is intentionally used in proallargtypes, or if this was just never noticed.

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/






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

От:
Marcos Pegoraro <marcos@f10.com.br>
Дата:
Em qua., 23 de set. de 2026 às 10:01, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
The v26 patch is ready for review/test.

Did you see that previous commited patches related to this one were reverted ?
See this discussion "pg_get_*_ddl() needs a redesign " 


regards
Marcos

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

От:
Marcos Pegoraro <marcos@f10.com.br>
Дата:
Em qui., 9 de jul. de 2026 às 05:54, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
Fixed the documentation. v19 patch is now ready for review.

I didn't understand when a word is considered reserved, so that it has double quotes.

postgres=# create table int(int4 integer primary key, integer integer, interval interval, boolean boolean, insert boolean);
CREATE TABLE
postgres=# select pg_get_table_ddl('int'::regclass, schema_qualified=>false, owner=>true, only_kinds => ARRAY['primary_key','check','table']);
                                                    pg_get_table_ddl
------------------------------------------------------------------------------------------------------------------------
 CREATE TABLE "int" (int4 integer NOT NULL, "integer" integer, "interval" interval, "boolean" boolean, insert boolean);
 ALTER TABLE "int" OWNER TO postgres;
 ALTER TABLE "int" ADD CONSTRAINT int_pkey PRIMARY KEY (int4);
(3 rows)

And as for the owner, I think that causes a bit of confusion when used with kinds parameters, doesn't it ?
It emits other ALTER TABLE except the OWNER TO.

postgres=# create table self(id integer primary key, self_id integer constraint self_self references self(id));
CREATE TABLE
postgres=# select pg_get_table_ddl('self'::regclass, owner=>true, except_kinds => ARRAY['table']);
                                         pg_get_table_ddl
--------------------------------------------------------------------------------------------------
 ALTER TABLE public.self ADD CONSTRAINT self_pkey PRIMARY KEY (id);
 ALTER TABLE public.self ADD CONSTRAINT self_self FOREIGN KEY (self_id) REFERENCES public.self(id);
(2 rows)
regards
Marcos

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

От:
Marcos Pegoraro <marcos@f10.com.br>
Дата:
Em qua., 8 de jul. de 2026 às 08:56, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
The v18 patch is now ready for your review.

On SGML part, schema_qualified is a param which comes before only_kinds.
But the <para> explaining schema_qualified is the latest to be explained, why ?
Shouldn't it be placed right after tablespace ?

regards
Marcos

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

От:
Marcos Pegoraro <marcos@f10.com.br>
Дата:
Em seg., 15 de jun. de 2026 às 04:52, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
I don't think per-contype flags are the right shape, though. The existing toggles group by catalog (indexes, constraints, rules, ...); splitting constraints into PK/FK/CHECK/UNIQUE/EXCLUDE/NOT NULL adds six flags on a second axis, and the function already carries nine. Only FKs have the cross-table dependency-ordering problem; the rest only reference the same table, so splitting them unlocks nothing new.

Ok, I understand your point. Initially, I saw the usefulness of this function for cloning a schema, something very common in a multi-tenant model. But creating the foreign keys along with the create table makes that unfeasible.

Options are variadic, so you could split your emit_local_constraints into 
+emit_local_foreign_keys_constraints(TableDdlContext * ctx) 
+ if (!(ctx->include_constraints || ctx->include_foreign_keys)) then
+   return

+emit_local_primary_keys_constraints(TableDdlContext * ctx) 
+ if (!(ctx->include_constraints || ctx->include_primary_keys)) then
+   return

pg_get_table_ddl('x','includes_constraints','true') -- would print all constraints
pg_get_table_ddl('x','include_primary_keys','true') -- would print only primary key constraints

regards
Marcos

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

От:
Marcos Pegoraro <marcos@f10.com.br>
Дата:
Em qui., 11 de jun. de 2026 às 04:48, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
 Fixed the issue above. The v5 patch is ready for review/testing.

One thing I noticed, though I'm not sure if it's the point here, is that it's not possible to extract only the foreign keys or only the triggers from the table. So if we want to extract the objects independently by type, we would need to have all the return types as optional, and we could have more granularity in the return types.

Just like you have...
if (!ctx->include_indexes)

You could have too
+ if (!ctx->include_create_table)
+ if (!ctx->include_foreign_keys)
+ if (!ctx->include_primary_keys)

Because only in this way can we more or less execute the dump behavior here, which is to create all the tables beforehand, then primary keys, then foreign keys, then triggers. 

I repeat, sorry if this is not the function's intended purpose.

regards
Marcos

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

От:
Marcos Pegoraro <marcos@f10.com.br>
Дата:
Em qua., 1 de jul. de 2026 às 11:12, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
pretty, owner, tablespace, and schema_qualified are plain boolean parameters with DEFAULT values
Filtering parameters: only_kinds / except_kinds as text[] 
If all parameters are optional, and all parameters are boolean, perhaps you could also make pretty, owner, tablespace, and schema_qualified as optional parts of only_kinds and except_kinds.

Therefore, we could call these two ways and the result would be the same.
pg_get_table_ddl('idxd'::regclass, owner => false, tablespace => false, except_kinds => '{primary_key}');
pg_get_table_ddl('idxd'::regclass, except_kinds => '{primary_key,tablespace,owner}');

Obviously this way you have to know if owner param is false or it exists on except_kinds. 
What do you think ?

regards
Marcos

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

От:
Marcos Pegoraro <marcos@f10.com.br>
Дата:
Em seg., 22 de jun. de 2026 às 03:27, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
The documentation paragraph for `includes_foreign_keys` now directs users to `only_foreign_keys` as the intended second pass. Regression coverage adds three cases: the FK-only emission for your cons example, the zero-row result for a table without FKs, and the error path. 

I still think this model of only having options for foreign keys is incomplete, maybe wrong.
Imagine then cloning a schema from a publication server to be executed on a subscription server. So I don't want any other constraints besides the primary key, for example. The way you implemented it is not possible.

Furthermore having only_foreign_keys and includes_foreign_keys seems confuse. 

regards
Marcos

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

От:
Rui Zhao <zhaorui126@gmail.com>
Дата:
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


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

От:
Rui Zhao <zhaorui126@gmail.com>
Дата:
Hi Akshay,

Re-tested v17 on current master (73dfe79fd6) -- all five issues are fixed,
and make check passes here.

I then ran a round-trip check against pg_dump: for each table, replay the
pg_get_table_ddl() output into a clone of the database and diff
"pg_dump -t" of both sides (both sides being pg_dump output, any surviving
diff is semantic, not formatting). Corpus: the create_sql scenarios from
002_pg_dump.pl, then the whole regression database. 483 of 560 tables
round-trip identically; setting aside the documented no-output kinds
(trigger/policy), the rest reduce to:

1) serial columns produce non-replayable DDL -- the output references the
sequence but never creates it:

    CREATE TABLE ser (id serial, v text);

    SELECT d FROM pg_get_table_ddl('ser'::regclass, owner => false) d;
    --  CREATE TABLE public.ser (id integer DEFAULT
nextval('public.ser_id_seq'::regclass) NOT NULL, v text);

    CREATE TABLE public.ser (id integer DEFAULT
nextval('public.ser_id_seq'::regclass) NOT NULL, v text);
    --  ERROR:  relation "public.ser_id_seq" does not exist    (on an
empty database)

pg_dump emits CREATE SEQUENCE + OWNED BY + SET DEFAULT. This accounts for
14 regression failures, including whole partition trees.

2) Partition/inheritance children that diverged from their parent don't
survive the PARTITION OF / INHERITS rebuild. Three variants:

  - a dropped default silently comes back:

        CREATE TABLE dp (a int DEFAULT 99) PARTITION BY LIST (a);
        CREATE TABLE dp1 PARTITION OF dp FOR VALUES IN (1);
        ALTER TABLE ONLY dp1 ALTER COLUMN a DROP DEFAULT;

        SELECT d FROM pg_get_table_ddl('dp'::regclass, owner => false) d;
        --  CREATE TABLE public.dp (a integer DEFAULT 99) PARTITION BY LIST (a);
        --  CREATE TABLE public.dp1 PARTITION OF public.dp FOR VALUES IN (1);

        SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef
          WHERE adrelid = 'dp1'::regclass;
        --  source:   (0 rows)
        --  replayed: 99    => INSERTs into dp1 now get 99, not NULL

  - a child's own NOT NULL constraint name is lost:

        CREATE TABLE np (a int NOT NULL) PARTITION BY LIST (a);
        CREATE TABLE np1 (a int CONSTRAINT np1_nn NOT NULL);
        ALTER TABLE np ATTACH PARTITION np1 FOR VALUES IN (1);

        SELECT d FROM pg_get_table_ddl('np'::regclass, owner => false) d;
        --  CREATE TABLE public.np (a integer NOT NULL) PARTITION BY LIST (a);
        --  CREATE TABLE public.np1 PARTITION OF public.np FOR VALUES IN (1);

        SELECT conname FROM pg_constraint
          WHERE conrelid = 'np1'::regclass AND contype = 'n';
        --  source:   np1_nn
        --  replayed: np_a_not_null

    and when the column itself is inherited, the emitted constraint
    errors instead of merging:

        CREATE TABLE p5 (a int);
        CREATE TABLE c5 () INHERITS (p5);
        ALTER TABLE c5 ADD CONSTRAINT c5_nn NOT NULL a;
        ALTER TABLE p5 ADD CONSTRAINT p5_nn NOT NULL a;

        SELECT d FROM pg_get_table_ddl('c5'::regclass, owner => false) d;
        --  CREATE TABLE public.c5 () INHERITS (public.p5);
        --  ALTER TABLE public.c5 ADD CONSTRAINT c5_nn NOT NULL a;

        ALTER TABLE public.c5 ADD CONSTRAINT c5_nn NOT NULL a;    --
replay, p5 recreated first
        --  ERROR:  cannot create not-null constraint "c5_nn" on
column "a" of table "c5"
        --  DETAIL:  A not-null constraint named "p5_nn" already
exists for this column.

  - a partition attached from a table with different column order is
    rebuilt in the parent's order:

        CREATE TABLE p (a int, b int, c int) PARTITION BY LIST (a);
        CREATE TABLE c1 (c int, b int, a int);
        ALTER TABLE p ATTACH PARTITION c1 FOR VALUES IN (1);

        SELECT d FROM pg_get_table_ddl('p'::regclass, owner => false) d;
        --  CREATE TABLE public.p (a integer, b integer, c integer)
PARTITION BY LIST (a);
        --  CREATE TABLE public.c1 PARTITION OF public.p FOR VALUES IN (1);
        --  => replayed c1 columns are a, b, c (source: c, b, a), so
        --     SELECT * / COPY / positional INSERT all shift

pg_dump handles all three: it emits the named constraint inline in the
child's CREATE body (the merge with the inherited constraint at CREATE
time keeps the local name, and attislocal / conislocal / coninhcount all
survive), and falls back to standalone CREATE + ATTACH PARTITION for
shapes a PARTITION OF / INHERITS clause can't express. The first two
variants have lightweight fixes that keep the PARTITION OF / INHERITS
shape --

    CREATE TABLE c5 (CONSTRAINT c5_nn NOT NULL a) INHERITS (p5);
    CREATE TABLE np1 PARTITION OF np (CONSTRAINT np1_nn NOT NULL a)
FOR VALUES IN (1);

plus a counter-statement for the divergent-default case (ALTER TABLE
ONLY dp1 ALTER COLUMN a DROP DEFAULT). Only the reordered-column case
really needs the standalone shape. The commit message lists child-local
DEFAULT overrides and named NOT NULL constraints as supported, so I'm
treating these as bugs rather than scope cuts.

3) emit_typed_column_storage() (ddlutils.c:2331, 2342) emits ALTER TABLE
without ONLY; SET STORAGE / SET COMPRESSION recurse:

    CREATE TYPE mytype AS (a int, b text);
    CREATE TABLE typed_t OF mytype;
    ALTER TABLE typed_t ALTER COLUMN b SET STORAGE external;
    CREATE TABLE tchild () INHERITS (typed_t);
    ALTER TABLE ONLY tchild ALTER COLUMN b SET STORAGE main;

    ALTER TABLE public.typed_t ALTER COLUMN b SET STORAGE EXTERNAL;
-- replay v17's output
    --  => tchild's attstorage flips m -> e

pg_dump uses ALTER TABLE ONLY here. (The other emitted ALTER TABLEs are
fine: OWNER / REPLICA IDENTITY / RLS / SET (options) don't recurse, and
ADD CONSTRAINT must stay ONLY-less since the dump relies on its recursion
to rebuild the children's suppressed inherited copies.)

4) Some per-table state pg_dump preserves is missing. Index statistics
targets:

    CREATE TABLE ist (c1 int);
    CREATE INDEX ist_idx ON ist ((c1 + 1));
    ALTER INDEX ist_idx ALTER COLUMN 1 SET STATISTICS 400;

    SELECT d FROM pg_get_table_ddl('ist'::regclass, owner => false) d;
    --  CREATE TABLE public.ist (c1 integer);
    --  CREATE INDEX ist_idx ON public.ist USING btree (((c1 + 1)));
 -- no SET STATISTICS

Same story for ALTER TABLE ... CLUSTER ON (the index comes back without
the indisclustered marker) and for ALTER TABLE ... DISABLE RULE (the rule
is emitted but comes back enabled, which changes behavior -- and rule is
a covered kind).

5) emit_indexes doesn't check indisvalid, so an invalid index is emitted
as a normal one:

    CREATE TABLE inv (x int);
    INSERT INTO inv VALUES (1), (1);
    CREATE UNIQUE INDEX CONCURRENTLY inv_uidx ON inv (x);
    --  ERROR:  could not create unique index "inv_uidx"    (leaves
indisvalid = false)

    SELECT d FROM pg_get_table_ddl('inv'::regclass, owner => false) d;
    --  CREATE TABLE public.inv (x integer);
    --  CREATE UNIQUE INDEX inv_uidx ON public.inv USING btree (x);

pg_dump skips those (getIndexes: "i.indisvalid OR t2.relkind = 'p'").

6) Minor: COMMENT ON and GRANT/REVOKE are not emitted. If that's
intentional -- like trigger/policy -- worth saying so in the doc.

Thanks,
Rui


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

От:
Rui Zhao <zhaorui126@gmail.com>
Дата:
Hi Akshay,

Sorry for the slow turnaround -- the World Cup got the better of my evenings.

A) Round-trip against pg_dump. I re-ran it on v22 (master 8767a10cb8): for each
of the 563 top-level tables in the regression database I ran pg_get_table_ddl,
dropped and recreated that table from its output in a copy of the database, and
diffed it against pg_dump. 504 of 563 round-trip clean, and the rate has climbed
with each round -- 483/560 at v17, 494/562 at v19, 501/562 at v21,
504/563 now --
with installcheck passing throughout.

For the record, here is what the remaining 59 are:

  - 31 are objects pg_get_table_ddl doesn't emit by design (triggers, grants,
    policies, comments, owned sequences). The pg_dump comparison flags them, but
    that is expected.

  - 17 are artifacts of testing (I replay into a copy with the table dropped),
    not defects: 14 rely on an owned sequence or dependent function
that goes with
    the drop and isn't part of the table's own DDL; 3 pick up a "1" suffix on a
    NOT NULL constraint whose name a sibling table still holds. The
emitted DDL is
    correct.

  - 10 are column reordering on partition or inheritance children -- the
    reordered-column limitation you deferred, plus its
inheritance-child variant.

  - 1 is real but cosmetic: a partition child's named EXCLUDE constraint isn't
    preserved -- it replays fine, only the child's name is regenerated. Just the
    parent constraint is emitted:

      CREATE TABLE ep (a int4range, b int4range) PARTITION BY LIST (a);
      CREATE TABLE ep1 (a int4range, b int4range,
                        CONSTRAINT ep1_excl EXCLUDE USING gist (a WITH
=, b WITH &&));
      ALTER TABLE ep ADD CONSTRAINT ep_excl EXCLUDE USING gist (a WITH
=, b WITH &&);
      ALTER TABLE ep ATTACH PARTITION ep1 FOR VALUES IN ('[0,1)');

      SELECT d FROM pg_get_table_ddl('ep', owner => false) d;
      --  CREATE TABLE public.ep (a int4range, b int4range) PARTITION
BY LIST (a);
      --  CREATE TABLE public.ep1 PARTITION OF public.ep FOR VALUES IN
('[0,1)');
      --  ALTER TABLE public.ep ADD CONSTRAINT ep_excl EXCLUDE USING
gist (a WITH =, b WITH &&);

    ep1's own constraint (ep1_excl) is never emitted, so on replay the parent's
    ADD CONSTRAINT recreates it auto-named ep1_a_b_excl instead of ep1_excl.

So the substantive round-trip gaps come down to the reordered columns (your
deferred limitation) and that one EXCLUDE naming case; none breaks replay. (1b,
the conislocal locality case from my v19 pass -- a hand-built one, not in the
corpus above -- is the same family and still open, as you'd noted.)

B) only_kinds/except_kinds filtering. The cross-kind dependency check is only
half there. When a statement leans on an object another kind emits -- a
constraint's backing index -- and you filter that kind out, REPLICA IDENTITY
USING INDEX catches it and errors cleanly ('requires kind "primary_key" to be
emitted'); CLUSTER ON and a self-referential FK don't, and dangle:

    CREATE TABLE b (x int);
    ALTER TABLE b ADD CONSTRAINT b_pk PRIMARY KEY (x);
    ALTER TABLE b CLUSTER ON b_pk;

    SELECT d FROM pg_get_table_ddl('b', owner => false,
                                   except_kinds => ARRAY['primary_key']) d;
    --  CREATE TABLE public.b (x integer NOT NULL);
    --  ALTER TABLE public.b CLUSTER ON b_pk;
    -- replay: ERROR:  index "b_pk" for table "b" does not exist

    CREATE TABLE t (id int PRIMARY KEY, parent_id int REFERENCES t(id));

    SELECT d FROM pg_get_table_ddl('t', owner => false,
                                   except_kinds => ARRAY['primary_key']) d;
    --  CREATE TABLE public.t (id integer NOT NULL, parent_id integer);
    --  ALTER TABLE public.t ADD CONSTRAINT t_parent_id_fkey
    --      FOREIGN KEY (parent_id) REFERENCES public.t(id);
    -- replay: ERROR:  there is no unique constraint matching given keys for
    --                 referenced table "t"

except_kinds => ARRAY['unique'] does the same. Both want the guard REPLICA
IDENTITY already has -- the regression test even covers it for all three index
kinds; CLUSTER ON and the FK case just aren't wired to it. A plain-index
CLUSTER ON and a cross-table FK are fine as they are -- the first drops with the
index kind, the second gets its key from the referenced table's own output.

C) One small code note. emit_partition_children() recurses into
pg_get_table_ddl_internal() once per partition-tree level:

    children = find_inheritance_children(ctx->relid, AccessShareLock);
    foreach(lc, children)
    {
        ...
        CHECK_FOR_INTERRUPTS();
        ...
        childstmts = pg_get_table_ddl_internal(&childctx);

For user-triggerable recursion like this the convention is to put a
check_stack_depth() next to that CHECK_FOR_INTERRUPTS(). It does fail cleanly
today -- I threw a 12000-level partition tree at it and got the usual "stack
depth limit exceeded" -- but only because some callee happens to check; the
recursion itself never does.

Regards,
Rui


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

От:
Rui Zhao <zhaorui126@gmail.com>
Дата:
Hi Akshay,

Thanks for v19. I re-ran the pg_dump round-trip check (the 002_pg_dump.pl
scenarios plus the whole regression database) on current master (2e6578292a).
The five fixes hold up: SET STORAGE/COMPRESSION now use ONLY, invalid indexes
are skipped, SET STATISTICS / CLUSTER ON / DISABLE RULE are emitted, and the
child-default and named-NOT NULL cases I sent all round-trip now. 494 of 562
tables match pg_dump exactly (was 483/560 on v17). Three things came out of
the run below; the third is a side effect of that invalid-index skip.

1) The named-NOT NULL handling for inheritance/partition children still isn't
complete. It works when the child constraint has a user-defined name, but the
corpus turns up two ways it still goes wrong.

1a) For an auto-named inherited NOT NULL it emits an out-of-line ADD CONSTRAINT
that collides with the one INHERITS/PARTITION OF already propagates, giving
non-replayable DDL:

    CREATE TABLE nt (a int PRIMARY KEY);
    CREATE TABLE ntc (PRIMARY KEY (a) DEFERRABLE) INHERITS (nt);

    SELECT d FROM pg_get_table_ddl('ntc'::regclass, owner => false) d;
    --  CREATE TABLE public.ntc () INHERITS (public.nt);
    --  ALTER TABLE public.ntc ADD CONSTRAINT ntc_a_not_null NOT NULL a;
    --  ALTER TABLE public.ntc ADD CONSTRAINT ntc_pkey PRIMARY KEY (a)
DEFERRABLE;

    -- replaying that, with nt already restored first so INHERITS propagates
    -- its nt_a_not_null down to ntc:
    ERROR:  cannot create not-null constraint "ntc_a_not_null" on
column "a" of table "ntc"
    DETAIL:  A not-null constraint named "nt_a_not_null" already
exists for this column.

(notnull_tbl4_cld2 in the regression suite fails the same way.)

1b) Even where it does replay, a partition attached from a standalone table
keeps its name but comes back with the wrong constraint locality, because the
constraint is folded into PARTITION OF instead of a standalone CREATE + ATTACH:

    CREATE TABLE mp (a int NOT NULL) PARTITION BY LIST (a);
    CREATE TABLE mp1 (a int CONSTRAINT mp1_nn NOT NULL);
    ALTER TABLE mp ATTACH PARTITION mp1 FOR VALUES IN (1);
    -- source mp1.mp1_nn: conislocal = false (the ATTACH merged it
into the parent's)

    SELECT d FROM pg_get_table_ddl('mp'::regclass, owner => false) d;
    --  CREATE TABLE public.mp (a integer NOT NULL) PARTITION BY LIST (a);
    --  CREATE TABLE public.mp1 PARTITION OF public.mp (CONSTRAINT
mp1_nn NOT NULL a) FOR VALUES IN (1);

Declaring the constraint inline makes it local, so mp1.mp1_nn comes back with
conislocal = true, and the behavior diverges once the parent's NOT NULL is
dropped:

    ALTER TABLE mp ALTER a DROP NOT NULL;
    -- source: mp1.a is no longer NOT NULL; reconstructed: mp1.a stays NOT NULL

pg_dump reconstructs both faithfully. For reference, this is where it puts
each constraint:

  - In the CREATE TABLE body: NOT NULL, and validated CHECK.
  - As a separate ALTER TABLE ... ADD CONSTRAINT: PRIMARY KEY, UNIQUE, EXCLUDE,
    FOREIGN KEY, and NOT VALID CHECK.

The split is the same on plain tables, inheritance children and partitions;
only the table shape differs:

  - A plain-inheritance child keeps CREATE ... INHERITS and emits only its
    locally-owned constraints (purely-inherited NOT NULL/CHECK are left to
    INHERITS); the NOT NULL goes in the body unnamed so it auto-names, e.g.
    for the 1a case: CREATE TABLE ntc (NOT NULL a) INHERITS (nt); ...

  - A partition is never dumped with CREATE ... PARTITION OF; it is a standalone
    CREATE TABLE plus ALTER TABLE ONLY parent ATTACH PARTITION, so the child's
    column order, constraint names and conislocal come from its own definition
    plus the merge ATTACH performs.

That partition path is the same standalone+ATTACH that the reordered-column
case you deferred needs, so 1b likely folds into that follow-up. I'll
leave the exact approach to you.

2) A NOT VALID CHECK constraint loses its NOT VALID flag on round-trip. The
constraint is emitted inline in CREATE TABLE, where it is validated against the
(empty) table, so the reconstructed constraint is marked validated:

    CREATE TABLE nv (a int);
    ALTER TABLE nv ADD CONSTRAINT chk CHECK (a < 50) NOT VALID;

    SELECT d FROM pg_get_table_ddl('nv'::regclass, owner => false) d;
    --  CREATE TABLE public.nv (a integer, CONSTRAINT chk CHECK ((a <
50)) NOT VALID);

    -- source nv.chk:       convalidated = false
    -- reconstructed nv.chk: convalidated = true

pg_dump emits NOT VALID constraints as a separate ALTER TABLE ... ADD
CONSTRAINT ... NOT VALID for this reason.

3) The invalid-index skip added for the earlier finding also drops legitimate
partitioned indexes. emit_indexes now has:

    /* Skip invalid indexes; they may be left over from a failed
CREATE INDEX CONCURRENTLY. */
    if (!idxform->indisvalid)
    {
        ReleaseSysCache(indTup);
        continue;
    }

but a partitioned index on the parent is normally indisvalid = false (e.g. one
created with ON ONLY, or before all child indexes are attached), so the whole
index is lost:

    CREATE TABLE p (a int) PARTITION BY RANGE (a);
    CREATE TABLE p1 PARTITION OF p FOR VALUES FROM (0) TO (100);
    CREATE INDEX ON ONLY p (a);          -- p_a_idx.indisvalid = false

    SELECT d FROM pg_get_table_ddl('p'::regclass, owner => false) d;
    --  CREATE TABLE public.p (a integer) PARTITION BY RANGE (a);
    --  CREATE TABLE public.p1 PARTITION OF public.p FOR VALUES FROM
(0) TO (100);

No CREATE INDEX at all; pg_dump keeps it as "CREATE INDEX p_a_idx ON ONLY
public.p USING btree (a)". That's the pg_dump condition I quoted earlier --
"i.indisvalid OR t2.relkind = 'p'"; the relkind = 'p' half needs to stay so
partitioned indexes aren't filtered out along with the failed-CIC ones.

Thanks,
Rui


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

От:
Rui Zhao <zhaorui126@gmail.com>
Дата:
Hi Akshay,

v25 applies to master (798bdcae89), builds warning-free, make check
244/244.  The three points from my last mail are all in: the doc note now
says the child constraint names are not preserved in either direction, the
schema_qualified => false script replays under search_path = sc with sb.yc
attached to sc.yp and sa.yp untouched, and the cross-schema ATTACH line is
in the expected output.

Round-trip against pg_dump, re-run on v25 and, alongside it, on v22 so
the two are like for like: 508 of the 563 top-level tables in the
regression database come back identical on both, and the same 55 tables
fall outside for the same reasons.  30 are objects the function doesn't
emit by design (triggers, grants, policies, comments, an owned sequence),
14 are the copy's DROP ... CASCADE taking a serial's sequence or a
function with it, and the remaining 11 are the partition child name and
column-order cases already deferred, three name collisions in the copy,
and one I hadn't looked at before:

1. An unlogged table's identity sequence that was SET LOGGED comes back
unlogged:

    CREATE UNLOGGED TABLE idu (a int GENERATED ALWAYS AS IDENTITY);
    ALTER SEQUENCE idu_a_seq SET LOGGED;
    SELECT d FROM pg_get_table_ddl('idu', owner => false) d;
    --  CREATE UNLOGGED TABLE public.idu (a integer GENERATED ALWAYS AS
    --      IDENTITY NOT NULL);

    -- replayed: idu_a_seq has relpersistence = u; the source has p

emit_identity_sequence_alterations() emits ALTER SEQUENCE ... SET UNLOGGED
when the sequence is unlogged (ddlutils.c:2990).  The sequence starts out
with the table's persistence, so the thing to test against is the table,
not LOGGED: on an unlogged table the sequence is unlogged by default and
the change worth emitting is the opposite one.  This is the mirror of
Zsolt's 11b, and the regression database has it (identity_dump_unlogged).
Attached 0001 compares against the table and emits SET LOGGED or SET
UNLOGGED accordingly, with the mirror test next to 11b.  With it the
round-trip is 509 of 563 and make check stays 244/244.

Two notes on the changes that aren't from my patches.

2. Switching the inline CHECKs to pg_get_constraintdef_body() also drops
PRETTYFLAG_INDENT, so a CHECK with a CASE no longer brings the deparser's
newlines into an otherwise single-line CREATE TABLE.  Good, and the
inline path now matches the table-level one; just noting it since the
change is visible and nothing in the test exercises it.

3. The ReleaseSysCache() calls added in front of ereport(ERROR) and
aclcheck_error() aren't needed.  The pin is registered with the resource
owner when the tuple is fetched, and the abort path releases it
(ResOwnerReleaseCatCache), which is what every other ereport(ERROR) with
a syscache tuple in hand relies on.  There are seven of these in
ddlutils.c now; harmless, just dead code.

From my side this is the last round.  With 0001 in, every table in the
regression database is accounted for, and v22 through v25 haven't moved
on that corpus in either direction.  I have no open items; I'll re-run
the round-trip on later versions only to check for regressions.

Regards,
Rui

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

От:
Rui Zhao <zhaorui126@gmail.com>
Дата:
Hi Akshay,

Nice patch -- server-side CREATE TABLE reconstruction is something we want in
production. I tested v11 on current master and it round-trips correctly
across the documented coverage, including the recent grammar (VIRTUAL
columns, NOT ENFORCED, temporal WITHOUT OVERLAPS / PERIOD) and state set
later via ALTER. A few comments:

1. Real bug in schema_qualified => false. strip_schema_prefix() strips the
base prefix anywhere in code position with no token-boundary check, so a
cross-schema name whose schema ends with the base schema's name gets
mangled:

postgres=# CREATE SCHEMA xs;
CREATE SCHEMA
postgres=# CREATE TABLE xs.reftbl(id int PRIMARY KEY);
CREATE TABLE
postgres=# CREATE SCHEMA s;
CREATE SCHEMA
postgres=# CREATE TABLE s.orders(id int PRIMARY KEY, ref int
REFERENCES xs.reftbl(id));
CREATE TABLE
postgres=# SELECT d FROM
pg_get_table_ddl('s.orders'::regclass,'schema_qualified','false') d;
                                              d
---------------------------------------------------------------------------------------------
 CREATE TABLE orders (id integer NOT NULL, ref integer);
 ALTER TABLE orders OWNER TO postgres;
 ALTER TABLE orders ADD CONSTRAINT orders_pkey PRIMARY KEY (id);
 ALTER TABLE orders ADD CONSTRAINT orders_ref_fkey FOREIGN KEY (ref)
REFERENCES xreftbl(id);
(4 rows)

The last FK line says REFERENCES xreftbl(id); it should be xs.reftbl(id),
and the result doesn't replay. Gating the match on a token boundary fixes
it -- though see (2), which would remove this code path (and the bug) entirely.

2. Bigger picture: is schema_qualified needed at all? None of the existing
pg_get_*def / pg_get_*ddl functions have such a knob, so this would be the
lone exception. The established convention is to let search_path decide
(generate_relation_name): pg_get_viewdef and pg_get_constraintdef already
work that way, and pg_get_indexdef supports it too via
pg_get_indexdef(idx, 0, true) (the PRETTYFLAG_SCHEMA code path). It is
genuinely what we want in production -- the caller
controls qualification through search_path. Set it to the table's schema for
unqualified output, or to pg_catalog (or '') for fully-qualified
schema.table; pg_dump itself dumps under an empty search_path
(ALWAYS_SECURE_SEARCH_PATH_SQL) for exactly this reason. Following the
convention would also drop the option and the strip_schema_prefix code (and
this bug).

That last point matters: there's no robust way to strip a schema prefix out
of already-generated SQL by text processing. Doing it safely means
re-tokenizing arbitrary SQL (string literals, quoted identifiers, dollar
quotes, comments, casts, operators, ...), and strip_schema_prefix is a
hand-rolled partial scanner of exactly that. It shows -- it has already
needed several over-stripping fixes during review (the base name appearing
inside a string literal, and inside a quoted identifier), and the
token-boundary case in (1) is yet another. Deciding qualification at
generation time (generate_relation_name) avoids the whole class: the
backend's real deparser already gets this right, rather than a post-hoc
string pass trying to re-derive it.

3. typedefs.list is missing TableDdlContext and LocalNotNullEntry, so
pgindent leaves the "Type * var" pointer spacing in ~30 places -- for
example the forward declarations at ddlutils.c:226-237:

    static void append_stmt(TableDdlContext * ctx);

There is also a stale comment at ddlutils.c:1830 in append_column_defs():
inherited columns are described as "emitted by the INHERITS clause (once
implemented)", but INHERITS is implemented now.

4. Related to (2): a temporary table's default output qualifies it with the
session-private pg_temp_NN schema (e.g. CREATE TEMPORARY TABLE pg_temp_3.t
...), which won't replay anywhere else. A reconstructed temp table should
just be CREATE TEMPORARY TABLE t (...) -- the TEMPORARY keyword already puts
it in pg_temp, so the schema name should never be emitted. This also falls
out for free under the search_path convention in (2): pg_temp is always in
the effective search_path, so the table is visible and wouldn't be qualified
in the first place.

5. The commit message is out of sync with the code and func-info.sgml on the
option interface: it still says "include and exclude" and lists plural kind
names (indexes, foreign_keys, triggers, policies, partitions), whereas the
code and docs use only/except and singular names (index, foreign_key,
trigger, policy, partition). Looks like a leftover from the
include/exclude -> only/except rename. (FWIW on the naming itself,
include/exclude is the more common convention for this kind of list
parameter -- pg_dump has --exclude-table/-schema/-extension, etc. -- while
only/except reads more like the SQL keywords.)

Otherwise it looks good.

Thanks,
Rui


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

От:
Zsolt Parragi <zsolt.parragi@percona.com>
Дата:
The previous features all look good to me, I only have one question
for the new flag.

> Calling
> pg_get_table_ddl(t, 'includes_foreign_keys', 'false') now emits everything
> except FOREIGN KEY constraints. This covers the multi-tenant clone
> workflow: create tables first without cross-table references, then re-run
> with the default to add the constraints once all targets exist.

I think this feature needs a bit more documentation, an
"only_foreign_keys" flag, or both.

CREATE TABLE refd (id int PRIMARY KEY);
CREATE TABLE cons (a int CHECK(a>0), b int UNIQUE, c int REFERENCES refd(id));

-- pass 1: running without foreign keys
SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','false');
-- execute everything

-- loading data

-- pass 2: running with everything
SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','true');
-- ERROR: relation "cons" already exists (and the unique constraint
also collides)

I could do a "grep FOREIGN KEY" before executing (unless it's a tricky
schema where that phrase appears elsewhere), or since psql continues
on error, it will simply work if I accept a significant error noise,
but then the documentation should be clear about this limitation.
Following the documented approach and getting a bunch of unexpected
errors could be confusing for users.


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

От:
Zsolt Parragi <zsolt.parragi@percona.com>
Дата:
Thanks for the update! The new version looks mostly good, I only found
one corner case that doesn't work, double quoted literals can still
get over-stripped:

CREATE SCHEMA s;
CREATE TABLE s.p (id int, "s.weird" int) PARTITION BY RANGE (id);
CREATE TABLE s.pc PARTITION OF s.p
    (CONSTRAINT chk CHECK ("s.weird" > 0)) FOR VALUES FROM (0) TO (100);
SELECT * FROM pg_get_table_ddl('s.pc', 'owner', 'false',
                               'schema_qualified', 'false');


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

От:
Zsolt Parragi <zsolt.parragi@percona.com>
Дата:
I can confirm that most fixes work correctly, but there are two
remaining issues / regressions caused by the fixes:

1.

CREATE TABLE pidx (a int, b int) PARTITION BY RANGE (a);
CREATE TABLE pidx_1 PARTITION OF pidx FOR VALUES FROM (0) TO (10);
CREATE TABLE pidx_2 PARTITION OF pidx FOR VALUES FROM (10) TO (20);
CREATE INDEX ON pidx (b);

is emitted as:

CREATE TABLE public.pidx (a integer, b integer) PARTITION BY RANGE (a);
CREATE INDEX pidx_b_idx ON ONLY public.pidx USING btree (b);
CREATE TABLE public.pidx_1 PARTITION OF public.pidx FOR VALUES FROM (0) TO (10);
CREATE INDEX pidx_1_b_idx ON public.pidx_1 USING btree (b);   <-- fails here
--  ERROR:  relation "pidx_1_b_idx" already exists
ALTER INDEX public.pidx_b_idx ATTACH PARTITION pidx_1_b_idx;
-- ...

2.

CREATE TABLE idt (a int GENERATED BY DEFAULT AS IDENTITY);
ALTER SEQUENCE idt_a_seq AS smallint;

is emitted as:

-- ERROR:  conflicting or redundant options
-- LINE 1: ....idt (a integer GENERATED BY DEFAULT AS IDENTITY (AS smallin...
CREATE TABLE public.idt (a integer GENERATED BY DEFAULT AS IDENTITY
(AS smallint MAXVALUE 32767) NOT NULL);


> > Interestingly pg_dump also skips this, that looks like a pg_dump bug?
>
> The behavior was indeed inconsistent with what emit_indexes() already
> did for non-constraint indexes. emit_local_constraints() now emits ALTER
> INDEX ... ALTER COLUMN n SET STATISTICS for PK/UNIQUE/EXCLUSION constraint
> backing indexes.

I submitted a matching patch for pg_dump:
https://www.postgresql.org/message-id/CAN4CZFMd9riOuV5LkM%2BuV%3DuF-HYp49C8Fh22xWyMxptGLBCZ3A%40mail.gmail.com


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

От:
Zsolt Parragi <zsolt.parragi@percona.com>
Дата:
I did some more testing, I noticed one more issue with self
referencing foreign keys:

CREATE TABLE t (id int PRIMARY KEY, parent_id int REFERENCES t(id));
SELECT pg_get_table_ddl('t'::regclass);
-- CREATE TABLE public.t (id integer NOT NULL, parent_id integer);
-- ALTER TABLE public.t OWNER TO postgres;
-- ALTER TABLE public.t ADD CONSTRAINT t_parent_id_fkey FOREIGN KEY
(parent_id) REFERENCES public.t(id);
-- ALTER TABLE public.t ADD CONSTRAINT t_pkey PRIMARY KEY (id);

It tries to add the foreign key before the primary, and fails with
`ERROR:  there is no unique constraint matching given keys for
referenced table "t"`

There's also another issue in schema_qualified false, with partitions
in different schemas:

CREATE SCHEMA s;
CREATE SCHEMA other;
CREATE TABLE s.pt (id int, val int) PARTITION BY RANGE (id);
CREATE TABLE other.pt_c PARTITION OF s.pt FOR VALUES FROM (0) TO (100);
SELECT pg_get_table_ddl('s.pt'::regclass, schema_qualified => false);
-- CREATE TABLE pt (id integer, val integer) PARTITION BY RANGE (id);
-- ALTER TABLE pt OWNER TO postgres;
-- CREATE TABLE pt_c PARTITION OF s.pt FOR VALUES FROM (0) TO (100);
-- ALTER TABLE pt_c OWNER TO postgres;

The second create table statement references pt as s.pt, which seems incorrect.
It is also missing its own schema qualification, which I'm unsure if
it is wrong or not. If I interpret the documentation strictly, it
isn't the target table, so it should appear with its schema
qualification?


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

От:
Zsolt Parragi <zsolt.parragi@percona.com>
Дата:
Hello!

I did some basic testing with the new functions, and found a few bugs:

1. Seems like check constraints on partitions are ignored:

CREATE TABLE p (id int, val int) PARTITION BY RANGE (id);
CREATE TABLE p_child PARTITION OF p (CONSTRAINT chk_inline CHECK (val > 0))
    FOR VALUES FROM (0) TO (100);
SELECT * FROM pg_get_table_ddl('p_child', 'owner','false');

2. inherited stored generated columns can't be replayed:

CREATE TABLE par_s (
    id int,
    g int GENERATED ALWAYS AS (id * 2) STORED
);
CREATE TABLE ch_s () INHERITS (par_s);
SELECT * FROM pg_get_table_ddl('ch_s', 'owner','false');
-- CREATE TABLE public.ch_s () INHERITS (public.par_s);
-- ALTER TABLE public.ch_s ALTER COLUMN g SET DEFAULT (id * 2);

Dropping ch_s, executing the returned statements:

ERROR:  column "g" of relation "ch_s" is a generated column
HINT:  Use ALTER TABLE ... ALTER COLUMN ... SET EXPRESSION instead.

3. named not null constraints can't be replayed:

CREATE TABLE t (a int CONSTRAINT my_nn NOT NULL);
SELECT * FROM pg_get_table_ddl('t'::regclass,'owner','false');
-- CREATE TABLE public.t ( a integer NOT NULL);
-- ALTER TABLE public.t ADD CONSTRAINT my_nn NOT NULL a;

Dropping t, executing the statements:

ERROR:  cannot create not-null constraint "my_nn" on column "a" of table "t"
DETAIL:  A not-null constraint named "t_a_not_null" already exists for
this column.


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

От:
Zsolt Parragi <zsolt.parragi@percona.com>
Дата:
Thanks, I can confirm that only_foreign_keys works properly.

> Combining `only_foreign_keys=true` with `includes_foreign_keys=false` is
> rejected upfront since it would produce no output.

Shouldn't only_foreign_keys=true with include_constraints=false also error out?


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

От:
Zsolt Parragi <zsolt.parragi@percona.com>
Дата:
Thanks, I can confirm that the previous bugs were fixed, however the
bugfixes also introduce a new issue, where inherited not null
constraints are missing:

CREATE TABLE par (a int);
CREATE TABLE ch (b int) INHERITS (par);
ALTER TABLE ch ADD CONSTRAINT my_nn NOT NULL a;
SELECT * FROM pg_get_table_ddl('ch','owner','false');
--  CREATE TABLE public.ch ( b integer) INHERITS (public.par);

The schema_qualified => false part also doesn't work as described:

+        CREATE STATISTICS statement.  References to
+        objects in the same schema as the target table (inheritance
+        parents, partition parents, identity sequences, and any
+        same-schema object the deparse helpers happen to mention) are
+        also emitted unqualified, so the script can be replayed under a
+        different search_path to recreate the table
+        in another schema.

but:

CREATE SCHEMA s1;
CREATE SEQUENCE s1.myseq;
CREATE FUNCTION s1.f(int) RETURNS int LANGUAGE sql IMMUTABLE AS 'SELECT $1';
CREATE TABLE s1.t (
    id  int DEFAULT nextval('s1.myseq'),
    val int,
    CONSTRAINT chk CHECK (s1.f(val) > 0)
);
SELECT * FROM pg_get_table_ddl('s1.t', 'owner','false',
'schema_qualified','false');
--  CREATE TABLE t ( id integer DEFAULT nextval('s1.myseq'::regclass),
val integer, CONSTRAINT chk CHECK ((s1.f(val) > 0)));

s1 appears twice in the output.

It also has an issue with strings containing the schema:

CREATE SCHEMA myschema;
CREATE TABLE myschema.p (id int, note text) PARTITION BY RANGE (id);
CREATE TABLE myschema.pc PARTITION OF myschema.p
    (CONSTRAINT chk CHECK (note <> 'myschema.secret')) FOR VALUES FROM
(0) TO (100);
SELECT * FROM pg_get_table_ddl('myschema.pc', 'owner','false',
'schema_qualified','false');
-- CREATE TABLE pc PARTITION OF p FOR VALUES FROM (0) TO (100);
-- ALTER TABLE pc ADD CONSTRAINT chk CHECK ((note <> 'secret'::text));


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

От:
Zsolt Parragi <zsolt.parragi@percona.com>
Дата:
Hello!

I can confirm the previous issues fixed, however I also found one more
with unique indexes on partitioned tables:

CREATE SCHEMA s;
CREATE TABLE s.p (id int, region text) PARTITION BY LIST (region);
CREATE TABLE s.p_a PARTITION OF s.p FOR VALUES IN ('a');
CREATE UNIQUE INDEX p_uidx ON s.p (id, region);

SELECT pg_get_table_ddl('s.p', owner => false);
-- CREATE TABLE s.p (id integer, region text) PARTITION BY LIST (region);
-- CREATE UNIQUE INDEX p_uidx ON s.p USING btree (id, region);
-- CREATE TABLE s.p_a PARTITION OF s.p FOR VALUES IN ('a');
-- CREATE UNIQUE INDEX p_a_id_region_idx ON s.p_a USING btree (id,
region); -- fails because index already exists


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

От:
Zsolt Parragi <zsolt.parragi@percona.com>
Дата:
The new version seem to work correctly to me, I didn't find any
further issues. Now the main blockers seem to be the remaining TODOs
related to includes_triggers/includes_policies.

I only have some minor comments about code structuring:

+ * get_inheritance_parents
+ *		Return a List of parent OIDs for relid, ordered by inhseqno.
+ *
+ * find_inheritance_children() walks the opposite direction (parent->children),

Shouldn't this follow the same naming and parameter pattern and live
at the same place in pg_inherits?

+static char *
+lookup_qualified_relname(Oid relid)
+...
+static char *
+lookup_relname_for_emit(Oid relid, bool schema_qualified, Oid base_namespace)

Is lookup_qualified_relname needed? It is only called within
lookup_relname_for_emit, and it results in a double syscache lookup,
which could be avoided if these were a single function.

+		/* COMPRESSION clause, only if explicitly set on the column. */
+		if (CompressionMethodIsValid(att->attcompression))
+		{
+			const char *cm = NULL;
+
+			switch (att->attcompression)
+			{
+				case TOAST_PGLZ_COMPRESSION:
+					cm = "pglz";
+					break;
+				case TOAST_LZ4_COMPRESSION:
+					cm = "lz4";
+					break;
+			}
+			if (cm)
+				appendStringInfo(buf, " COMPRESSION %s", cm);
+		}

Isn't this basically GetCompressionMethodName(att->attcompression)?

+		/* STORAGE clause, only if it differs from the type's default. */
+		if (att->attstorage != get_typstorage(att->atttypid))
+		{
+			const char *storage = NULL;
+
+			switch (att->attstorage)
+			{
+				case TYPSTORAGE_PLAIN:
+...

And this seems like storage_name(att->attstorage) from tablecmds.c,
the only issue is that that's currently static


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

От:
Zsolt Parragi <zsolt.parragi@percona.com>
Дата:
The new design is definitely an improvement as a more generic
interface. I'm unsure about the "include" wording, to me that suggest
"also includes", while the actual behavior is "only includes". To me
it would be a bit surprising pg_get_table_ddl including something not
including the basic create table statement.

I also found a few issues with include in v10.

Include check doesn't seem to work:

CREATE TABLE t (
    id int PRIMARY KEY,
    qty int CHECK (qty > 0),
    CONSTRAINT t_id_pos CHECK (id > 0)
);

SELECT * FROM pg_get_table_ddl('t', 'include', 'check'); -- empty?

The include partitions clause also doesn't work:

CREATE TABLE p (id int, val text) PARTITION BY RANGE (id);
CREATE TABLE p_a PARTITION OF p FOR VALUES FROM (0) TO (100);
CREATE TABLE p_b PARTITION OF p FOR VALUES FROM (100) TO (200);

SELECT * FROM pg_get_table_ddl('p','include','partitions'); -- empty

There's also an issue with replica identity non primary key unique indexes:

CREATE TABLE t2 (a int NOT NULL UNIQUE, b int);
ALTER TABLE t2 REPLICA IDENTITY USING INDEX t2_a_key;
SELECT * FROM pg_get_table_ddl('t2','exclude','unique');
-- ALTER TABLE public.t2 REPLICA IDENTITY USING INDEX t2_a_key;
-- but there's no such index


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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:


On Sun, Jun 28, 2026 at 10:14 PM Rui Zhao <zhaorui126@gmail.com> wrote:
Hi Akshay,

Nice patch -- server-side CREATE TABLE reconstruction is something we want in
production. I tested v11 on current master and it round-trips correctly
across the documented coverage, including the recent grammar (VIRTUAL
columns, NOT ENFORCED, temporal WITHOUT OVERLAPS / PERIOD) and state set
later via ALTER. A few comments:

1. Real bug in schema_qualified => false. strip_schema_prefix() strips the
base prefix anywhere in code position with no token-boundary check, so a
cross-schema name whose schema ends with the base schema's name gets
mangled:

postgres=# CREATE SCHEMA xs;
CREATE SCHEMA
postgres=# CREATE TABLE xs.reftbl(id int PRIMARY KEY);
CREATE TABLE
postgres=# CREATE SCHEMA s;
CREATE SCHEMA
postgres=# CREATE TABLE s.orders(id int PRIMARY KEY, ref int
REFERENCES xs.reftbl(id));
CREATE TABLE
postgres=# SELECT d FROM
pg_get_table_ddl('s.orders'::regclass,'schema_qualified','false') d;
                                              d
---------------------------------------------------------------------------------------------
 CREATE TABLE orders (id integer NOT NULL, ref integer);
 ALTER TABLE orders OWNER TO postgres;
 ALTER TABLE orders ADD CONSTRAINT orders_pkey PRIMARY KEY (id);
 ALTER TABLE orders ADD CONSTRAINT orders_ref_fkey FOREIGN KEY (ref)
REFERENCES xreftbl(id);
(4 rows)

The last FK line says REFERENCES xreftbl(id); it should be xs.reftbl(id),
and the result doesn't replay. Gating the match on a token boundary fixes
it -- though see (2), which would remove this code path (and the bug) entirely.

   Fixed. 

2. Bigger picture: is schema_qualified needed at all? None of the existing
pg_get_*def / pg_get_*ddl functions have such a knob, so this would be the
lone exception. The established convention is to let search_path decide
(generate_relation_name): pg_get_viewdef and pg_get_constraintdef already
work that way, and pg_get_indexdef supports it too via
pg_get_indexdef(idx, 0, true) (the PRETTYFLAG_SCHEMA code path). It is
genuinely what we want in production -- the caller
controls qualification through search_path. Set it to the table's schema for
unqualified output, or to pg_catalog (or '') for fully-qualified
schema.table; pg_dump itself dumps under an empty search_path
(ALWAYS_SECURE_SEARCH_PATH_SQL) for exactly this reason. Following the
convention would also drop the option and the strip_schema_prefix code (and
this bug).

That last point matters: there's no robust way to strip a schema prefix out
of already-generated SQL by text processing. Doing it safely means
re-tokenizing arbitrary SQL (string literals, quoted identifiers, dollar
quotes, comments, casts, operators, ...), and strip_schema_prefix is a
hand-rolled partial scanner of exactly that. It shows -- it has already
needed several over-stripping fixes during review (the base name appearing
inside a string literal, and inside a quoted identifier), and the
token-boundary case in (1) is yet another. Deciding qualification at
generation time (generate_relation_name) avoids the whole class: the
backend's real deparser already gets this right, rather than a post-hoc
string pass trying to re-derive it.

 Done. strip_schema_prefix and append_stripped_stmt are gone. Instead of post-processing, I added four thin wrappers in ruleutils.c. Let the deparser decide qualification at generation time: 
- pg_get_indexdef_ddl passes PRETTYFLAG_SCHEMA to the worker, enabling generate_relation_name instead of generate_qualified_relation_name.
- pg_get_ruledef_ddl — same flag.
- pg_get_constraintdef_body — returns body only (FK targets already use generate_relation_name); emit_local_constraints now builds the ALTER TABLE ctx->qualname ADD CONSTRAINT prefix itself.
- pg_get_statisticsobjdef_ddl — uses StatisticsObjIsVisible() to qualify the statistics object name     
The schema_qualified=false path still narrows search_path to the base schema, which is what makes all four helpers emit unqualified names for same-schema objects. The option is kept for per-call convenience.
3. typedefs.list is missing TableDdlContext and LocalNotNullEntry, so
pgindent leaves the "Type * var" pointer spacing in ~30 places -- for
example the forward declarations at ddlutils.c:226-237:

    static void append_stmt(TableDdlContext * ctx);

There is also a stale comment at ddlutils.c:1830 in append_column_defs():
inherited columns are described as "emitted by the INHERITS clause (once
implemented)", but INHERITS is implemented now.

4. Related to (2): a temporary table's default output qualifies it with the
session-private pg_temp_NN schema (e.g. CREATE TEMPORARY TABLE pg_temp_3.t
...), which won't replay anywhere else. A reconstructed temp table should
just be CREATE TEMPORARY TABLE t (...) -- the TEMPORARY keyword already puts
it in pg_temp, so the schema name should never be emitted. This also falls
out for free under the search_path convention in (2): pg_temp is always in
the effective search_path, so the table is visible and wouldn't be qualified
in the first place.

5. The commit message is out of sync with the code and func-info.sgml on the
option interface: it still says "include and exclude" and lists plural kind
names (indexes, foreign_keys, triggers, policies, partitions), whereas the
code and docs use only/except and singular names (index, foreign_key,
trigger, policy, partition). Looks like a leftover from the
include/exclude -> only/except rename. (FWIW on the naming itself,
include/exclude is the more common convention for this kind of list
parameter -- pg_dump has --exclude-table/-schema/-extension, etc. -- while
only/except reads more like the SQL keywords.)

   Fixed 3, 4 and 5. 

   The v12 patch is ready for review/test.

Otherwise it looks good.

Thanks,
Rui

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
Hi Rui,

Thanks again for the detailed review. I've included both of your patches.

The v25 patch is ready for review/test.

On Tue, Aug 11, 2026 at 5:21 PM Rui Zhao <zhaorui126@gmail.com> wrote:
Hi Akshay,

v24 applies to master (1d1d7b0e9c), builds warning-free, installcheck 246/246.
All four fixes check out here.

Three things.

1. The new doc paragraph promises something that doesn't happen.  It says the
child constraint names come back if you emit the whole tree:

  Round-trip fidelity for these names requires emitting the whole tree
  via the parent table.

Emitting the whole tree doesn't preserve them either -- the child's constraint
isn't in the output at all:

    CREATE TABLE pk_p (a int, b int) PARTITION BY LIST (a);
    CREATE TABLE pk_c (a int, b int, CONSTRAINT pk_c_mypk PRIMARY KEY (a,b));
    ALTER TABLE pk_p ADD CONSTRAINT pk_p_mypk PRIMARY KEY (a,b);
    ALTER TABLE pk_p ATTACH PARTITION pk_c FOR VALUES IN (1);

    SELECT d FROM pg_get_table_ddl('pk_p', owner => false) d;
    --  CREATE TABLE public.pk_p (a integer NOT NULL, b integer NOT NULL)
    --      PARTITION BY LIST (a);
    --  CREATE TABLE public.pk_c PARTITION OF public.pk_p
    --      (CONSTRAINT pk_c_a_not_null NOT NULL a,
    --       CONSTRAINT pk_c_b_not_null NOT NULL b) FOR VALUES IN (1);
    --  ALTER TABLE public.pk_p ADD CONSTRAINT pk_p_mypk PRIMARY KEY (a, b);

    -- replayed into a fresh database:
    -- source  pg_constraint on pk_c: pk_c_mypk
    -- replay  pg_constraint on pk_c: pk_c_pkey

Same for UNIQUE (uq_c_myuq) and EXCLUDE (ep1_myexcl), which are not mentioned in
the parent's output either.  That is the half you said you could see working but
haven't written yet, so until it is in, the sentence should say the names are
not preserved in either direction rather than point at a workaround that isn't
there.

2. schema_qualified => false modifies the source when the table is partitioned
with a child in another schema.  Attached 0001 fixes this.  The docs give that
parameter this purpose:

  the script can be replayed under a different search_path to recreate the
  table in another schema

but the child is hard-qualified, and so is the parent reference in its
PARTITION OF:

    CREATE SCHEMA sa; CREATE SCHEMA sb;
    CREATE TABLE sa.yp (a int, b int) PARTITION BY LIST (a);
    CREATE TABLE sb.yc (a int, b int);
    CREATE INDEX yc_myidx ON sb.yc (b);
    CREATE INDEX yp_myidx ON sa.yp (b);
    ALTER TABLE sa.yp ATTACH PARTITION sb.yc FOR VALUES IN (1);

    SELECT d FROM pg_get_table_ddl('sa.yp', owner => false,
                                   schema_qualified => false) d;
    --  CREATE TABLE yp (a integer, b integer) PARTITION BY LIST (a);
    --  CREATE TABLE sb.yc PARTITION OF sa.yp FOR VALUES IN (1);
    --  CREATE INDEX yc_myidx ON sb.yc USING btree (b);
    --  CREATE INDEX yp_myidx ON ONLY yp USING btree (b);
    --  ALTER INDEX yp_myidx ATTACH PARTITION sb.yc_myidx;

sa.yp is written both ways in the one script.  Replaying it with
search_path = sc, against a database that has sa.yp but not yet sb.yc:

    SET
    CREATE TABLE
    CREATE TABLE
    CREATE INDEX
    CREATE INDEX
    ERROR:  cannot attach index "yc_myidx" as a partition of index "yp_myidx"
    DETAIL:  Index "yc_myidx" is not an index on any partition of table "yp".

    SELECT inhparent::regclass, inhrelid::regclass FROM pg_inherits;
    --  sa.yp        | sb.yc
    --  sa.yp_myidx  | sb.yc_b_idx

sc.yp is left with no partitions, and sa.yp has gained one it did not have
before -- the second CREATE TABLE attached the child to the source parent.

The child carrying its own schema is right, and the test comment says why.  But
it is done by switching the child's whole context to schema_qualified, and that
flag also governs the child's reference back to the parent -- which is in the
base namespace, the one search_path stands in for, so it does not have to be
qualified.  0001 passes the base namespace down instead, which keeps the part
you fixed and drops the part that came along with it, and gives:

    --  CREATE TABLE yp (a integer, b integer) PARTITION BY LIST (a);
    --  CREATE TABLE sb.yc PARTITION OF yp FOR VALUES IN (1);
    --  CREATE INDEX yc_myidx ON sb.yc USING btree (b);
    --  CREATE INDEX yp_myidx ON ONLY yp USING btree (b);
    --  ALTER INDEX yp_myidx ATTACH PARTITION sb.yc_myidx;

The run above then completes, with sb.yc attached to sc.yp and sa.yp untouched,
and replaying under search_path = sa still reproduces the original.  In
installcheck it moves one line, in the test that covers this case:

    - CREATE TABLE pgtbl_ddl_part_other.pt_c PARTITION OF
pgtbl_ddl_part_s.pt ...
    + CREATE TABLE pgtbl_ddl_part_other.pt_c PARTITION OF pt ...

3. The ATTACH PARTITION fix has no test for the shape it fixes.  Attached 0002
adds one.  The six expected lines the fix moved are all same-schema, where the
old code worked by search_path; a fix that qualified with the base namespace
instead of the child's own would pass them too.  One CREATE INDEX on the
cross-schema partition block already in pg_get_table_ddl.sql covers it: with
0002 in, putting get_rel_name() back in emit_indexes() turns

   ALTER INDEX pt_val_idx ATTACH PARTITION pgtbl_ddl_part_other.pt_c_val_idx;

into the bare form, and it is the only one of the seven ATTACH lines that is
cross-schema.

Both patches are on top of v24; installcheck is 246/246 with them.

Regards,
Rui

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:


On Thu, Jul 2, 2026 at 7:47 AM Kyotaro Horiguchi <horikyota.ntt@gmail.com> wrote:
Hello,

Looking at this, one thing that concerns me is the large amount of
overlap with dumpTableSchema() in pg_dump.

I wonder if it would make sense to separate the SQL generation logic
into frontend/backend-shared code so that it could also be used by
pg_dump. The catalog lookup would naturally remain separate, but
sharing the DDL generation itself would significantly reduce the
duplication.

Thanks for the suggestion. The overlap is real, but sharing the SQL generation logic at the C level runs into a few structural mismatches: dumpTableSchema drives column rendering off pre-populated TableInfo arrays that pg_dump bulk-loaded at startup, while the backend function uses live syscache lookups - a shared builder would need an adapter layer roughly as large as the code it replaces. The pattern PostgreSQL already uses for this kind of sharing is to call backend pg_get_*def functions via SQL from pg_dump — it already does this 20 times for indexes, constraints, triggers, rules, and statistics. The natural long-term path would be for pg_dump to call pg_get_table_ddl() the same way and retire dumpTableSchema, but that is a substantial refactor in its own right and feels out of scope here.

By the way, a couple of comments use a Unicode RIGHTWARDS ARROW
(U+2192). Please use an ASCII equivalent instead.
   
   Will fix it in the next v15 patch. 

Regards,

--
Kyotaro Horiguchi
NTT Open Source Software Center

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:


On Wed, Jul 1, 2026 at 8:41 PM Marcos Pegoraro <marcos@f10.com.br> wrote:
Em qua., 1 de jul. de 2026 às 11:12, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
pretty, owner, tablespace, and schema_qualified are plain boolean parameters with DEFAULT values
Filtering parameters: only_kinds / except_kinds as text[] 
If all parameters are optional, and all parameters are boolean, perhaps you could also make pretty, owner, tablespace, and schema_qualified as optional parts of only_kinds and except_kinds.

Therefore, we could call these two ways and the result would be the same.
pg_get_table_ddl('idxd'::regclass, owner => false, tablespace => false, except_kinds => '{primary_key}');
pg_get_table_ddl('idxd'::regclass, except_kinds => '{primary_key,tablespace,owner}');

Obviously this way you have to know if owner param is false or it exists on except_kinds. 
What do you think ?

owner is the one case where it could work, but to make it consistent with how owner behaves in pg_get_tablespace_ddl and pg_get_database_ddl, we should not add it.

tablespace doesn't map to a kind at all. It controls the inline TABLESPACE clause within the CREATE TABLE statement body it's a sub-clause, not a separate statement. If we added tablespace as a kind, except_kinds => '{table,tablespace}' would be wrong (if you're skipping the table statement, there's no inline clause to suppress), and except_kinds => '{tablespace}' would imply skipping a standalone statement that doesn't exist.

pretty and schema_qualified are rendering/formatting options, not statement filters. They affect how every statement is rendered — indentation, name qualification — not which statements are emitted. Putting them in except_kinds conflates two orthogonal axes: filtering (what to emit) and formatting (how to emit it).
                                                                                                                                                                                                           
The current design intentionally keeps these separate: only_kinds/except_kinds for statement-level filtering, booleans for rendering and inline-clause control. Merging them would make except_kinds overloaded and harder to document clearly.
regards
Marcos

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:

On Thu, Jun 11, 2026 at 2:13 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote:
Thanks for the update! The new version looks mostly good, I only found
one corner case that doesn't work, double quoted literals can still
get over-stripped:

CREATE SCHEMA s;
CREATE TABLE s.p (id int, "s.weird" int) PARTITION BY RANGE (id);
CREATE TABLE s.pc PARTITION OF s.p
    (CONSTRAINT chk CHECK ("s.weird" > 0)) FOR VALUES FROM (0) TO (100);
SELECT * FROM pg_get_table_ddl('s.pc', 'owner', 'false',
                               'schema_qualified', 'false');

  Fixed the issue above. The v5 patch is ready for review/testing.
 

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:


On Tue, Jun 23, 2026 at 7:33 PM Marcos Pegoraro <marcos@f10.com.br> wrote:
Em ter., 23 de jun. de 2026 às 10:34, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
A quick note on a design change I made in the pg_get_table_ddl option surface

Much better now.

All options can be present or omitted, so why couldn't the owner also be part of the include/exclude options too ?

Just one addition, some options are plural and others are not.
We have indexes, policies, triggers, but we also have check, unique, exclusion.
You don't know how many indices or checks you have, so wouldn't it be better to have them in singular form ?

Regarding moving owner into the include/exclude syntax, I prefer keeping it as a top-level boolean for now:
1) It aligns with existing sibling functions (pg_get_role_ddl, pg_get_tablespace_ddl, and pg_get_database_ddl), which all use a boolean owner.
2) Existing regression tests and early callers pass owner => false for output stability. Moving this into the exclude list would create unnecessary churn that I'd prefer to avoid bundling here.

As for the naming inconsistency (singular vs. plural), you're entirely right mixing indexes/policies/triggers with checks/uniques/exclusions was an oversight. Note that I left statistics alone to match the CREATE STATISTICS keyword.

I'll address the naming consistency in the next patch. 

regards
Marcos

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
Thanks for the detailed review. I have fixed the issues 1, 2, 4 and 5. 

Issue no 3: For the tree case (pg_get_table_ddl on the parent): letting conislocal=false through for PK/UNIQUE/EXCLUSION on a direct partition child, emitting the child constraint first with ONLY on both sides, then reusing the ALTER INDEX ... ATTACH PARTITION block I can see that working. 

For the standalone-child case, I agree that it's fragile. `CREATE TABLE ... PARTITION OF auto-creates the child constraint, so an explicit ADD CONSTRAINT collides. Without a flag on the context to distinguish "called on child alone" from "called as part of a tree walk", there's no clean way to rename after the fact. I'll leave that as a documented limitation (added to func-info.sgml in this version alongside security labels) until the tree case is in and we can revisit. 

The v24 patch is ready for review/test.

On Wed, Jul 29, 2026 at 9:37 PM Rui Zhao <zhaorui126@gmail.com> wrote:
Hi Akshay,

Thanks -- v23 builds clean here and installcheck is green (246/246). All three
from my last mail check out: CLUSTER ON no longer dangles on the case I posted,
the self-referential FK errors with the same shape as the REPLICA IDENTITY
guard, and check_stack_depth() is in the right place in emit_partition_children.

Five things after going over the new code.

1) The FK guard misses index-backed keys. fk_backing_constraint_is_blocked()
scans pg_constraint for a PK/UNIQUE on confrelid, but a foreign key only needs a
unique *index* over the referenced columns -- there needn't be a constraint at
all:

    CREATE TABLE u (id int, parent_id int);
    CREATE UNIQUE INDEX u_id_uidx ON u (id);
    ALTER TABLE u ADD CONSTRAINT u_fk FOREIGN KEY (parent_id) REFERENCES u (id);

    SELECT d FROM pg_get_table_ddl('u', owner => false,
                                   except_kinds => ARRAY['index']) d;
    --  CREATE TABLE public.u (id integer, parent_id integer);
    --  ALTER TABLE public.u ADD CONSTRAINT u_fk FOREIGN KEY (parent_id)
    --      REFERENCES public.u(id);
    -- replay: ERROR:  there is no unique constraint matching given keys for
    --                 referenced table "u"

The scan finds no candidate, concludes "not blocked", and we are back to the
dangling case. Worth noting REPLICA IDENTITY does catch this same shape -- a
table whose replica-identity index is a plain unique index gives

    ERROR:  REPLICA IDENTITY for table "ri" requires kind "index" to be emitted

pg_constraint.conindid on the FK row points straight at the backing index in
both shapes -- u_id_uidx above, and the PK's own index for the constraint-backed
case from my last mail -- so feeding that to the index_ddl_kind() you just added
for CLUSTER ON covers both, and the matching scan can go:

- if (con->confrelid == ctx->relid)
+ if (con->confrelid == ctx->relid &&
+ OidIsValid(con->conindid))
  {
- TableDdlKind ref_kind;
+ TableDdlKind ref_kind = index_ddl_kind(con->conindid);

- if (fk_backing_constraint_is_blocked(conTup, conRel,
- ctx, &ref_kind))
+ if (!is_kind_included(ctx, ref_kind))
  {
@@
- errdetail("The key references columns constrained by a \"%s\"
constraint, which is not in the active filter.",
+ errdetail("The key references an index produced by the \"%s\" kind,
which is not in the active filter.",

with fk_backing_constraint_is_blocked() and its forward declaration deleted --
net -122 lines. The errdetail moves because "constrained by an \"index\"
constraint" doesn't read right for the plain-index case; that wording now
matches the REPLICA IDENTITY guard, and the same edit applies to three lines in
pg_get_table_ddl.out. On top of v23 that gives:

    self-FK on a plain unique index, except index  ->  errors (silent before)
    self-FK on a PK / UNIQUE, that kind filtered   ->  still errors
    cross-table FK, backing kind filtered          ->  still no error
    nothing filtered                               ->  unchanged
    installcheck                                   ->  246/246

2) CLUSTER ON is dropped even when its backing kind is emitted.
emit_cluster_on() still opens with

    if (!is_kind_included(ctx, TABLE_DDL_KIND_INDEX))
        return;

which predates the new per-index check below it, and for a constraint-backed
clustered index the two now disagree:

    CREATE TABLE b (x int);
    ALTER TABLE b ADD CONSTRAINT b_pk PRIMARY KEY (x);
    ALTER TABLE b CLUSTER ON b_pk;

    SELECT d FROM pg_get_table_ddl('b', owner => false,
                only_kinds => ARRAY['table','primary_key']) d;
    --  CREATE TABLE public.b (x integer NOT NULL);
    --  ALTER TABLE public.b ADD CONSTRAINT b_pk PRIMARY KEY (x);

The PK is emitted, so the index exists on replay and CLUSTER ON would have
applied cleanly -- I appended it by hand to check -- but the early return drops
it because "index" isn't in the filter. except_kinds => ARRAY['index'] behaves
the same. With index_ddl_kind() in place that outer test is redundant anyway,
since it returns TABLE_DDL_KIND_INDEX for a plain index, so dropping it is
enough:

  List    *indexoids;
  ListCell   *lc;

- if (!is_kind_included(ctx, TABLE_DDL_KIND_INDEX))
- return;
-
  indexoids = RelationGetIndexList(ctx->rel);

With that, a PK- or UNIQUE-backed CLUSTER ON survives only_kinds =>
ARRAY['table','primary_key'] and except_kinds => ARRAY['index'], while the
plain-index case and the filtered-backing-kind cases behave as they do now;
installcheck stays at 246/246, including your new CLUSTER ON tests.

3) On the deferred naming case -- two notes, then it's your call on scope.

It isn't only EXCLUDE. A custom-named PK or UNIQUE on a partition child goes the
same way, since they all sit behind the same conislocal filter:

    CREATE TABLE pk_p (a int, b int) PARTITION BY LIST (a);
    CREATE TABLE pk_c (a int, b int, CONSTRAINT pk_c_mypk PRIMARY KEY (a,b));
    ALTER TABLE pk_p ADD CONSTRAINT pk_p_mypk PRIMARY KEY (a,b);
    ALTER TABLE pk_p ATTACH PARTITION pk_c FOR VALUES IN (1);

round-trips as pk_c_mypk -> pk_c_pkey, and the UNIQUE equivalent as
uq_c_myuq -> uq_c_a_b_key.

On the cost: I think it splits in two, and ChooseConstraintName is only needed
for one half.

When the whole tree is emitted -- pg_get_table_ddl on the parent -- the child's
name is still in pg_constraint (ATTACH only clears conislocal and bumps
coninhcount), so nothing has to be synthesised. It is the shape emit_indexes
already produces for a plain partitioned index:

    CREATE INDEX ic_myidx ON public.ic USING btree (b);
    CREATE INDEX ip_myidx ON ONLY public.ip USING btree (b);
    ALTER INDEX public.ip_myidx ATTACH PARTITION ic_myidx;

child first, parent ON ONLY, then ATTACH (that child name is unqualified per
(4), but these are all in one schema). The same works for constraints, and
the ordering already holds because emit_partition_children runs before
emit_local_constraints. I hacked it up here to check: let the conislocal filter
through for a partition child's PK/UNIQUE/EXCLUSION, put ONLY on both sides, and
reuse your ALTER INDEX ... ATTACH PARTITION block. ep1_excl, pk_c_mypk and
uq_c_myuq then come back with conislocal = false and coninhcount = 1, matching
the source, with installcheck still green.

The other half is where your concern lands, and I was wrong to wave it off.
Called on the child alone, the parent must already exist with its constraint, so
CREATE TABLE ... PARTITION OF creates the child's automatically and an explicit
ADD CONSTRAINT collides:

    ERROR:  multiple primary keys for table "parted_pk_a" are not allowed

which is what your own regression test gave me before I gated it. Renaming after
the fact does need the generated name, so that path is the fragile one, and
telling the two apart needs a flag on the context.

If the tree case is worth having on its own, it's the one round-trip fidelity
depends on; the standalone-child call could stay a documented limitation.

4) Unrelated, but it turned up while comparing against pg_dump:
emit_indexes() qualifies the parent in ALTER INDEX ... ATTACH PARTITION but not
the child, so a partition child in another schema doesn't replay:

    CREATE SCHEMA sa; CREATE SCHEMA sb;
    CREATE TABLE sa.yp (a int, b int) PARTITION BY LIST (a);
    CREATE TABLE sb.yc (a int, b int);
    CREATE INDEX yc_myidx ON sb.yc (b);
    CREATE INDEX yp_myidx ON sa.yp (b);
    ALTER TABLE sa.yp ATTACH PARTITION sb.yc FOR VALUES IN (1);

    SELECT d FROM pg_get_table_ddl('sa.yp', owner => false) d;
    --  CREATE TABLE sa.yp (a integer, b integer) PARTITION BY LIST (a);
    --  CREATE TABLE sb.yc PARTITION OF sa.yp FOR VALUES IN (1);
    --  CREATE INDEX yc_myidx ON sb.yc USING btree (b);
    --  CREATE INDEX yp_myidx ON ONLY sa.yp USING btree (b);
    --  ALTER INDEX sa.yp_myidx ATTACH PARTITION yc_myidx;
    -- replay: ERROR:  relation "yc_myidx" does not exist

schema_qualified doesn't help, since the child name is never qualified either
way:

    SELECT d FROM pg_get_table_ddl('sa.yp', owner => false,
                schema_qualified => true) d;
    --  ALTER INDEX sa.yp_myidx ATTACH PARTITION yc_myidx;

Same-schema cases only work because search_path happens to find the child.
pg_dump writes both sides qualified -- ALTER INDEX sa.yp_myidx ATTACH PARTITION
sb.yc_myidx; -- and running the child name through lookup_relname_for_emit(),
as you already do for the parent, is enough:

  foreach(clc, childIdxOids)
  {
  Oid childIdxOid = lfirst_oid(clc);
- char    *childIdxName = get_rel_name(childIdxOid);
+ char    *childIdxName = lookup_relname_for_emit(childIdxOid,
+    ctx->schema_qualified,
+    ctx->base_namespace);
@@
  parentIdxName,
- quote_identifier(childIdxName));
+ childIdxName);

With that the cross-schema case replays, schema_qualified => false still leaves
a same-schema child unqualified, and in installcheck it only moves six existing
ATTACH lines to the qualified form.

5) Two limitations aren't in the documented exclusion list yet. The list
in func-info.sgml has owned sequences, partition children whose column order
differs from the parent, triggers/policies, and COMMENT/GRANT, but not:

  - security labels, which you said back on v21 should be noted as a known
    limitation -- there's no mention of them in the docs as of v23;
  - the partition-child constraint naming above. Its sibling, the column
    reordering, is documented, so a reader would expect to find this one there
    too once it's deferred.

Regards,
Rui

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:


On Sun, Jul 12, 2026 at 12:53 AM Marcos Pegoraro <marcos@f10.com.br> wrote:
Em qui., 9 de jul. de 2026 às 05:54, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
Fixed the documentation. v19 patch is now ready for review.

I didn't understand when a word is considered reserved, so that it has double quotes.

postgres=# create table int(int4 integer primary key, integer integer, interval interval, boolean boolean, insert boolean);
CREATE TABLE
postgres=# select pg_get_table_ddl('int'::regclass, schema_qualified=>false, owner=>true, only_kinds => ARRAY['primary_key','check','table']);
                                                    pg_get_table_ddl
------------------------------------------------------------------------------------------------------------------------
 CREATE TABLE "int" (int4 integer NOT NULL, "integer" integer, "interval" interval, "boolean" boolean, insert boolean);
 ALTER TABLE "int" OWNER TO postgres;
 ALTER TABLE "int" ADD CONSTRAINT int_pkey PRIMARY KEY (int4);
(3 rows)

And as for the owner, I think that causes a bit of confusion when used with kinds parameters, doesn't it ?
It emits other ALTER TABLE except the OWNER TO.

postgres=# create table self(id integer primary key, self_id integer constraint self_self references self(id));
CREATE TABLE
postgres=# select pg_get_table_ddl('self'::regclass, owner=>true, except_kinds => ARRAY['table']);
                                         pg_get_table_ddl
--------------------------------------------------------------------------------------------------
 ALTER TABLE public.self ADD CONSTRAINT self_pkey PRIMARY KEY (id);
 ALTER TABLE public.self ADD CONSTRAINT self_self FOREIGN KEY (self_id) REFERENCES public.self(id);
(2 rows)
regards
Marcos

On quoting: This is correct behavior, not a bug. quote_identifier quotes any identifier whose keyword category is not UNRESERVED_KEYWORD. insert is UNRESERVED_KEYWORD in PostgreSQL's grammar (it can be used as an identifier without quoting), while integer, interval, and boolean are COL_NAME_KEYWORD — they can only serve as column names in restricted contexts, so they need quoting when used as general identifiers. The table name "int" is quoted for the same reason (int is COL_NAME_KEYWORD). This is identical to what pg_dump and quote_identifier do throughout PostgreSQL.                              

On owner + except_kinds: Also correct behavior by design. The owner parameter controls whether the ALTER TABLE ... OWNER TO statement is included when the table kind is in scope. OWNER TO is grouped under the table kind intentionally: in a multi-pass workflow (first pass emits CREATE TABLE + constraints, second pass adds only FKs), you don't want OWNER TO re-emitted in the second pass. When you say except_kinds => ARRAY['table'] you are explicitly asking for sub-object DDL only, and ownership is a table-level property. The owner flag says "include ownership if you're emitting table-level DDL"; it doesn't override the kind filter.

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
All,

A quick note on a design change I made in the pg_get_table_ddl option surface.

The earlier draft exposed one boolean option per sub-object kind: include_indexes, include_primary_key, include_check, include_foreign_keys, include_rules, include_statistics, include_rls, include_replica_identity, include_partitions, and so on. Every new kind we wanted to gate (triggers, policies, exclusion constraints…) meant another option name. Callers wanting "everything except FKs" had to flip nine flags, and the include/exclude polarity was not symmetric: to drop one item, you toggled one flag, but to keep only one item, you toggled all the others. 

I have replaced those with two options:
  - include — comma-separated list of kinds; emit only the listed ones. 
  - exclude — comma-separated list of kinds; emit everything except the listed ones.

Vocabulary: table, indexes, primary_key, unique, check, foreign_keys, exclusion, rules, statistics, triggers, policies, rls, replica_identity, partitions.
NOT NULL is intentionally omitted from the vocabulary; it's always emitted to avoid silently producing schemas that accept NULLs when the source would have rejected them. 

The v10 patch is ready for review.

On Tue, Jun 23, 2026 at 2:52 PM Akshay Joshi <akshay.joshi@enterprisedb.com> wrote:


On Tue, Jun 23, 2026 at 12:51 PM Kyotaro Horiguchi <horikyota.ntt@gmail.com> wrote:
At Mon, 22 Jun 2026 18:10:53 +0530, Akshay Joshi <akshay.joshi@enterprisedb.com> wrote in
> The v9 patch is ready for review.

I have not looked closely at the DDL generation logic itself, but I
have a few comments on how pg_get_table_ddl handles its options.

Since pg_get_table_ddl_internal() appears to copy these values into
TableDdlContext almost immediately, I wonder whether TableDdlContext
could be initialized by the caller instead.

Using positional boolean arguments is probably fine when there are
only a handful of options, but with around fifteen of them the current
approach seems somewhat error-prone.

It might also be clearer to initialize the default values first, and
then override only the fields corresponding to explicitly specified
options, rather than folding the default handling and option lookup
into the same expression.

I assume that changing the implementation model, as I mentioned in my other email, will solve this problem as well. We can drop the entire includes_* family and only_foreign_keys, and replace them with two mutually exclusive variadic keys:
include => 'kind1,kind2,...' — emit only these kinds
exclude => 'kind1,kind2,...' — emit everything except these kinds

Regards,

--
Kyotaro Horiguchi
NTT Open Source Software Center

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:


On Tue, Jul 14, 2026 at 4:31 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote:
Hello!

I did another big sweep/testing round. These are the issues found during it:

1. CLUSTER ON is emitted before ALTER TABLE ADD CONSTRAINT, and fails:

CREATE TABLE clu (a int, b int);
ALTER TABLE clu ADD CONSTRAINT clu_pk PRIMARY KEY (a);
CLUSTER clu USING clu_pk;

SELECT stmt FROM pg_get_table_ddl('clu', owner => false) stmt;

2. CONSTRAINT using index / tablespace dropped:

CREATE TABLE pko (a int, b int);
ALTER TABLE pko ADD CONSTRAINT pko_pk PRIMARY KEY (a) WITH
(fillfactor=50) USING INDEX TABLESPACE ts1;
ALTER TABLE pko ADD CONSTRAINT pko_u UNIQUE (b) WITH (fillfactor=60);

SELECT stmt FROM pg_get_table_ddl('pko', owner => false) stmt;
-- ALTER TABLE public.pko ADD CONSTRAINT pko_pk PRIMARY KEY (a);

3. SET STATISTICS is skipped:

CREATE TABLE cst (a int, b text);
ALTER TABLE cst ALTER COLUMN a SET STATISTICS 500;
ALTER TABLE cst ALTER COLUMN b SET STATISTICS 0;

SELECT stmt FROM pg_get_table_ddl('cst', owner => false) stmt;

and

CREATE TABLE stx (a int, b int);
CREATE STATISTICS stx_s (dependencies) ON a, b FROM stx;
ALTER STATISTICS stx_s SET STATISTICS 750;

SELECT stmt FROM pg_get_table_ddl('stx', owner => false) stmt;

4. toast.* parameteres also:

CREATE TABLE tst (a int, b text)
  WITH (toast.autovacuum_enabled = false, fillfactor = 70);

SELECT stmt FROM pg_get_table_ddl('tst', owner => false) stmt;

5. STORAGE / COMPRESSION overrides on inherited columns are skipped:

CREATE TABLE ps (a int, b text) PARTITION BY RANGE (a);
CREATE TABLE ps_1 PARTITION OF ps FOR VALUES FROM (0) TO (10);
ALTER TABLE ps_1 ALTER COLUMN b SET STORAGE EXTERNAL;
ALTER TABLE ps_1 ALTER COLUMN b SET COMPRESSION lz4;

SELECT stmt FROM pg_get_table_ddl('ps', owner => false) stmt;

6. Is it okay to error out on foreign table partitions, wouldn't a
warning and skipping it be better?

CREATE EXTENSION IF NOT EXISTS postgres_fdw;
CREATE SERVER loopback FOREIGN DATA WRAPPER postgres_fdw;
CREATE TABLE fpar (a int) PARTITION BY RANGE (a);
CREATE TABLE fpar_1 PARTITION OF fpar FOR VALUES FROM (0) TO (10);
CREATE FOREIGN TABLE fpar_2 PARTITION OF fpar FOR VALUES FROM (10) TO
(20) SERVER loopback;

SELECT stmt FROM pg_get_table_ddl('fpar') stmt;

It works with except_only partitions, but then it won't show any
partitions at all.

7. NOT NULL NOT VALID loses NOT VALID:

CREATE TABLE nnv (a int, b int);
ALTER TABLE nnv ADD CONSTRAINT nnv_a_nn NOT NULL a NOT VALID;

SELECT stmt FROM pg_get_table_ddl('nnv', owner => false) stmt;

8. custom index names are skipped:

CREATE TABLE cc (a int) PARTITION BY RANGE (a);
CREATE TABLE cc_1 PARTITION OF cc FOR VALUES FROM (0) TO (10);
CREATE INDEX ON ONLY cc (a);
CREATE INDEX cc_custom ON cc_1 (a);
ALTER INDEX cc_a_idx ATTACH PARTITION cc_custom;
ALTER TABLE cc_1 CLUSTER ON cc_custom;
SELECT stmt FROM pg_get_table_ddl('cc', owner => false) stmt;

And because of that replay fails, as CLUSTER ON refers to it.

9. explicit access method on partitioned parent is skipped:

CREATE TABLE pam (a int) PARTITION BY RANGE (a);
ALTER TABLE pam SET ACCESS METHOD heap;

SELECT stmt FROM pg_get_table_ddl('pam', owner => false) stmt;

10. tablespace => false doesn't remove tablespace from CREATE INDEX:

CREATE TABLE tsp (a int, b point) TABLESPACE ts1;
CREATE INDEX tsp_gist ON tsp USING gist (b) TABLESPACE ts1;

SELECT stmt FROM pg_get_table_ddl('tsp', owner => false, tablespace =>
false) stmt;

11. ALTER SEQUENCE is skipped:

CREATE TABLE idt (a int GENERATED BY DEFAULT AS IDENTITY);
ALTER SEQUENCE idt_a_seq AS smallint;

SELECT stmt FROM pg_get_table_ddl('idt', owner => false) stmt;

and

CREATE TABLE idl (a int GENERATED ALWAYS AS IDENTITY);
ALTER SEQUENCE idl_a_seq SET UNLOGGED;

SELECT stmt FROM pg_get_table_ddl('idl', owner => false) stmt;

   Fixed all the preceding issues. 

12. ALTER INDEX .. SET STATISTICS on indexes is skipped:

CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE exs (a int, b int);
ALTER TABLE exs ADD CONSTRAINT exs_ex EXCLUDE USING gist ((a + b) WITH =);
ALTER INDEX exs_ex ALTER COLUMN 1 SET STATISTICS 900;

SELECT stmt FROM pg_get_table_ddl('exs', owner => false) stmt;

Interestingly pg_dump also skips this, that looks like a pg_dump bug?
    The behavior was indeed inconsistent with what emit_indexes() already did for non-constraint indexes. emit_local_constraints() now emits ALTER INDEX ... ALTER COLUMN n SET STATISTICS for PK/UNIQUE/EXCLUSION constraint backing indexes.
    

13. SECURITY LABEL skipped, this probably only needs a documentation mention?

LOAD 'dummy_seclabel';
CREATE TABLE secl (a int);
SECURITY LABEL FOR dummy ON TABLE secl IS 'classified';
SECURITY LABEL FOR dummy ON COLUMN secl.a IS 'unclassified';

SELECT stmt FROM pg_get_table_ddl('secl', owner => false) stmt;

 Won't fix, doc-only: SECURITY LABEL requires a specific security-label provider (extension) to be loaded before the label can be applied. pg_get_table_ddl() has no way to know which provider will be present at replay time, and emitting DDL that references an unloaded provider would cause replay errors. This is analogous to the existing trigger and policy limitations. It should be documented as a known limitation: the generated DDL is complete for everything the server can reconstruct without provider-specific knowledge, and security labels should be noted as manually required. 

The v21 patch is ready for review.

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
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@gmail.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

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:


On Thu, Jun 11, 2026 at 6:38 PM Marcos Pegoraro <marcos@f10.com.br> wrote:
Em qui., 11 de jun. de 2026 às 04:48, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
 Fixed the issue above. The v5 patch is ready for review/testing.

One thing I noticed, though I'm not sure if it's the point here, is that it's not possible to extract only the foreign keys or only the triggers from the table. So if we want to extract the objects independently by type, we would need to have all the return types as optional, and we could have more granularity in the return types.

Just like you have...
if (!ctx->include_indexes)

You could have too
+ if (!ctx->include_create_table)
+ if (!ctx->include_foreign_keys)
+ if (!ctx->include_primary_keys)

Because only in this way can we more or less execute the dump behavior here, which is to create all the tables beforehand, then primary keys, then foreign keys, then triggers. 

I repeat, sorry if this is not the function's intended purpose.

I don't think per-contype flags are the right shape, though. The existing toggles group by catalog (indexes, constraints, rules, ...); splitting constraints into PK/FK/CHECK/UNIQUE/EXCLUDE/NOT NULL adds six flags on a second axis, and the function already carries nine. Only FKs have the cross-table dependency-ordering problem; the rest only reference the same table, so splitting them unlocks nothing new.

On include_create_table, we are reconstructing the DDL for the table itself, so I don't think we should skip the CREATE TABLE statement. I'd rather always emit CREATE TABLE.


regards
Marcos

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:


On Mon, Jun 22, 2026 at 7:30 PM Chao Li <li.evan.chao@gmail.com> wrote:


> On Jun 22, 2026, at 20:40, Akshay Joshi <akshay.joshi@enterprisedb.com> wrote:
>
> You're right, and thanks for spotting this. The existing pattern in pg_proc.dat for variadic-text functions (e.g., jsonb_delete, json_extract_path) uses _text at the variadic position in both proargtypes and proallargtypes, with provariadic => 'text'. That is the convention documented by the sanity check in src/test/regress/sql/opr_sanity.sql.
>
> The same issue applies to pg_get_role_ddl, pg_get_tablespace_ddl (both variants), and pg_get_database_ddl, but that will require a separate patch.
>

Thanks for confirming. Then I will file a patch tomorrow to fix those.

   I started working on it, but if you want to take the lead, just let me know and I won't send my version over 

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/




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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
Thanks for the review, Rui. Except for issue 1b), the others are fixed.
The v20 patch is ready for review.

On Fri, Jul 10, 2026 at 10:06 AM Rui Zhao <zhaorui126@gmail.com> wrote:
Hi Akshay,

Thanks for v19. I re-ran the pg_dump round-trip check (the 002_pg_dump.pl
scenarios plus the whole regression database) on current master (2e6578292a).
The five fixes hold up: SET STORAGE/COMPRESSION now use ONLY, invalid indexes
are skipped, SET STATISTICS / CLUSTER ON / DISABLE RULE are emitted, and the
child-default and named-NOT NULL cases I sent all round-trip now. 494 of 562
tables match pg_dump exactly (was 483/560 on v17). Three things came out of
the run below; the third is a side effect of that invalid-index skip.

1) The named-NOT NULL handling for inheritance/partition children still isn't
complete. It works when the child constraint has a user-defined name, but the
corpus turns up two ways it still goes wrong.

1a) For an auto-named inherited NOT NULL it emits an out-of-line ADD CONSTRAINT
that collides with the one INHERITS/PARTITION OF already propagates, giving
non-replayable DDL:

    CREATE TABLE nt (a int PRIMARY KEY);
    CREATE TABLE ntc (PRIMARY KEY (a) DEFERRABLE) INHERITS (nt);

    SELECT d FROM pg_get_table_ddl('ntc'::regclass, owner => false) d;
    --  CREATE TABLE public.ntc () INHERITS (public.nt);
    --  ALTER TABLE public.ntc ADD CONSTRAINT ntc_a_not_null NOT NULL a;
    --  ALTER TABLE public.ntc ADD CONSTRAINT ntc_pkey PRIMARY KEY (a)
DEFERRABLE;

    -- replaying that, with nt already restored first so INHERITS propagates
    -- its nt_a_not_null down to ntc:
    ERROR:  cannot create not-null constraint "ntc_a_not_null" on
column "a" of table "ntc"
    DETAIL:  A not-null constraint named "nt_a_not_null" already
exists for this column.

(notnull_tbl4_cld2 in the regression suite fails the same way.)

1b) Even where it does replay, a partition attached from a standalone table
keeps its name but comes back with the wrong constraint locality, because the
constraint is folded into PARTITION OF instead of a standalone CREATE + ATTACH:

    CREATE TABLE mp (a int NOT NULL) PARTITION BY LIST (a);
    CREATE TABLE mp1 (a int CONSTRAINT mp1_nn NOT NULL);
    ALTER TABLE mp ATTACH PARTITION mp1 FOR VALUES IN (1);
    -- source mp1.mp1_nn: conislocal = false (the ATTACH merged it
into the parent's)

    SELECT d FROM pg_get_table_ddl('mp'::regclass, owner => false) d;
    --  CREATE TABLE public.mp (a integer NOT NULL) PARTITION BY LIST (a);
    --  CREATE TABLE public.mp1 PARTITION OF public.mp (CONSTRAINT
mp1_nn NOT NULL a) FOR VALUES IN (1);

Declaring the constraint inline makes it local, so mp1.mp1_nn comes back with
conislocal = true, and the behavior diverges once the parent's NOT NULL is
dropped:

    ALTER TABLE mp ALTER a DROP NOT NULL;
    -- source: mp1.a is no longer NOT NULL; reconstructed: mp1.a stays NOT NULL

pg_dump reconstructs both faithfully. For reference, this is where it puts
each constraint:

  - In the CREATE TABLE body: NOT NULL, and validated CHECK.
  - As a separate ALTER TABLE ... ADD CONSTRAINT: PRIMARY KEY, UNIQUE, EXCLUDE,
    FOREIGN KEY, and NOT VALID CHECK.

The split is the same on plain tables, inheritance children and partitions;
only the table shape differs:

  - A plain-inheritance child keeps CREATE ... INHERITS and emits only its
    locally-owned constraints (purely-inherited NOT NULL/CHECK are left to
    INHERITS); the NOT NULL goes in the body unnamed so it auto-names, e.g.
    for the 1a case: CREATE TABLE ntc (NOT NULL a) INHERITS (nt); ...

  - A partition is never dumped with CREATE ... PARTITION OF; it is a standalone
    CREATE TABLE plus ALTER TABLE ONLY parent ATTACH PARTITION, so the child's
    column order, constraint names and conislocal come from its own definition
    plus the merge ATTACH performs.

That partition path is the same standalone+ATTACH that the reordered-column
case you deferred needs, so 1b likely folds into that follow-up. I'll
leave the exact approach to you.

2) A NOT VALID CHECK constraint loses its NOT VALID flag on round-trip. The
constraint is emitted inline in CREATE TABLE, where it is validated against the
(empty) table, so the reconstructed constraint is marked validated:

    CREATE TABLE nv (a int);
    ALTER TABLE nv ADD CONSTRAINT chk CHECK (a < 50) NOT VALID;

    SELECT d FROM pg_get_table_ddl('nv'::regclass, owner => false) d;
    --  CREATE TABLE public.nv (a integer, CONSTRAINT chk CHECK ((a <
50)) NOT VALID);

    -- source nv.chk:       convalidated = false
    -- reconstructed nv.chk: convalidated = true

pg_dump emits NOT VALID constraints as a separate ALTER TABLE ... ADD
CONSTRAINT ... NOT VALID for this reason.

3) The invalid-index skip added for the earlier finding also drops legitimate
partitioned indexes. emit_indexes now has:

    /* Skip invalid indexes; they may be left over from a failed
CREATE INDEX CONCURRENTLY. */
    if (!idxform->indisvalid)
    {
        ReleaseSysCache(indTup);
        continue;
    }

but a partitioned index on the parent is normally indisvalid = false (e.g. one
created with ON ONLY, or before all child indexes are attached), so the whole
index is lost:

    CREATE TABLE p (a int) PARTITION BY RANGE (a);
    CREATE TABLE p1 PARTITION OF p FOR VALUES FROM (0) TO (100);
    CREATE INDEX ON ONLY p (a);          -- p_a_idx.indisvalid = false

    SELECT d FROM pg_get_table_ddl('p'::regclass, owner => false) d;
    --  CREATE TABLE public.p (a integer) PARTITION BY RANGE (a);
    --  CREATE TABLE public.p1 PARTITION OF public.p FOR VALUES FROM
(0) TO (100);

No CREATE INDEX at all; pg_dump keeps it as "CREATE INDEX p_a_idx ON ONLY
public.p USING btree (a)". That's the pg_dump condition I quoted earlier --
"i.indisvalid OR t2.relkind = 'p'"; the relkind = 'p' half needs to stay so
partitioned indexes aren't filtered out along with the failed-CIC ones.

Thanks,
Rui

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:


On Mon, Jun 22, 2026 at 8:25 PM Marcos Pegoraro <marcos@f10.com.br> wrote:
Em seg., 22 de jun. de 2026 às 03:27, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
The documentation paragraph for `includes_foreign_keys` now directs users to `only_foreign_keys` as the intended second pass. Regression coverage adds three cases: the FK-only emission for your cons example, the zero-row result for a table without FKs, and the error path. 

I still think this model of only having options for foreign keys is incomplete, maybe wrong.
Imagine then cloning a schema from a publication server to be executed on a subscription server. So I don't want any other constraints besides the primary key, for example. The way you implemented it is not possible.

Furthermore having only_foreign_keys and includes_foreign_keys seems confuse. 

   OK. I'd like to change the model, not just the flag names. Drop the entire includes_* family and only_foreign_keys, replace them with two mutually-exclusive variadic keys:                      
                                                                                                                                                                                                           
  - include => 'kind1,kind2,...' — emit only these kinds
  - exclude => 'kind1,kind2,...' — emit everything except these                                                                                                                                            
                                                                                                                                                                                                           
  Setting both is an error. Setting neither emits everything (today's default behavior, which is preserved).                                                                                                                                         
  Vocabulary: indexes, primary_key, unique, check, foreign_keys, exclusion, rules, statistics, triggers, policies, rls, replica_identity, partitions. Unknown kind → parse-time error, which also      
  catches typos that the boolean version silently accepted.

  If everyone approves the model above, I'll try implementing it. 

regards
Marcos

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
Thanks for the review, I have fixed the mentioned issue.
The v15 patch is ready for review.

On Thu, Jul 2, 2026 at 3:50 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote:
I did some more testing, I noticed one more issue with self
referencing foreign keys:

CREATE TABLE t (id int PRIMARY KEY, parent_id int REFERENCES t(id));
SELECT pg_get_table_ddl('t'::regclass);
-- CREATE TABLE public.t (id integer NOT NULL, parent_id integer);
-- ALTER TABLE public.t OWNER TO postgres;
-- ALTER TABLE public.t ADD CONSTRAINT t_parent_id_fkey FOREIGN KEY
(parent_id) REFERENCES public.t(id);
-- ALTER TABLE public.t ADD CONSTRAINT t_pkey PRIMARY KEY (id);

It tries to add the foreign key before the primary, and fails with
`ERROR:  there is no unique constraint matching given keys for
referenced table "t"`

There's also another issue in schema_qualified false, with partitions
in different schemas:

CREATE SCHEMA s;
CREATE SCHEMA other;
CREATE TABLE s.pt (id int, val int) PARTITION BY RANGE (id);
CREATE TABLE other.pt_c PARTITION OF s.pt FOR VALUES FROM (0) TO (100);
SELECT pg_get_table_ddl('s.pt'::regclass, schema_qualified => false);
-- CREATE TABLE pt (id integer, val integer) PARTITION BY RANGE (id);
-- ALTER TABLE pt OWNER TO postgres;
-- CREATE TABLE pt_c PARTITION OF s.pt FOR VALUES FROM (0) TO (100);
-- ALTER TABLE pt_c OWNER TO postgres;

The second create table statement references pt as s.pt, which seems incorrect.
It is also missing its own schema qualification, which I'm unsure if
it is wrong or not. If I interpret the documentation strictly, it
isn't the target table, so it should appear with its schema
qualification?


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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
 Thanks for the continued review. I've rebased and updated the patch.
  
Andrew's commit replaced the VARIADIC text[] option interface on pg_get_role_ddl(), pg_get_tablespace_ddl(), and pg_get_database_ddl() with typed named boolean parameters. pg_get_table_ddl() now follows the exact same convention: pretty, owner, tablespace, and schema_qualified are plain boolean parameters with DEFAULT values, using PG_GETARG_BOOL() directly rather than parse_ddl_options().  The shared DdlOptType / DdlOption / parse_ddl_options infrastructure is gone entirely.

Filtering parameters: only_kinds / except_kinds as text[] 
Based on review feedback the filtering parameters(include/exclude) have been revised:
The two mutually-exclusive filter arguments are now named only_kinds and except_kinds (matching the SQL set-operation vocabulary) and take text[] rather than a comma-separated text value (e.g. only_kinds => ARRAY['index','foreign_key']).  This provides type safety and allows named-argument syntax. 

Note: only and except are the keywords, and I couldn't find any better name. Suggestions are welcome. 

The v14 patch is ready for review/test.

On Tue, Jun 30, 2026 at 3:01 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote:
Hello

I noticed that there's an inconsistency with schema qualification, if
the schema is in the search path:

CREATE SCHEMA s;
CREATE TABLE s.parent (id int PRIMARY KEY);
CREATE TABLE s.t (id int PRIMARY KEY, pid int REFERENCES s.parent(id),
name text);
CREATE INDEX t_name_idx ON s.t (name);
CREATE STATISTICS s.t_stat ON id, pid FROM s.t;
SET search_path = s, public;
SELECT pg_get_table_ddl('s.t', 'owner', 'false');

outputs:

CREATE TABLE s.t (id integer NOT NULL, pid integer, name text);
CREATE INDEX t_name_idx ON t USING btree (name); -- should be s.t
ALTER TABLE s.t ADD CONSTRAINT t_pid_fkey FOREIGN KEY (pid) REFERENCES
parent(id); -- should be s.parent
ALTER TABLE s.t ADD CONSTRAINT t_pkey PRIMARY KEY (id);
CREATE STATISTICS t_stat ON id, pid FROM t; -- should be s.t


In the included testcase:

+drop cascades to view v
+drop cascades to sequence s
+ERROR:  relation "parted_range_1" already exists
+CONTEXT:  SQL statement "CREATE TABLE pgtbl_ddl_test.parted_range_1
PARTITION OF pgtbl_ddl_test.parted_range FOR VALUES FROM (0) TO
(100);"
+PL/pgSQL function inline_code_block line 22 at EXECUTE

is this error expected, doesn't it break the test?


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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
You're right, and thanks for spotting this. The existing pattern in pg_proc.dat for variadic-text functions (e.g., jsonb_delete, json_extract_path) uses _text at the variadic position in both proargtypes and proallargtypes, with provariadic => 'text'. That is the convention documented by the sanity check in src/test/regress/sql/opr_sanity.sql.

The same issue applies to pg_get_role_ddl, pg_get_tablespace_ddl (both variants), and pg_get_database_ddl, but that will require a separate patch.

The v9 patch is ready for review.

On Mon, Jun 22, 2026 at 12:25 PM Chao Li <li.evan.chao@gmail.com> wrote:


> On Jun 22, 2026, at 14:26, Akshay Joshi <akshay.joshi@enterprisedb.com> wrote:
>
> Thanks for the review; you're right, `includes_foreign_keys=false` on its own is a half-measure. Re-running with the default to add FKs back collides with the existing CREATE TABLE, UNIQUE indexes, etc.
>
> I've added an only_foreign_keys option (boolean, default false) as the natural complement of includes_foreign_keys=false. When set to true, the function emits only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY statements and suppresses everything else (CREATE TABLE, owner, indexes, non-FK constraints, rules, statistics, replica identity, RLS toggles). Partition-child recursion still runs so child FKs are reached too. Combining `only_foreign_keys=true` with `includes_foreign_keys=false` is rejected upfront since it would produce no output.
>
> The documentation paragraph for `includes_foreign_keys` now directs users to `only_foreign_keys` as the intended second pass. Regression coverage adds three cases: the FK-only emission for your cons example, the zero-row result for a table without FKs, and the error path.
>
> The v8 patch is ready for review.
>
> On Sat, Jun 20, 2026 at 1:15 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote:
> The previous features all look good to me, I only have one question
> for the new flag.
>
> > Calling
> > pg_get_table_ddl(t, 'includes_foreign_keys', 'false') now emits everything
> > except FOREIGN KEY constraints. This covers the multi-tenant clone
> > workflow: create tables first without cross-table references, then re-run
> > with the default to add the constraints once all targets exist.
>
> I think this feature needs a bit more documentation, an
> "only_foreign_keys" flag, or both.
>
> CREATE TABLE refd (id int PRIMARY KEY);
> CREATE TABLE cons (a int CHECK(a>0), b int UNIQUE, c int REFERENCES refd(id));
>
> -- pass 1: running without foreign keys
> SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','false');
> -- execute everything
>
> -- loading data
>
> -- pass 2: running with everything
> SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','true');
> -- ERROR: relation "cons" already exists (and the unique constraint
> also collides)
>
> I could do a "grep FOREIGN KEY" before executing (unless it's a tricky
> schema where that phrase appears elsewhere), or since psql continues
> on error, it will simply work if I accept a significant error noise,
> but then the documentation should be clear about this limitation.
> Following the documented approach and getting a bunch of unexpected
> errors could be confusing for users.
>
>
> <v8-0001-Add-pg_get_table_ddl-to-reconstruct-CREATE-TABLE.patch>

I have a comment, or maybe a question:
```
+{ oid => '8215', descr => 'get DDL to recreate a table',
+  proname => 'pg_get_table_ddl', prorows => '50', provariadic => 'text',
+  proisstrict => 'f', proretset => 't', provolatile => 's', proparallel => 'r',
+  pronargdefaults => '1', prorettype => 'text',
+  proargtypes => 'regclass text', proallargtypes => '{regclass,text}',
+  proargmodes => '{i,v}', proargdefaults => '{NULL}',
+  prosrc => 'pg_get_table_ddl' },
```

Since provariadic is text, I wonder if proallargtypes should be {regclass,_text}, with _text meaning an array of text.

I’m asking because I have had this suspicion for some time. I saw a few other procs using the same pattern, for example:
```
{ oid => '6501', descr => 'get DDL to recreate a role',
  proname => 'pg_get_role_ddl', prorows => '10', provariadic => 'text',
  proisstrict => 'f', proretset => 't', provolatile => 's',
  pronargdefaults => '1', prorettype => 'text', proargtypes => 'regrole text',
  proallargtypes => '{regrole,text}', proargmodes => '{i,v}',
  proargdefaults => '{NULL}', prosrc => 'pg_get_role_ddl' },
```

But for jsonb_delete etc procs, _text is used:
```
{ oid => '3343',
  proname => 'jsonb_delete', provariadic => 'text', prorettype => 'jsonb',
  proargtypes => 'jsonb _text', proallargtypes => '{jsonb,_text}',
  proargmodes => '{i,v}', proargnames => '{from_json,path_elems}',
  prosrc => 'jsonb_delete_array' },
```

So I wonder whether “text” rather than “_text" is intentionally used in proallargtypes, or if this was just never noticed.

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/




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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
Thanks for the review; you're right, `includes_foreign_keys=false` on its own is a half-measure. Re-running with the default to add FKs back collides with the existing CREATE TABLE, UNIQUE indexes, etc.

I've added an only_foreign_keys option (boolean, default false) as the natural complement of includes_foreign_keys=false. When set to true, the function emits only the ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY statements and suppresses everything else (CREATE TABLE, owner, indexes, non-FK constraints, rules, statistics, replica identity, RLS toggles). Partition-child recursion still runs so child FKs are reached too. Combining `only_foreign_keys=true` with `includes_foreign_keys=false` is rejected upfront since it would produce no output.

The documentation paragraph for `includes_foreign_keys` now directs users to `only_foreign_keys` as the intended second pass. Regression coverage adds three cases: the FK-only emission for your cons example, the zero-row result for a table without FKs, and the error path. 

The v8 patch is ready for review.

On Sat, Jun 20, 2026 at 1:15 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote:
The previous features all look good to me, I only have one question
for the new flag.

> Calling
> pg_get_table_ddl(t, 'includes_foreign_keys', 'false') now emits everything
> except FOREIGN KEY constraints. This covers the multi-tenant clone
> workflow: create tables first without cross-table references, then re-run
> with the default to add the constraints once all targets exist.

I think this feature needs a bit more documentation, an
"only_foreign_keys" flag, or both.

CREATE TABLE refd (id int PRIMARY KEY);
CREATE TABLE cons (a int CHECK(a>0), b int UNIQUE, c int REFERENCES refd(id));

-- pass 1: running without foreign keys
SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','false');
-- execute everything

-- loading data

-- pass 2: running with everything
SELECT * FROM pg_get_table_ddl('cons','includes_foreign_keys','true');
-- ERROR: relation "cons" already exists (and the unique constraint
also collides)

I could do a "grep FOREIGN KEY" before executing (unless it's a tricky
schema where that phrase appears elsewhere), or since psql continues
on error, it will simply work if I accept a significant error noise,
but then the documentation should be clear about this limitation.
Following the documented approach and getting a bunch of unexpected
errors could be confusing for users.


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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
Thanks for the feedback, Zsolt! I've fixed the issues you pointed out.
The v22 patch is ready for review and testing.

On Thu, Jul 16, 2026 at 2:39 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote:
I can confirm that most fixes work correctly, but there are two
remaining issues / regressions caused by the fixes:

1.

CREATE TABLE pidx (a int, b int) PARTITION BY RANGE (a);
CREATE TABLE pidx_1 PARTITION OF pidx FOR VALUES FROM (0) TO (10);
CREATE TABLE pidx_2 PARTITION OF pidx FOR VALUES FROM (10) TO (20);
CREATE INDEX ON pidx (b);

is emitted as:

CREATE TABLE public.pidx (a integer, b integer) PARTITION BY RANGE (a);
CREATE INDEX pidx_b_idx ON ONLY public.pidx USING btree (b);
CREATE TABLE public.pidx_1 PARTITION OF public.pidx FOR VALUES FROM (0) TO (10);
CREATE INDEX pidx_1_b_idx ON public.pidx_1 USING btree (b);   <-- fails here
--  ERROR:  relation "pidx_1_b_idx" already exists
ALTER INDEX public.pidx_b_idx ATTACH PARTITION pidx_1_b_idx;
-- ...

2.

CREATE TABLE idt (a int GENERATED BY DEFAULT AS IDENTITY);
ALTER SEQUENCE idt_a_seq AS smallint;

is emitted as:

-- ERROR:  conflicting or redundant options
-- LINE 1: ....idt (a integer GENERATED BY DEFAULT AS IDENTITY (AS smallin...
CREATE TABLE public.idt (a integer GENERATED BY DEFAULT AS IDENTITY
(AS smallint MAXVALUE 32767) NOT NULL);


> > Interestingly pg_dump also skips this, that looks like a pg_dump bug?
>
> The behavior was indeed inconsistent with what emit_indexes() already
> did for non-constraint indexes. emit_local_constraints() now emits ALTER
> INDEX ... ALTER COLUMN n SET STATISTICS for PK/UNIQUE/EXCLUSION constraint
> backing indexes.

I submitted a matching patch for pg_dump:
https://www.postgresql.org/message-id/CAN4CZFMd9riOuV5LkM%2BuV%3DuF-HYp49C8Fh22xWyMxptGLBCZ3A%40mail.gmail.com

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
Fixed the documentation. v19 patch is now ready for review.

On Wed, Jul 8, 2026 at 7:29 PM Marcos Pegoraro <marcos@f10.com.br> wrote:
Em qua., 8 de jul. de 2026 às 08:56, Akshay Joshi <akshay.joshi@enterprisedb.com> escreveu:
The v18 patch is now ready for your review.

On SGML part, schema_qualified is a param which comes before only_kinds.
But the <para> explaining schema_qualified is the latest to be explained, why ?
Shouldn't it be placed right after tablespace ?

regards
Marcos

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
Thanks to Zsolt and Marcos for the review. 

Added schema_qualified (boolean, default true). When false, the target table is emitted unqualified everywhere (e.g., CREATE TABLE, ALTER TABLE, INHERITS, PARTITION OF, identity SEQUENCE NAME, etc.); same-schema sibling references follow suit; cross-schema references (e.g. FK targets in a different schema) remain qualified for correctness. Output from the always-qualified ruleutils helpers (pg_get_indexdef_string, pg_get_constraintdef_command, pg_get_ruledef, pg_get_statisticsobjdef_string) is post-processed to strip the base-schema prefix.

1) CHECK constraints on partition children are now emitted as ALTER TABLE … ADD CONSTRAINT … CHECK (…). They had been silently dropped because the PARTITION OF form has no column list to inline them into.                                                                                                                                                                                                    
2) Inherited generated columns no longer emit a spurious ALTER COLUMN … SET DEFAULT, which would fail at replay.
3) User-named NOT NULL constraints are now emitted inline as CONSTRAINT <name> NOT NULL. Auto-named NOT NULLs keep the existing inline-NOT NULL + ALTER TABLE dedup behaviour so the common-case output is unchanged.
4) Added regression coverage for the three bug fixes plus the schema_qualified=false paths (same-schema vs cross-schema, INHERITS/PARTITION OF parents, custom identity sequence name, replay into a different target schema)

Attached is the v3 patch, ready for review.

On Tue, Jun 9, 2026 at 2:42 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote:
Hello!

I did some basic testing with the new functions, and found a few bugs:

1. Seems like check constraints on partitions are ignored:

CREATE TABLE p (id int, val int) PARTITION BY RANGE (id);
CREATE TABLE p_child PARTITION OF p (CONSTRAINT chk_inline CHECK (val > 0))
    FOR VALUES FROM (0) TO (100);
SELECT * FROM pg_get_table_ddl('p_child', 'owner','false');

2. inherited stored generated columns can't be replayed:

CREATE TABLE par_s (
    id int,
    g int GENERATED ALWAYS AS (id * 2) STORED
);
CREATE TABLE ch_s () INHERITS (par_s);
SELECT * FROM pg_get_table_ddl('ch_s', 'owner','false');
-- CREATE TABLE public.ch_s () INHERITS (public.par_s);
-- ALTER TABLE public.ch_s ALTER COLUMN g SET DEFAULT (id * 2);

Dropping ch_s, executing the returned statements:

ERROR:  column "g" of relation "ch_s" is a generated column
HINT:  Use ALTER TABLE ... ALTER COLUMN ... SET EXPRESSION instead.

3. named not null constraints can't be replayed:

CREATE TABLE t (a int CONSTRAINT my_nn NOT NULL);
SELECT * FROM pg_get_table_ddl('t'::regclass,'owner','false');
-- CREATE TABLE public.t ( a integer NOT NULL);
-- ALTER TABLE public.t ADD CONSTRAINT my_nn NOT NULL a;

Dropping t, executing the statements:

ERROR:  cannot create not-null constraint "my_nn" on column "a" of table "t"
DETAIL:  A not-null constraint named "t_a_not_null" already exists for
this column.


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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
Hi Rui,

Thanks again for the review. I've included your patche.

The v26 patch is ready for review/test.

On Tue, Sep 8, 2026 at 8:49 PM Rui Zhao <zhaorui126@gmail.com> wrote:
Hi Akshay,

v25 applies to master (798bdcae89), builds warning-free, make check
244/244.  The three points from my last mail are all in: the doc note now
says the child constraint names are not preserved in either direction, the
schema_qualified => false script replays under search_path = sc with sb.yc
attached to sc.yp and sa.yp untouched, and the cross-schema ATTACH line is
in the expected output.

Round-trip against pg_dump, re-run on v25 and, alongside it, on v22 so
the two are like for like: 508 of the 563 top-level tables in the
regression database come back identical on both, and the same 55 tables
fall outside for the same reasons.  30 are objects the function doesn't
emit by design (triggers, grants, policies, comments, an owned sequence),
14 are the copy's DROP ... CASCADE taking a serial's sequence or a
function with it, and the remaining 11 are the partition child name and
column-order cases already deferred, three name collisions in the copy,
and one I hadn't looked at before:

1. An unlogged table's identity sequence that was SET LOGGED comes back
unlogged:

    CREATE UNLOGGED TABLE idu (a int GENERATED ALWAYS AS IDENTITY);
    ALTER SEQUENCE idu_a_seq SET LOGGED;
    SELECT d FROM pg_get_table_ddl('idu', owner => false) d;
    --  CREATE UNLOGGED TABLE public.idu (a integer GENERATED ALWAYS AS
    --      IDENTITY NOT NULL);

    -- replayed: idu_a_seq has relpersistence = u; the source has p

emit_identity_sequence_alterations() emits ALTER SEQUENCE ... SET UNLOGGED
when the sequence is unlogged (ddlutils.c:2990).  The sequence starts out
with the table's persistence, so the thing to test against is the table,
not LOGGED: on an unlogged table the sequence is unlogged by default and
the change worth emitting is the opposite one.  This is the mirror of
Zsolt's 11b, and the regression database has it (identity_dump_unlogged).
Attached 0001 compares against the table and emits SET LOGGED or SET
UNLOGGED accordingly, with the mirror test next to 11b.  With it the
round-trip is 509 of 563 and make check stays 244/244.

Two notes on the changes that aren't from my patches.

2. Switching the inline CHECKs to pg_get_constraintdef_body() also drops
PRETTYFLAG_INDENT, so a CHECK with a CASE no longer brings the deparser's
newlines into an otherwise single-line CREATE TABLE.  Good, and the
inline path now matches the table-level one; just noting it since the
change is visible and nothing in the test exercises it.

3. The ReleaseSysCache() calls added in front of ereport(ERROR) and
aclcheck_error() aren't needed.  The pin is registered with the resource
owner when the tuple is fetched, and the abort path releases it
(ResOwnerReleaseCatCache), which is what every other ereport(ERROR) with
a syscache tuple in hand relies on.  There are seven of these in
ddlutils.c now; harmless, just dead code.

From my side this is the last round.  With 0001 in, every table in the
regression database is accounted for, and v22 through v25 haven't moved
on that corpus in either direction.  I have no open items; I'll re-run
the round-trip on later versions only to check for regressions.

Regards,
Rui

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
Thank you for the thorough round-trip analysis; this caught real bugs. I have fixed several of the issues you highlighted, though a few fall outside this function's current scope.

Out of Scope (Documented in the Function Description)

Issue 1 (Serial Columns): Owned sequences are independent catalog objects. This is the same reason pg_get_table_ddl does not emit CREATE TYPE for composite types used in columns. While the nextval() default is correctly emitted, the sequence itself requires separate capture. I have added an explicit note to the documentation to call this out.

Issue 2c (Reordered Partition Columns): You correctly noted that this scenario requires a standalone CREATE + ATTACH PARTITION fallback. Detecting column-order divergence and falling back to the non-PARTITION OF syntax is a larger architecture change, so I have deferred it to a follow-up task. I have documented this as a known limitation for now.

Issue 6 (COMMENT/GRANT): These are intentionally omitted, consistent with how we handle triggers and policies. I have added them to the documented list of exclusions.

The v18 patch is now ready for your review.

On Tue, Jul 7, 2026 at 10:23 PM Rui Zhao <zhaorui126@gmail.com> wrote:
Hi Akshay,

Re-tested v17 on current master (73dfe79fd6) -- all five issues are fixed,
and make check passes here.

I then ran a round-trip check against pg_dump: for each table, replay the
pg_get_table_ddl() output into a clone of the database and diff
"pg_dump -t" of both sides (both sides being pg_dump output, any surviving
diff is semantic, not formatting). Corpus: the create_sql scenarios from
002_pg_dump.pl, then the whole regression database. 483 of 560 tables
round-trip identically; setting aside the documented no-output kinds
(trigger/policy), the rest reduce to:

1) serial columns produce non-replayable DDL -- the output references the
sequence but never creates it:

    CREATE TABLE ser (id serial, v text);

    SELECT d FROM pg_get_table_ddl('ser'::regclass, owner => false) d;
    --  CREATE TABLE public.ser (id integer DEFAULT
nextval('public.ser_id_seq'::regclass) NOT NULL, v text);

    CREATE TABLE public.ser (id integer DEFAULT
nextval('public.ser_id_seq'::regclass) NOT NULL, v text);
    --  ERROR:  relation "public.ser_id_seq" does not exist    (on an
empty database)

pg_dump emits CREATE SEQUENCE + OWNED BY + SET DEFAULT. This accounts for
14 regression failures, including whole partition trees.

2) Partition/inheritance children that diverged from their parent don't
survive the PARTITION OF / INHERITS rebuild. Three variants:

  - a dropped default silently comes back:

        CREATE TABLE dp (a int DEFAULT 99) PARTITION BY LIST (a);
        CREATE TABLE dp1 PARTITION OF dp FOR VALUES IN (1);
        ALTER TABLE ONLY dp1 ALTER COLUMN a DROP DEFAULT;

        SELECT d FROM pg_get_table_ddl('dp'::regclass, owner => false) d;
        --  CREATE TABLE public.dp (a integer DEFAULT 99) PARTITION BY LIST (a);
        --  CREATE TABLE public.dp1 PARTITION OF public.dp FOR VALUES IN (1);

        SELECT pg_get_expr(adbin, adrelid) FROM pg_attrdef
          WHERE adrelid = 'dp1'::regclass;
        --  source:   (0 rows)
        --  replayed: 99    => INSERTs into dp1 now get 99, not NULL

  - a child's own NOT NULL constraint name is lost:

        CREATE TABLE np (a int NOT NULL) PARTITION BY LIST (a);
        CREATE TABLE np1 (a int CONSTRAINT np1_nn NOT NULL);
        ALTER TABLE np ATTACH PARTITION np1 FOR VALUES IN (1);

        SELECT d FROM pg_get_table_ddl('np'::regclass, owner => false) d;
        --  CREATE TABLE public.np (a integer NOT NULL) PARTITION BY LIST (a);
        --  CREATE TABLE public.np1 PARTITION OF public.np FOR VALUES IN (1);

        SELECT conname FROM pg_constraint
          WHERE conrelid = 'np1'::regclass AND contype = 'n';
        --  source:   np1_nn
        --  replayed: np_a_not_null

    and when the column itself is inherited, the emitted constraint
    errors instead of merging:

        CREATE TABLE p5 (a int);
        CREATE TABLE c5 () INHERITS (p5);
        ALTER TABLE c5 ADD CONSTRAINT c5_nn NOT NULL a;
        ALTER TABLE p5 ADD CONSTRAINT p5_nn NOT NULL a;

        SELECT d FROM pg_get_table_ddl('c5'::regclass, owner => false) d;
        --  CREATE TABLE public.c5 () INHERITS (public.p5);
        --  ALTER TABLE public.c5 ADD CONSTRAINT c5_nn NOT NULL a;

        ALTER TABLE public.c5 ADD CONSTRAINT c5_nn NOT NULL a;    --
replay, p5 recreated first
        --  ERROR:  cannot create not-null constraint "c5_nn" on
column "a" of table "c5"
        --  DETAIL:  A not-null constraint named "p5_nn" already
exists for this column.

  - a partition attached from a table with different column order is
    rebuilt in the parent's order:

        CREATE TABLE p (a int, b int, c int) PARTITION BY LIST (a);
        CREATE TABLE c1 (c int, b int, a int);
        ALTER TABLE p ATTACH PARTITION c1 FOR VALUES IN (1);

        SELECT d FROM pg_get_table_ddl('p'::regclass, owner => false) d;
        --  CREATE TABLE public.p (a integer, b integer, c integer)
PARTITION BY LIST (a);
        --  CREATE TABLE public.c1 PARTITION OF public.p FOR VALUES IN (1);
        --  => replayed c1 columns are a, b, c (source: c, b, a), so
        --     SELECT * / COPY / positional INSERT all shift

pg_dump handles all three: it emits the named constraint inline in the
child's CREATE body (the merge with the inherited constraint at CREATE
time keeps the local name, and attislocal / conislocal / coninhcount all
survive), and falls back to standalone CREATE + ATTACH PARTITION for
shapes a PARTITION OF / INHERITS clause can't express. The first two
variants have lightweight fixes that keep the PARTITION OF / INHERITS
shape --

    CREATE TABLE c5 (CONSTRAINT c5_nn NOT NULL a) INHERITS (p5);
    CREATE TABLE np1 PARTITION OF np (CONSTRAINT np1_nn NOT NULL a)
FOR VALUES IN (1);

plus a counter-statement for the divergent-default case (ALTER TABLE
ONLY dp1 ALTER COLUMN a DROP DEFAULT). Only the reordered-column case
really needs the standalone shape. The commit message lists child-local
DEFAULT overrides and named NOT NULL constraints as supported, so I'm
treating these as bugs rather than scope cuts.

3) emit_typed_column_storage() (ddlutils.c:2331, 2342) emits ALTER TABLE
without ONLY; SET STORAGE / SET COMPRESSION recurse:

    CREATE TYPE mytype AS (a int, b text);
    CREATE TABLE typed_t OF mytype;
    ALTER TABLE typed_t ALTER COLUMN b SET STORAGE external;
    CREATE TABLE tchild () INHERITS (typed_t);
    ALTER TABLE ONLY tchild ALTER COLUMN b SET STORAGE main;

    ALTER TABLE public.typed_t ALTER COLUMN b SET STORAGE EXTERNAL;
-- replay v17's output
    --  => tchild's attstorage flips m -> e

pg_dump uses ALTER TABLE ONLY here. (The other emitted ALTER TABLEs are
fine: OWNER / REPLICA IDENTITY / RLS / SET (options) don't recurse, and
ADD CONSTRAINT must stay ONLY-less since the dump relies on its recursion
to rebuild the children's suppressed inherited copies.)

4) Some per-table state pg_dump preserves is missing. Index statistics
targets:

    CREATE TABLE ist (c1 int);
    CREATE INDEX ist_idx ON ist ((c1 + 1));
    ALTER INDEX ist_idx ALTER COLUMN 1 SET STATISTICS 400;

    SELECT d FROM pg_get_table_ddl('ist'::regclass, owner => false) d;
    --  CREATE TABLE public.ist (c1 integer);
    --  CREATE INDEX ist_idx ON public.ist USING btree (((c1 + 1)));
 -- no SET STATISTICS

Same story for ALTER TABLE ... CLUSTER ON (the index comes back without
the indisclustered marker) and for ALTER TABLE ... DISABLE RULE (the rule
is emitted but comes back enabled, which changes behavior -- and rule is
a covered kind).

5) emit_indexes doesn't check indisvalid, so an invalid index is emitted
as a normal one:

    CREATE TABLE inv (x int);
    INSERT INTO inv VALUES (1), (1);
    CREATE UNIQUE INDEX CONCURRENTLY inv_uidx ON inv (x);
    --  ERROR:  could not create unique index "inv_uidx"    (leaves
indisvalid = false)

    SELECT d FROM pg_get_table_ddl('inv'::regclass, owner => false) d;
    --  CREATE TABLE public.inv (x integer);
    --  CREATE UNIQUE INDEX inv_uidx ON public.inv USING btree (x);

pg_dump skips those (getIndexes: "i.indisvalid OR t2.relkind = 'p'").

6) Minor: COMMENT ON and GRANT/REVOKE are not emitted. If that's
intentional -- like trigger/policy -- worth saying so in the doc.

Thanks,
Rui

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

От:
Akshay Joshi <akshay.joshi@enterprisedb.com>
Дата:
Thanks Zsolt for the review. I have tried to fix most of the edge cases. I added 12 new test cases.
The v16 patch is ready for review.
Note: Windows-MinGW-Meson builds are failing but not because of this patch.

On Fri, Jul 3, 2026 at 4:04 AM Zsolt Parragi <zsolt.parragi@percona.com> wrote:
Hello!

I can confirm the previous issues fixed, however I also found one more
with unique indexes on partitioned tables:

CREATE SCHEMA s;
CREATE TABLE s.p (id int, region text) PARTITION BY LIST (region);
CREATE TABLE s.p_a PARTITION OF s.p FOR VALUES IN ('a');
CREATE UNIQUE INDEX p_uidx ON s.p (id, region);

SELECT pg_get_table_ddl('s.p', owner => false);
-- CREATE TABLE s.p (id integer, region text) PARTITION BY LIST (region);
-- CREATE UNIQUE INDEX p_uidx ON s.p USING btree (id, region);
-- CREATE TABLE s.p_a PARTITION OF s.p FOR VALUES IN ('a');
-- CREATE UNIQUE INDEX p_a_id_region_idx ON s.p_a USING btree (id,
region); -- fails because index already exists


FAQ