create table like including storage parameter

First seen: 2025-09-29 00:00:00+00:00 · Messages: 32 · Participants: 9

Latest Update

2026-05-07 · opus 4.7

New in this round: Henson Choi's line-by-line review

A single new message from Henson Choi (assam258@gmail.com) — the same reviewer who earlier delivered the 96.8% coverage report. This round is a non-blocking polish review with mostly cosmetic items, but two substantive points deserve noting:

Substantive items

  1. Test isolation bug (item [1]). create_table_like.sql references trigger_func without creating it locally, depending on triggers.sql having run earlier via parallel_schedule. This works for make check but breaks make check TESTS=create_table_like. This violates the PostgreSQL convention that every regression file be self-contained. It's a real defect, not cosmetic — isolated test runs are a common developer workflow.

  2. Missing EXCLUDING TRIGGERS coverage (item [2]). The grammar accepts it but no test exercises it. Since EXCLUDING is the default, behavioral equivalence is trivial, but a smoke test is warranted to lock down keyword acceptance.

  3. Whole-row WHEN restriction — deliberate or gap? (item [7]). Henson is probing whether the rejection of OLD.*/NEW.* in WHEN clauses is a hard semantic limitation or merely an implementation shortcut. He offers a concrete future direction: pass the target table's composite type OID as to_rowtype to enable remapping. He requests an XXX comment at the rejection site if this is a known gap. This is the only item with forward-looking technical content — it reframes the earlier-analyzed restriction as potentially liftable rather than fundamental.

Confirmation items (reviewer validating prior design choices)

Pure cosmetics (items 3–6)

Documentation wording mismatch between create_table.sgml ("will be created on the new table") and create_foreign_table.sgml ("are copied to the new table") — latter matches the house style used by INCLUDING STATISTICS etc. Two comment-capitalization/verbosity nits in trigger.c and parse_utilcmd.c. Commit message grammar cleanup (singular/plural, "won't being copied", missing comma).

Reviewer's verdict

"Patch is in good shape… none of these are blockers." This is effectively a green light pending small touch-ups. No new objections to the architecture, no revisiting of the RangeVar-vs-Oid debate, no pushback on the INCLUDING PARAMETERS naming. The thread appears to be converging toward commit-readiness.

History (1 prior analysis)
2026-05-06 · opus 4.7

CREATE TABLE LIKE: INCLUDING PARAMETERS and INCLUDING TRIGGERS

Core Problem

CREATE TABLE ... (LIKE source_table INCLUDING ...) is PostgreSQL's mechanism for cloning a table's definitional attributes to a new table. Prior to this work, the INCLUDING clause supported attributes like DEFAULTS, CONSTRAINTS, INDEXES, STATISTICS, STORAGE (column-level storage like PLAIN/EXTERNAL/EXTENDED), COMMENTS, COMPRESSION, GENERATED, and IDENTITY — but two conspicuous gaps remained:

  1. Table-level storage parameters (reloptions in pg_class, e.g. fillfactor, autovacuum_*, toast.*, parallel_workers) were not copyable. A user cloning a carefully tuned table had to manually re-specify every WITH (...) option.
  2. Triggers were not copyable. Unlike indexes and constraints (which have their own generateClonedIndexStmt/constraint cloning machinery), triggers had no parallel path through expandTableLikeClause.

This thread proposes two patches addressing each gap. They are related because both need the same catalog-reconstruction pattern: decompile a stored object back into a parse node, then re-execute the creation DDL against the new target relation.

Design Decision #1: Syntax for Storage Parameters

The initial proposal used INCLUDING STORAGE PARAMETER, explicitly disambiguated from the existing INCLUDING STORAGE (which copies per-column attstorage). David G. Johnston pushed back: since the set of copyable table-level options might grow beyond storage (e.g., planner-related options), qualifying the name with "STORAGE" was misleading. The final chosen syntax is INCLUDING PARAMETERS, generic enough to subsume future non-storage table options.

Jian's initial objection was pragmatic — PARAMETERS would require a new keyword — but he eventually reclassified it as an unreserved, bare-label keyword, which does not break existing SQL.

Notably, INCLUDING PARAMETERS is silently ignored for foreign tables (they have no reloptions in the traditional sense) and is rejected for views because view reloptions (check_option, security_barrier) have a disjoint namespace from table reloptions — copying check_option=local into a plain table would produce "unrecognized parameter". Jian correctly diagnosed that the relkind filter in transformTableLikeClause is too permissive for this specific copy operation.

Design Decision #2: Code Reuse via untransformRelOptions

Euler Taveira's review caught that Jian's v5 duplicated the logic that converts a Datum array of reloption text entries back into a List * of DefElem — logic that already exists as untransformRelOptions(). Even though that function is widely used by extensions (a weak argument for duplication), Euler insisted on reuse. This is a routine but important catalog-decompilation pattern: reloptions are stored as text[] of "name=value" strings, and any consumer reconstructing a DDL statement must parse them back.

Design Decision #3: Trigger Cloning — The RangeVar vs. Oid Tension

The most architecturally interesting debate concerns the trigger patch. Jian's v2/v3 added Oid fields (relOid, refRelOid) directly into CreateTrigStmt to avoid name re-resolution when cloning. Andrey Borodin (x4mmm) objected on principle:

"Parse Nodes seem to use 'textual' identifiers without resolving them to Oids. Oids are specific to session catalog snapshot... Other INCLUDING options (indexes, constraints) don't modify their statement nodes this way — they create fresh nodes with resolved references."

Robert Haas (committer) weighed in decisively. He acknowledged the existing pattern is "incredibly ugly" — translating OID → name → OID through parse-node reconstruction is bug-prone, and not just for table names but also AM names, tablespaces, and column names. But his verdict was architectural conservatism:

"It's better to use the same ugly hack consistently than to use different ugly hacks in different places. Then if somebody ever decides to try to clean this up, they can clean up all the cases in the same way."

This is a significant meta-principle for PostgreSQL internals work: do not introduce a locally superior pattern that fragments a system-wide ugly pattern, because cleanup becomes impossible. Jian dropped v4-0001/0002 (the Oid-injection refactors) and conformed to the existing generateClonedIndexStmt-style pattern, which relies on transformCreateStmt having forcibly schema-qualified the target RangeVar upstream in ProcessUtilitySlow.

Design Decision #4: What Exactly Gets Copied for Triggers

Several subtle semantic choices emerged from review:

  • Internal triggers (FK-associated) are skipped. These are owned by pg_constraint entries, not user DDL, so cloning them would produce orphaned triggers referencing nonexistent constraints.
  • INSTEAD OF triggers on views cannot be cloned onto a plain table (rejected with error) since they're semantically view-specific.
  • Whole-row references in WHEN clauses are rejected. This is because WHEN (OLD IS NOT NULL) encodes a specific relation's rowtype; re-parse-analyzing it against a new table is unsafe.
  • tgenabled state (origin/replica/always/disabled). Initially Jian proposed documenting that enabled state is not preserved. Zsolt Parragi pushed back citing the INCLUDING CONSTRAINTS precedent — constraint enforced/not-enforced state is preserved, so asymmetry would be surprising. Jian accepted and added v6-0002 to copy tgenabled.
  • Trigger comments required adding a trigcomment field to CreateTrigStmt, mirroring the existing pattern of stxcomment in CreateStatsStmt and idxcomment in IndexStmt. Comments are only copied when INCLUDING COMMENTS is specified.
  • Partition triggers are not recursively cloned — only triggers on the partitioned table itself.

Design Decision #5: Column-Level Attribute Options

Jian asked whether ALTER TABLE ... ALTER COLUMN ... SET (n_distinct=...) options should also be copied under INCLUDING PARAMETERS. Euler rejected this: n_distinct and n_distinct_inherited are planner hints tied to the source table's actual data distribution. Propagating them to a fresh (empty) table would risk enshrining bad plans. This is a thoughtful distinction between structural parameters (fillfactor, autovacuum thresholds) and data-derived parameters (statistics overrides).

Refactoring of CreateTrigger

A secondary refactor removes the transformation of WHEN clauses during cloning. The stored pg_trigger.tgqual is already a transformed expression tree; re-running parse analysis on it (as CreateTrigger normally does for fresh DDL) is both redundant and incorrect. This mirrors transformStatsStmt's handling for extended statistics. A transformed boolean flag on CreateTrigStmt signals "skip parse analysis of whenClause". The whenClause parameter of CreateTrigger was also noted as always-NULL at call sites, suggesting future simplification.

Testing and Coverage

An independent quality review (assam258@gmail.com) reported 96.8% line coverage on modified code in trigger.c and parse_utilcmd.c. The uncovered lines are catalog-inconsistency elog guards (untestable without corruption) and the FK-internal-trigger skip path — for which a concrete test was suggested. Documentation was flagged as too terse, omitting mention of tgenabled preservation, the whole-row WHEN restriction, and the INSTEAD OF restriction.

Architectural Significance

These patches look small but exercise a recurring PostgreSQL idiom: catalog round-tripping for object cloning. Every time PostgreSQL adds a "copy this structural attribute" feature, it must:

  1. Fetch the stored form from system catalogs.
  2. Decompile it back into a parse-tree representation (generateClonedXxxStmt).
  3. Re-execute the DDL against a new target, relying on ProcessUtilitySlow having fully qualified names.

The trigger patch exposes the fragility of this pattern (the Oid-vs-name debate) and Robert Haas's principle that consistency with the existing bad pattern beats local improvement — a worthwhile lesson in incremental system evolution.