From 5e4b5dbfd99e13df5350185b5ec190dd97dfd205 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Wed, 20 Aug 2025 14:30:41 -0400 Subject: [PATCH 1/4] working pg_get_table_ddl but with some limitations particularly around partitions and FK constraints. --- src/backend/commands/tablecmds.c | 19 + src/backend/utils/adt/ruleutils.c | 617 ++++++++++++++++++++++++++++++ src/include/catalog/pg_proc.dat | 4 +- src/include/commands/tablecmds.h | 1 + 4 files changed, 640 insertions(+), 1 deletion(-) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 082a3575d62..29990ddc96e 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -19272,6 +19272,25 @@ remove_on_commit_action(Oid relid) } } +/* + * Return the ON COMMIT action set for a relation, or NOOP if none + * is registered +*/ +OnCommitAction +get_on_commit_action(Oid relid) +{ + ListCell *l; + + foreach(l, on_commits) + { + OnCommitItem *oc = (OnCommitItem *) lfirst(l); + + if (oc->relid == relid) + return oc->oncommit; + } + return ONCOMMIT_NOOP; +} + /* * Perform ON COMMIT actions. * diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 3d6e6bdbfd2..a182b852cdc 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -20,15 +20,20 @@ #include #include "access/amapi.h" +#include "access/heaptoast.h" #include "access/htup_details.h" #include "access/relation.h" #include "access/table.h" +#include "access/toast_compression.h" +#include "catalog/partition.h" #include "catalog/pg_aggregate.h" #include "catalog/pg_am.h" +#include "catalog/pg_attrdef.h" #include "catalog/pg_authid.h" #include "catalog/pg_collation.h" #include "catalog/pg_constraint.h" #include "catalog/pg_depend.h" +#include "catalog/pg_inherits.h" #include "catalog/pg_language.h" #include "catalog/pg_opclass.h" #include "catalog/pg_operator.h" @@ -38,6 +43,7 @@ #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" #include "commands/defrem.h" +#include "commands/tablecmds.h" #include "commands/tablespace.h" #include "common/keywords.h" #include "executor/spi.h" @@ -13707,3 +13713,614 @@ get_range_partbound_string(List *bound_datums) return buf->data; } + +/* + * pg_get_table_ddl - Generate CREATE TABLE DDL for a given relation + * + * This function should be added to src/backend/utils/adt/ruleutils.c + */ + +/* Function declaration for ruleutils.c */ +Datum pg_get_table_ddl(PG_FUNCTION_ARGS); + +/* + * pg_get_table_ddl + * + * Returns the CREATE TABLE DDL statement for the specified table. + * Handles regular, temporary, unlogged, and partitioned tables. + */ +Datum +pg_get_table_ddl(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + StringInfoData buf; + Relation rel; + TupleDesc tupdesc; + Form_pg_class relform; + char *tablename; + char *schemaname; + char *tablespacename = NULL; + bool first_col = true; + int i; + + /* Open the relation */ + rel = relation_open(relid, AccessShareLock); + relform = RelationGetForm(rel); + + /* Only handle tables and partitioned tables */ + if (relform->relkind != RELKIND_RELATION && + relform->relkind != RELKIND_PARTITIONED_TABLE) + { + relation_close(rel, AccessShareLock); + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("relation \"%s\" is not a table", + RelationGetRelationName(rel)))); + } + + /* Get tuple descriptor */ + tupdesc = RelationGetDescr(rel); + + initStringInfo(&buf); + + /* Get table and schema names */ + tablename = RelationGetRelationName(rel); + schemaname = get_namespace_name(RelationGetNamespace(rel)); + + /* Start building the CREATE TABLE statement */ + appendStringInfo(&buf, "CREATE "); + + /* Add table type modifiers */ + if (relform->relpersistence == RELPERSISTENCE_UNLOGGED) + appendStringInfoString(&buf, "UNLOGGED "); + else if (relform->relpersistence == RELPERSISTENCE_TEMP) + appendStringInfoString(&buf, "TEMPORARY "); + + appendStringInfoString(&buf, "TABLE "); + + /* Add schema-qualified table name */ + appendStringInfo(&buf, "%s.%s", + quote_identifier(schemaname), + quote_identifier(tablename)); + + /* Handle typed tables */ + if (OidIsValid(relform->reloftype)) + { + /* For typed tables, we don't need full column definitions, + * just any local columns or constraints */ + bool has_local_columns = false; + char *typename = format_type_be(relform->reloftype); + appendStringInfo(&buf, " OF %s", typename); + + /* Check if there are any local columns beyond the type */ + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + if (attr->attisdropped || attr->attinhcount > 0) + continue; + has_local_columns = true; + break; + } + + if (has_local_columns || (rel->rd_att->constr && rel->rd_att->constr->num_check > 0)) + { + appendStringInfoString(&buf, "\n(\n"); + first_col = true; + + /* Add any local columns */ + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + + if (attr->attisdropped || attr->attinhcount > 0) + continue; + + if (!first_col) + appendStringInfoString(&buf, ",\n"); + first_col = false; + + /* Only show local modifications/constraints for this column */ + appendStringInfo(&buf, " %s", quote_identifier(NameStr(attr->attname))); + + if (attr->attnotnull) + appendStringInfoString(&buf, " NOT NULL"); + + if (attr->atthasdef) + { + AttrDefault *defval = NULL; + + if (rel->rd_att->constr && rel->rd_att->constr->defval) + { + for (int j = 0; j < rel->rd_att->constr->num_defval; j++) + { + if (rel->rd_att->constr->defval[j].adnum == attr->attnum) + { + defval = &rel->rd_att->constr->defval[j]; + break; + } + } + } + + if (defval) + appendStringInfo(&buf, " DEFAULT %s", + text_to_cstring(pg_get_expr_worker(cstring_to_text(defval->adbin), relid, 0))); + } + } + + /* Add CHECK constraints for typed tables */ + if (rel->rd_att->constr && rel->rd_att->constr->num_check > 0) + { + for (i = 0; i < rel->rd_att->constr->num_check; i++) + { + ConstrCheck *check = &rel->rd_att->constr->check[i]; + if (!first_col) + appendStringInfoString(&buf, ",\n"); + first_col = false; + appendStringInfo(&buf, " CONSTRAINT %s CHECK (%s)", + quote_identifier(check->ccname), + text_to_cstring(pg_get_expr_worker(cstring_to_text(check->ccbin), relid, 0))); + } + } + + appendStringInfoString(&buf, "\n)"); + } + } + /* Handle partition tables - only show local columns if any */ + else if (rel->rd_rel->relispartition) + { + /* For partitions, we typically don't show inherited columns, + * only local additions or modifications */ + bool has_local_additions = false; + + /* Check for any local columns or constraints */ + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + if (attr->attisdropped) + continue; + /* Check if this is a local column (not inherited) */ + if (attr->attinhcount == 0) + { + has_local_additions = true; + break; + } + } + + if (has_local_additions || (rel->rd_att->constr && rel->rd_att->constr->num_check > 0)) + { + appendStringInfoString(&buf, "\n(\n"); + first_col = true; + + /* Add local columns only */ + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + char *typename_with_typemod; + + if (attr->attisdropped || attr->attinhcount > 0) + continue; + + if (!first_col) + appendStringInfoString(&buf, ",\n"); + first_col = false; + + /* Full column definition for local columns */ + appendStringInfo(&buf, " %s", quote_identifier(NameStr(attr->attname))); + + typename_with_typemod = format_type_with_typemod(attr->atttypid, attr->atttypmod); + appendStringInfo(&buf, " %s", typename_with_typemod); + + if (attr->attnotnull) + appendStringInfoString(&buf, " NOT NULL"); + + if (attr->atthasdef) + { + AttrDefault *defval = NULL; + + if (rel->rd_att->constr && rel->rd_att->constr->defval) + { + for (int j = 0; j < rel->rd_att->constr->num_defval; j++) + { + if (rel->rd_att->constr->defval[j].adnum == attr->attnum) + { + defval = &rel->rd_att->constr->defval[j]; + break; + } + } + } + + if (defval) + appendStringInfo(&buf, " DEFAULT %s", + text_to_cstring(pg_get_expr_worker(cstring_to_text(defval->adbin), relid, 0))); + } + } + + /* Add local CHECK constraints */ + if (rel->rd_att->constr && rel->rd_att->constr->num_check > 0) + { + for (i = 0; i < rel->rd_att->constr->num_check; i++) + { + ConstrCheck *check = &rel->rd_att->constr->check[i]; + if (!first_col) + appendStringInfoString(&buf, ",\n"); + first_col = false; + appendStringInfo(&buf, " CONSTRAINT %s CHECK (%s)", + quote_identifier(check->ccname), + text_to_cstring(pg_get_expr_worker(cstring_to_text(check->ccbin), relid, 0))); + } + } + + appendStringInfoString(&buf, "\n)"); + } + } + /* Handle regular tables and partitioned tables */ + else + { + appendStringInfoString(&buf, "\n(\n"); + + /* Process each column */ + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + char *typename_with_typemod; + bool notnull; + char storage_type; + + /* Skip dropped columns */ + if (attr->attisdropped) + continue; + + if (!first_col) + appendStringInfoString(&buf, ",\n"); + first_col = false; + + /* Column name */ + appendStringInfo(&buf, " %s", quote_identifier(NameStr(attr->attname))); + + /* Data type with type modifiers */ + typename_with_typemod = format_type_with_typemod(attr->atttypid, attr->atttypmod); + appendStringInfo(&buf, " %s", typename_with_typemod); + + /* Collation */ + if (OidIsValid(attr->attcollation)) + { + HeapTuple coll_tuple; + Form_pg_collation coll_form; + + coll_tuple = SearchSysCache1(COLLOID, ObjectIdGetDatum(attr->attcollation)); + if (HeapTupleIsValid(coll_tuple)) + { + /* Only show collation if it's not the default for the type */ + Oid default_collation = get_typcollation(attr->atttypid); + + coll_form = (Form_pg_collation) GETSTRUCT(coll_tuple); + + if (attr->attcollation != default_collation) + { + char *coll_namespace = get_namespace_name(coll_form->collnamespace); + if (coll_namespace && strcmp(coll_namespace, "pg_catalog") != 0) + appendStringInfo(&buf, " COLLATE %s.%s", + quote_identifier(coll_namespace), + quote_identifier(NameStr(coll_form->collname))); + else + appendStringInfo(&buf, " COLLATE %s", + quote_identifier(NameStr(coll_form->collname))); + } + ReleaseSysCache(coll_tuple); + } + } + + /* NOT NULL constraint */ + notnull = attr->attnotnull; + if (notnull) + appendStringInfoString(&buf, " NOT NULL"); + + /* Default expression */ + if (attr->atthasdef) + { + AttrDefault *defval = NULL; + + if (rel->rd_att->constr && rel->rd_att->constr->defval) + { + for (int j = 0; j < rel->rd_att->constr->num_defval; j++) + { + if (rel->rd_att->constr->defval[j].adnum == attr->attnum) + { + defval = &rel->rd_att->constr->defval[j]; + break; + } + } + } + + if (defval) + { + appendStringInfo(&buf, " DEFAULT %s", + text_to_cstring(pg_get_expr_worker(cstring_to_text(defval->adbin), relid, 0))); + } + } + + /* Storage type */ + storage_type = attr->attstorage; + if (storage_type != get_typstorage(attr->atttypid)) + { + switch (storage_type) + { + case 'p': + appendStringInfoString(&buf, " STORAGE PLAIN"); + break; + case 'e': + appendStringInfoString(&buf, " STORAGE EXTERNAL"); + break; + case 'm': + appendStringInfoString(&buf, " STORAGE MAIN"); + break; + case 'x': + appendStringInfoString(&buf, " STORAGE EXTENDED"); + break; + } + } + + /* Compression method (PostgreSQL 14+) */ + if (attr->attcompression != InvalidCompressionMethod) + { + const char *compression_name = GetCompressionMethodName(attr->attcompression); + if (compression_name) + appendStringInfo(&buf, " COMPRESSION %s", compression_name); + } + } + + /* Add table constraints (PRIMARY KEY, UNIQUE, CHECK, etc.) */ + if (rel->rd_att->constr && rel->rd_att->constr->num_check > 0) + { + for (i = 0; i < rel->rd_att->constr->num_check; i++) + { + ConstrCheck *check = &rel->rd_att->constr->check[i]; + appendStringInfo(&buf, ",\n CONSTRAINT %s CHECK (%s)", + quote_identifier(check->ccname), + text_to_cstring(pg_get_expr_worker(cstring_to_text(check->ccbin), relid, 0))); + } + } + + /* Add indexes as constraints (PRIMARY KEY, UNIQUE) */ + { + List *indexoidlist = RelationGetIndexList(rel); + ListCell *l; + + foreach(l, indexoidlist) + { + Oid indexoid = lfirst_oid(l); + Relation indexrel; + Form_pg_index indexform; + HeapTuple indexTuple; + + indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid)); + if (!HeapTupleIsValid(indexTuple)) + continue; + + indexform = (Form_pg_index) GETSTRUCT(indexTuple); + + if (indexform->indisprimary || indexform->indisunique) + { + indexrel = relation_open(indexoid, AccessShareLock); + + if (indexform->indisprimary) + appendStringInfoString(&buf, ",\n PRIMARY KEY ("); + else + appendStringInfoString(&buf, ",\n UNIQUE ("); + + /* Add index column names */ + for (int j = 0; j < indexform->indnatts; j++) + { + int16 attnum = indexform->indkey.values[j]; + char *attname = get_attname(relid, attnum, false); + + if (j > 0) + appendStringInfoString(&buf, ", "); + appendStringInfo(&buf, "%s", quote_identifier(attname)); + } + appendStringInfoString(&buf, ")"); + + relation_close(indexrel, AccessShareLock); + } + + ReleaseSysCache(indexTuple); + } + + list_free(indexoidlist); + } + + appendStringInfoString(&buf, "\n)"); + } + + /* Add INHERITS clause for inheritance (not partitioning) */ + if (rel->rd_rel->relhassubclass == false && rel->rd_rel->relispartition == false) + { + Relation catalogrel; + SysScanDesc scan; + ScanKeyData key; + HeapTuple inheritsTuple; + bool first_parent = true; + + catalogrel = table_open(InheritsRelationId, AccessShareLock); + + ScanKeyInit(&key, + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + + scan = systable_beginscan(catalogrel, InheritsRelidSeqnoIndexId, + true, NULL, 1, &key); + + while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) + { + Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(inheritsTuple); + char *parent_name; + char *parent_schema; + + if (first_parent) + { + appendStringInfoString(&buf, " INHERITS ("); + first_parent = false; + } + else + appendStringInfoString(&buf, ", "); + + parent_name = get_rel_name(inh->inhparent); + parent_schema = get_namespace_name(get_rel_namespace(inh->inhparent)); + + if (parent_schema && strcmp(parent_schema, "public") != 0) + appendStringInfo(&buf, "%s.%s", + quote_identifier(parent_schema), + quote_identifier(parent_name)); + else + appendStringInfo(&buf, "%s", quote_identifier(parent_name)); + } + + if (!first_parent) + appendStringInfoString(&buf, ")"); + + systable_endscan(scan); + table_close(catalogrel, AccessShareLock); + } + + /* Add PARTITION OF clause for partitioned tables */ + if (rel->rd_rel->relispartition) + { + char *parent_name; + char *parent_schema; + Oid parent_oid = get_partition_parent(relid, false); + + if (OidIsValid(parent_oid)) + { + parent_name = get_rel_name(parent_oid); + parent_schema = get_namespace_name(get_rel_namespace(parent_oid)); + + appendStringInfoString(&buf, " PARTITION OF "); + if (parent_schema && strcmp(parent_schema, "public") != 0) + appendStringInfo(&buf, "%s.%s", + quote_identifier(parent_schema), + quote_identifier(parent_name)); + else + appendStringInfo(&buf, "%s", quote_identifier(parent_name)); + + /* Add partition bounds */ + if (rel->rd_partkey) + { + Datum partition_bound_datum; + char *partition_bound; + + /* Use DirectFunctionCall1 to call pg_get_partition_constraintdef */ + partition_bound_datum = DirectFunctionCall1(pg_get_partition_constraintdef, + ObjectIdGetDatum(relid)); + + if (DatumGetPointer(partition_bound_datum) != NULL) + { + partition_bound = TextDatumGetCString(partition_bound_datum); + if (partition_bound && strlen(partition_bound) > 0) + appendStringInfo(&buf, " %s", partition_bound); + } + } + } + } + + /* Add WITH options */ + { + bool first_option = true; + + /* Fill factor - use RelationGetFillFactor */ + if (relform->relkind == RELKIND_RELATION) + { + int fillfactor = RelationGetFillFactor(rel, HEAP_DEFAULT_FILLFACTOR); + if (fillfactor != HEAP_DEFAULT_FILLFACTOR) + { + if (first_option) + { + appendStringInfoString(&buf, " WITH ("); + first_option = false; + } + else + appendStringInfoString(&buf, ", "); + + appendStringInfo(&buf, "fillfactor=%d", fillfactor); + } + } + + /* Toast tuple target */ + if (relform->reltoastrelid != InvalidOid) + { + Relation toast_rel = relation_open(relform->reltoastrelid, AccessShareLock); + if (toast_rel->rd_options) + { + StdRdOptions *toast_options = (StdRdOptions *) toast_rel->rd_options; + + /* XXX check if this is the right test */ + if (toast_options->toast_tuple_target != TOAST_TUPLE_TARGET) + { + if (first_option) + { + appendStringInfoString(&buf, " WITH ("); + first_option = false; + } + else + appendStringInfoString(&buf, ", "); + + appendStringInfo(&buf, "toast_tuple_target=%d", + toast_options->toast_tuple_target); + } + } + relation_close(toast_rel, AccessShareLock); + } + + if (!first_option) + appendStringInfoString(&buf, ")"); + } + + /* Add tablespace */ + if (OidIsValid(relform->reltablespace)) + { + tablespacename = get_tablespace_name(relform->reltablespace); + if (tablespacename) + appendStringInfo(&buf, " TABLESPACE %s", quote_identifier(tablespacename)); + } + + /* Add ON COMMIT clause for temporary tables */ + if (relform->relpersistence == RELPERSISTENCE_TEMP) + { + /* Get ON COMMIT from local oncommits structure */ + OnCommitAction oncommit = get_on_commit_action(relid); + + /* Only add ON COMMIT clause if we successfully determined the behavior */ + switch (oncommit) + { + case ONCOMMIT_DELETE_ROWS: + appendStringInfoString(&buf, " ON COMMIT DELETE ROWS"); + break; + case ONCOMMIT_PRESERVE_ROWS: + appendStringInfoString(&buf, " ON COMMIT PRESERVE ROWS"); + break; + case ONCOMMIT_DROP: + appendStringInfoString(&buf, " ON COMMIT DROP"); + break; + case ONCOMMIT_NOOP: + default: + /* Don't add anything for default behavior */ + break; + } + } + + /* Close the relation */ + relation_close(rel, AccessShareLock); + + PG_RETURN_TEXT_P(cstring_to_text(buf.data)); +} + +/* + * Add this to the fmgr table in src/include/catalog/pg_proc.dat: + * + * { oid => '8888', descr => 'show CREATE TABLE command for table', + * proname => 'pg_get_table_ddl', prorettype => 'text', + * proargtypes => 'regclass', prosrc => 'pg_get_table_ddl' }, + * + * And add this to src/include/utils/builtins.h: + * extern Datum pg_get_table_ddl(PG_FUNCTION_ARGS); + */ diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 118d6da1ace..8c4cf24b62a 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -12575,5 +12575,7 @@ proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}', proargnames => '{pid,io_id,io_generation,state,operation,off,length,target,handle_data_len,raw_result,result,target_desc,f_sync,f_localmem,f_buffered}', prosrc => 'pg_get_aios' }, - +{ oid => '8888', descr => 'show CREATE TABLE command for table', + proname => 'pg_get_table_ddl', prorettype => 'text', + proargtypes => 'regclass', prosrc => 'pg_get_table_ddl' }, ] diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h index 6832470d387..aa5ff4a93f6 100644 --- a/src/include/commands/tablecmds.h +++ b/src/include/commands/tablecmds.h @@ -91,6 +91,7 @@ extern void check_of_type(HeapTuple typetuple); extern void register_on_commit_action(Oid relid, OnCommitAction action); extern void remove_on_commit_action(Oid relid); +extern OnCommitAction get_on_commit_action(Oid relid); extern void PreCommit_on_commit_actions(void); extern void AtEOXact_on_commit_actions(bool isCommit); From a6a4f276be12602d1a1a95a2aced18be802efedd Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Sat, 6 Sep 2025 17:48:22 -0400 Subject: [PATCH 2/4] small tidy up, add initial test file --- src/backend/utils/adt/ruleutils.c | 12 +++++++++--- src/test/regress/expected/object_ddl.out | 19 +++++++++++++++++++ src/test/regress/parallel_schedule | 2 +- src/test/regress/sql/object_ddl.sql | 16 ++++++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 src/test/regress/expected/object_ddl.out create mode 100644 src/test/regress/sql/object_ddl.sql diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index a182b852cdc..1fd12ebe61d 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -13779,9 +13779,13 @@ pg_get_table_ddl(PG_FUNCTION_ARGS) appendStringInfoString(&buf, "TABLE "); /* Add schema-qualified table name */ - appendStringInfo(&buf, "%s.%s", - quote_identifier(schemaname), - quote_identifier(tablename)); + if (relform->relpersistence == RELPERSISTENCE_TEMP) + appendStringInfo(&buf, "%s", + quote_identifier(tablename)); + else + appendStringInfo(&buf, "%s.%s", + quote_identifier(schemaname), + quote_identifier(tablename)); /* Handle typed tables */ if (OidIsValid(relform->reloftype)) @@ -14308,6 +14312,8 @@ pg_get_table_ddl(PG_FUNCTION_ARGS) } } + appendStringInfoString(&buf, ";"); + /* Close the relation */ relation_close(rel, AccessShareLock); diff --git a/src/test/regress/expected/object_ddl.out b/src/test/regress/expected/object_ddl.out new file mode 100644 index 00000000000..e2fa94ebfe9 --- /dev/null +++ b/src/test/regress/expected/object_ddl.out @@ -0,0 +1,19 @@ +create table foo (t text not null, i serial primary key, check (t > 'a')); +create temp table bar (t text not null) on commit delete rows; +\pset tuples_only +\pset format unaligned +select pg_get_table_ddl('foo'); +CREATE TABLE public.foo +( + t text NOT NULL, + i integer NOT NULL DEFAULT nextval('foo_i_seq'::regclass), + CONSTRAINT foo_t_check CHECK ((t > 'a'::text)), + PRIMARY KEY (i) +); +select pg_get_table_ddl('bar'); +CREATE TEMPORARY TABLE bar +( + t text NOT NULL +) ON COMMIT DELETE ROWS; +drop table foo; +drop table bar; diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index fbffc67ae60..1e5630eaa5b 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -43,7 +43,7 @@ test: copy copyselect copydml copyencoding insert insert_conflict # Note: many of the tests in later groups depend on create_index # ---------- test: create_function_c create_misc create_operator create_procedure create_table create_type create_schema -test: create_index create_index_spgist create_view index_including index_including_gist +test: create_index create_index_spgist create_view index_including index_including_gist object_ddl # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/object_ddl.sql b/src/test/regress/sql/object_ddl.sql new file mode 100644 index 00000000000..78cf3d68cfe --- /dev/null +++ b/src/test/regress/sql/object_ddl.sql @@ -0,0 +1,16 @@ +create table foo (t text not null, i serial primary key, check (t > 'a')); + +create temp table bar (t text not null) on commit delete rows; + + +\pset tuples_only +\pset format unaligned + +select pg_get_table_ddl('foo'); + +select pg_get_table_ddl('bar'); + + +drop table foo; + +drop table bar; From 2a55b06093b99b27aa0c66345bff1db4045ac15d Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Mon, 8 Sep 2025 08:41:04 -0400 Subject: [PATCH 3/4] better partition handling --- src/backend/utils/adt/ruleutils.c | 121 +++++++++++++++-------- src/test/regress/expected/object_ddl.out | 28 +++++- src/test/regress/sql/object_ddl.sql | 17 +++- 3 files changed, 124 insertions(+), 42 deletions(-) diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 1fd12ebe61d..e47ae9ade2e 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -13714,15 +13714,6 @@ get_range_partbound_string(List *bound_datums) return buf->data; } -/* - * pg_get_table_ddl - Generate CREATE TABLE DDL for a given relation - * - * This function should be added to src/backend/utils/adt/ruleutils.c - */ - -/* Function declaration for ruleutils.c */ -Datum pg_get_table_ddl(PG_FUNCTION_ARGS); - /* * pg_get_table_ddl * @@ -14193,38 +14184,99 @@ pg_get_table_ddl(PG_FUNCTION_ARGS) char *parent_name; char *parent_schema; Oid parent_oid = get_partition_parent(relid, false); + HeapTuple reltuple; + text *boundDatum; + + + /* Get pg_class.relpartbound */ + reltuple = SearchSysCache1(RELOID, + ObjectIdGetDatum(RelationGetRelid(rel))); + if (!HeapTupleIsValid(reltuple)) + elog(ERROR, "cache lookup failed for relation %u", relid); + + /* There must be a partition bound (XXX I think) */ + boundDatum = DatumGetTextPP(SysCacheGetAttrNotNull(RELOID, reltuple, + Anum_pg_class_relpartbound)); if (OidIsValid(parent_oid)) { parent_name = get_rel_name(parent_oid); parent_schema = get_namespace_name(get_rel_namespace(parent_oid)); appendStringInfoString(&buf, " PARTITION OF "); - if (parent_schema && strcmp(parent_schema, "public") != 0) - appendStringInfo(&buf, "%s.%s", - quote_identifier(parent_schema), - quote_identifier(parent_name)); - else - appendStringInfo(&buf, "%s", quote_identifier(parent_name)); + appendStringInfo(&buf, "%s.%s", + quote_identifier(parent_schema), + quote_identifier(parent_name)); - /* Add partition bounds */ - if (rel->rd_partkey) - { - Datum partition_bound_datum; - char *partition_bound; + appendStringInfo(&buf, " %s", text_to_cstring(pg_get_expr_worker(boundDatum, relid, 0))); + } - /* Use DirectFunctionCall1 to call pg_get_partition_constraintdef */ - partition_bound_datum = DirectFunctionCall1(pg_get_partition_constraintdef, - ObjectIdGetDatum(relid)); + ReleaseSysCache(reltuple); + } - if (DatumGetPointer(partition_bound_datum) != NULL) - { - partition_bound = TextDatumGetCString(partition_bound_datum); - if (partition_bound && strlen(partition_bound) > 0) - appendStringInfo(&buf, " %s", partition_bound); - } + /* Add PARTITION BY clause for partitioned tables */ + + /* XXX check if this handles all cases */ + + if (relform->relkind == RELKIND_PARTITIONED_TABLE) + { + PartitionKey partkey = RelationGetPartitionKey(rel); + char *parttype; + + switch (partkey->strategy) + { + case PARTITION_STRATEGY_RANGE: + parttype = "RANGE"; + break; + case PARTITION_STRATEGY_LIST: + parttype = "LIST"; + break; + case PARTITION_STRATEGY_HASH: + parttype = "HASH"; + break; + default: + parttype = "UNKNOWN"; + break; + } + + appendStringInfo(&buf, " PARTITION BY %s (", parttype); + + /* Add partition key columns */ + for (i = 0; i < partkey->partnatts; i++) + { + AttrNumber attnum = partkey->partattrs[i]; + char *attname; + + if (i > 0) + appendStringInfoString(&buf, ", "); + + if (attnum > 0) + { + /* Regular column */ + attname = get_attname(relid, attnum, false); + appendStringInfo(&buf, "%s", quote_identifier(attname)); + } + else + { + /* Expression */ + Node *partexpr = list_nth(partkey->partexprs, + partkey->partattrs[i] - 1 - FirstLowInvalidHeapAttributeNumber); + char *exprstr = deparse_expression(partexpr, + deparse_context_for(RelationGetRelationName(rel), relid), + false, false); + appendStringInfo(&buf, "(%s)", exprstr); + } + + /* Add collation if specified */ + if (OidIsValid(partkey->partcollation[i])) + { + char *collname = get_collation_name(partkey->partcollation[i]); + if (collname && strcmp(collname, "default") != 0) + appendStringInfo(&buf, " COLLATE %s", quote_identifier(collname)); } } + + appendStringInfoString(&buf, ")"); } /* Add WITH options */ @@ -14319,14 +14371,3 @@ pg_get_table_ddl(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text(buf.data)); } - -/* - * Add this to the fmgr table in src/include/catalog/pg_proc.dat: - * - * { oid => '8888', descr => 'show CREATE TABLE command for table', - * proname => 'pg_get_table_ddl', prorettype => 'text', - * proargtypes => 'regclass', prosrc => 'pg_get_table_ddl' }, - * - * And add this to src/include/utils/builtins.h: - * extern Datum pg_get_table_ddl(PG_FUNCTION_ARGS); - */ diff --git a/src/test/regress/expected/object_ddl.out b/src/test/regress/expected/object_ddl.out index e2fa94ebfe9..89a5858675c 100644 --- a/src/test/regress/expected/object_ddl.out +++ b/src/test/regress/expected/object_ddl.out @@ -1,8 +1,8 @@ create table foo (t text not null, i serial primary key, check (t > 'a')); create temp table bar (t text not null) on commit delete rows; -\pset tuples_only \pset format unaligned select pg_get_table_ddl('foo'); +pg_get_table_ddl CREATE TABLE public.foo ( t text NOT NULL, @@ -10,10 +10,36 @@ CREATE TABLE public.foo CONSTRAINT foo_t_check CHECK ((t > 'a'::text)), PRIMARY KEY (i) ); +(1 row) select pg_get_table_ddl('bar'); +pg_get_table_ddl CREATE TEMPORARY TABLE bar ( t text NOT NULL ) ON COMMIT DELETE ROWS; +(1 row) +CREATE TABLE test_part_parent ( + a integer DEFAULT 2501, + b integer DEFAULT 142857 +) +PARTITION BY LIST (a); +create table test_part_partition_123 partition of test_part_parent for values in (1,2,3); +create table test_part_partition_def partition of test_part_parent default; +select pg_get_table_ddl('test_part_parent'); +pg_get_table_ddl +CREATE TABLE public.test_part_parent +( + a integer DEFAULT 2501, + b integer DEFAULT 142857 +) PARTITION BY LIST (a); +(1 row) +select pg_get_table_ddl('test_part_partition_123'); +pg_get_table_ddl +CREATE TABLE public.test_part_partition_123 PARTITION OF public.test_part_parent FOR VALUES IN (1, 2, 3); +(1 row) +select pg_get_table_ddl('test_part_partition_def'); +pg_get_table_ddl +CREATE TABLE public.test_part_partition_def PARTITION OF public.test_part_parent DEFAULT; +(1 row) drop table foo; drop table bar; diff --git a/src/test/regress/sql/object_ddl.sql b/src/test/regress/sql/object_ddl.sql index 78cf3d68cfe..f09c24b800a 100644 --- a/src/test/regress/sql/object_ddl.sql +++ b/src/test/regress/sql/object_ddl.sql @@ -3,13 +3,28 @@ create table foo (t text not null, i serial primary key, check (t > 'a')); create temp table bar (t text not null) on commit delete rows; -\pset tuples_only \pset format unaligned select pg_get_table_ddl('foo'); select pg_get_table_ddl('bar'); +CREATE TABLE test_part_parent ( + a integer DEFAULT 2501, + b integer DEFAULT 142857 +) +PARTITION BY LIST (a); + +create table test_part_partition_123 partition of test_part_parent for values in (1,2,3); + +create table test_part_partition_def partition of test_part_parent default; + +select pg_get_table_ddl('test_part_parent'); + +select pg_get_table_ddl('test_part_partition_123'); + +select pg_get_table_ddl('test_part_partition_def'); + drop table foo; From 74a01e53fb3cad7b9314754a572e26c1fc2fa058 Mon Sep 17 00:00:00 2001 From: Andrew Dunstan Date: Mon, 8 Sep 2025 08:45:05 -0400 Subject: [PATCH 4/4] pgindent run --- src/backend/utils/adt/ruleutils.c | 1214 +++++++++++++++-------------- 1 file changed, 618 insertions(+), 596 deletions(-) diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index e47ae9ade2e..13f2d93371b 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -13723,53 +13723,53 @@ get_range_partbound_string(List *bound_datums) Datum pg_get_table_ddl(PG_FUNCTION_ARGS) { - Oid relid = PG_GETARG_OID(0); - StringInfoData buf; - Relation rel; - TupleDesc tupdesc; - Form_pg_class relform; - char *tablename; - char *schemaname; - char *tablespacename = NULL; - bool first_col = true; - int i; - - /* Open the relation */ - rel = relation_open(relid, AccessShareLock); - relform = RelationGetForm(rel); - - /* Only handle tables and partitioned tables */ - if (relform->relkind != RELKIND_RELATION && - relform->relkind != RELKIND_PARTITIONED_TABLE) - { - relation_close(rel, AccessShareLock); - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("relation \"%s\" is not a table", - RelationGetRelationName(rel)))); - } + Oid relid = PG_GETARG_OID(0); + StringInfoData buf; + Relation rel; + TupleDesc tupdesc; + Form_pg_class relform; + char *tablename; + char *schemaname; + char *tablespacename = NULL; + bool first_col = true; + int i; + + /* Open the relation */ + rel = relation_open(relid, AccessShareLock); + relform = RelationGetForm(rel); + + /* Only handle tables and partitioned tables */ + if (relform->relkind != RELKIND_RELATION && + relform->relkind != RELKIND_PARTITIONED_TABLE) + { + relation_close(rel, AccessShareLock); + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("relation \"%s\" is not a table", + RelationGetRelationName(rel)))); + } /* Get tuple descriptor */ tupdesc = RelationGetDescr(rel); - initStringInfo(&buf); + initStringInfo(&buf); - /* Get table and schema names */ - tablename = RelationGetRelationName(rel); - schemaname = get_namespace_name(RelationGetNamespace(rel)); + /* Get table and schema names */ + tablename = RelationGetRelationName(rel); + schemaname = get_namespace_name(RelationGetNamespace(rel)); - /* Start building the CREATE TABLE statement */ - appendStringInfo(&buf, "CREATE "); + /* Start building the CREATE TABLE statement */ + appendStringInfo(&buf, "CREATE "); - /* Add table type modifiers */ - if (relform->relpersistence == RELPERSISTENCE_UNLOGGED) - appendStringInfoString(&buf, "UNLOGGED "); - else if (relform->relpersistence == RELPERSISTENCE_TEMP) - appendStringInfoString(&buf, "TEMPORARY "); + /* Add table type modifiers */ + if (relform->relpersistence == RELPERSISTENCE_UNLOGGED) + appendStringInfoString(&buf, "UNLOGGED "); + else if (relform->relpersistence == RELPERSISTENCE_TEMP) + appendStringInfoString(&buf, "TEMPORARY "); - appendStringInfoString(&buf, "TABLE "); + appendStringInfoString(&buf, "TABLE "); - /* Add schema-qualified table name */ + /* Add schema-qualified table name */ if (relform->relpersistence == RELPERSISTENCE_TEMP) appendStringInfo(&buf, "%s", quote_identifier(tablename)); @@ -13778,414 +13778,429 @@ pg_get_table_ddl(PG_FUNCTION_ARGS) quote_identifier(schemaname), quote_identifier(tablename)); - /* Handle typed tables */ - if (OidIsValid(relform->reloftype)) - { - /* For typed tables, we don't need full column definitions, - * just any local columns or constraints */ - bool has_local_columns = false; - char *typename = format_type_be(relform->reloftype); - appendStringInfo(&buf, " OF %s", typename); - - /* Check if there are any local columns beyond the type */ - for (i = 0; i < tupdesc->natts; i++) - { - Form_pg_attribute attr = TupleDescAttr(tupdesc, i); - if (attr->attisdropped || attr->attinhcount > 0) - continue; - has_local_columns = true; - break; - } - - if (has_local_columns || (rel->rd_att->constr && rel->rd_att->constr->num_check > 0)) - { - appendStringInfoString(&buf, "\n(\n"); - first_col = true; - - /* Add any local columns */ - for (i = 0; i < tupdesc->natts; i++) - { - Form_pg_attribute attr = TupleDescAttr(tupdesc, i); - - if (attr->attisdropped || attr->attinhcount > 0) - continue; - - if (!first_col) - appendStringInfoString(&buf, ",\n"); - first_col = false; - - /* Only show local modifications/constraints for this column */ - appendStringInfo(&buf, " %s", quote_identifier(NameStr(attr->attname))); - - if (attr->attnotnull) - appendStringInfoString(&buf, " NOT NULL"); - - if (attr->atthasdef) - { - AttrDefault *defval = NULL; - - if (rel->rd_att->constr && rel->rd_att->constr->defval) - { - for (int j = 0; j < rel->rd_att->constr->num_defval; j++) - { - if (rel->rd_att->constr->defval[j].adnum == attr->attnum) - { - defval = &rel->rd_att->constr->defval[j]; - break; - } - } - } - - if (defval) - appendStringInfo(&buf, " DEFAULT %s", + /* Handle typed tables */ + if (OidIsValid(relform->reloftype)) + { + /* + * For typed tables, we don't need full column definitions, just any + * local columns or constraints + */ + bool has_local_columns = false; + char *typename = format_type_be(relform->reloftype); + + appendStringInfo(&buf, " OF %s", typename); + + /* Check if there are any local columns beyond the type */ + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + + if (attr->attisdropped || attr->attinhcount > 0) + continue; + has_local_columns = true; + break; + } + + if (has_local_columns || (rel->rd_att->constr && rel->rd_att->constr->num_check > 0)) + { + appendStringInfoString(&buf, "\n(\n"); + first_col = true; + + /* Add any local columns */ + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + + if (attr->attisdropped || attr->attinhcount > 0) + continue; + + if (!first_col) + appendStringInfoString(&buf, ",\n"); + first_col = false; + + /* Only show local modifications/constraints for this column */ + appendStringInfo(&buf, " %s", quote_identifier(NameStr(attr->attname))); + + if (attr->attnotnull) + appendStringInfoString(&buf, " NOT NULL"); + + if (attr->atthasdef) + { + AttrDefault *defval = NULL; + + if (rel->rd_att->constr && rel->rd_att->constr->defval) + { + for (int j = 0; j < rel->rd_att->constr->num_defval; j++) + { + if (rel->rd_att->constr->defval[j].adnum == attr->attnum) + { + defval = &rel->rd_att->constr->defval[j]; + break; + } + } + } + + if (defval) + appendStringInfo(&buf, " DEFAULT %s", text_to_cstring(pg_get_expr_worker(cstring_to_text(defval->adbin), relid, 0))); - } - } - - /* Add CHECK constraints for typed tables */ - if (rel->rd_att->constr && rel->rd_att->constr->num_check > 0) - { - for (i = 0; i < rel->rd_att->constr->num_check; i++) - { - ConstrCheck *check = &rel->rd_att->constr->check[i]; - if (!first_col) - appendStringInfoString(&buf, ",\n"); - first_col = false; - appendStringInfo(&buf, " CONSTRAINT %s CHECK (%s)", - quote_identifier(check->ccname), - text_to_cstring(pg_get_expr_worker(cstring_to_text(check->ccbin), relid, 0))); - } - } - - appendStringInfoString(&buf, "\n)"); - } - } - /* Handle partition tables - only show local columns if any */ - else if (rel->rd_rel->relispartition) - { - /* For partitions, we typically don't show inherited columns, - * only local additions or modifications */ - bool has_local_additions = false; - - /* Check for any local columns or constraints */ - for (i = 0; i < tupdesc->natts; i++) - { - Form_pg_attribute attr = TupleDescAttr(tupdesc, i); - if (attr->attisdropped) - continue; - /* Check if this is a local column (not inherited) */ - if (attr->attinhcount == 0) - { - has_local_additions = true; - break; - } - } - - if (has_local_additions || (rel->rd_att->constr && rel->rd_att->constr->num_check > 0)) - { - appendStringInfoString(&buf, "\n(\n"); - first_col = true; - - /* Add local columns only */ - for (i = 0; i < tupdesc->natts; i++) - { - Form_pg_attribute attr = TupleDescAttr(tupdesc, i); - char *typename_with_typemod; - - if (attr->attisdropped || attr->attinhcount > 0) - continue; - - if (!first_col) - appendStringInfoString(&buf, ",\n"); - first_col = false; - - /* Full column definition for local columns */ - appendStringInfo(&buf, " %s", quote_identifier(NameStr(attr->attname))); - - typename_with_typemod = format_type_with_typemod(attr->atttypid, attr->atttypmod); - appendStringInfo(&buf, " %s", typename_with_typemod); - - if (attr->attnotnull) - appendStringInfoString(&buf, " NOT NULL"); - - if (attr->atthasdef) - { - AttrDefault *defval = NULL; - - if (rel->rd_att->constr && rel->rd_att->constr->defval) - { - for (int j = 0; j < rel->rd_att->constr->num_defval; j++) - { - if (rel->rd_att->constr->defval[j].adnum == attr->attnum) - { - defval = &rel->rd_att->constr->defval[j]; - break; - } - } - } - - if (defval) - appendStringInfo(&buf, " DEFAULT %s", + } + } + + /* Add CHECK constraints for typed tables */ + if (rel->rd_att->constr && rel->rd_att->constr->num_check > 0) + { + for (i = 0; i < rel->rd_att->constr->num_check; i++) + { + ConstrCheck *check = &rel->rd_att->constr->check[i]; + + if (!first_col) + appendStringInfoString(&buf, ",\n"); + first_col = false; + appendStringInfo(&buf, " CONSTRAINT %s CHECK (%s)", + quote_identifier(check->ccname), + text_to_cstring(pg_get_expr_worker(cstring_to_text(check->ccbin), relid, 0))); + } + } + + appendStringInfoString(&buf, "\n)"); + } + } + /* Handle partition tables - only show local columns if any */ + else if (rel->rd_rel->relispartition) + { + /* + * For partitions, we typically don't show inherited columns, only + * local additions or modifications + */ + bool has_local_additions = false; + + /* Check for any local columns or constraints */ + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + + if (attr->attisdropped) + continue; + /* Check if this is a local column (not inherited) */ + if (attr->attinhcount == 0) + { + has_local_additions = true; + break; + } + } + + if (has_local_additions || (rel->rd_att->constr && rel->rd_att->constr->num_check > 0)) + { + appendStringInfoString(&buf, "\n(\n"); + first_col = true; + + /* Add local columns only */ + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + char *typename_with_typemod; + + if (attr->attisdropped || attr->attinhcount > 0) + continue; + + if (!first_col) + appendStringInfoString(&buf, ",\n"); + first_col = false; + + /* Full column definition for local columns */ + appendStringInfo(&buf, " %s", quote_identifier(NameStr(attr->attname))); + + typename_with_typemod = format_type_with_typemod(attr->atttypid, attr->atttypmod); + appendStringInfo(&buf, " %s", typename_with_typemod); + + if (attr->attnotnull) + appendStringInfoString(&buf, " NOT NULL"); + + if (attr->atthasdef) + { + AttrDefault *defval = NULL; + + if (rel->rd_att->constr && rel->rd_att->constr->defval) + { + for (int j = 0; j < rel->rd_att->constr->num_defval; j++) + { + if (rel->rd_att->constr->defval[j].adnum == attr->attnum) + { + defval = &rel->rd_att->constr->defval[j]; + break; + } + } + } + + if (defval) + appendStringInfo(&buf, " DEFAULT %s", text_to_cstring(pg_get_expr_worker(cstring_to_text(defval->adbin), relid, 0))); - } - } - - /* Add local CHECK constraints */ - if (rel->rd_att->constr && rel->rd_att->constr->num_check > 0) - { - for (i = 0; i < rel->rd_att->constr->num_check; i++) - { - ConstrCheck *check = &rel->rd_att->constr->check[i]; - if (!first_col) - appendStringInfoString(&buf, ",\n"); - first_col = false; - appendStringInfo(&buf, " CONSTRAINT %s CHECK (%s)", - quote_identifier(check->ccname), - text_to_cstring(pg_get_expr_worker(cstring_to_text(check->ccbin), relid, 0))); - } - } - - appendStringInfoString(&buf, "\n)"); - } - } - /* Handle regular tables and partitioned tables */ - else - { - appendStringInfoString(&buf, "\n(\n"); - - /* Process each column */ - for (i = 0; i < tupdesc->natts; i++) - { - Form_pg_attribute attr = TupleDescAttr(tupdesc, i); - char *typename_with_typemod; - bool notnull; - char storage_type; - - /* Skip dropped columns */ - if (attr->attisdropped) - continue; - - if (!first_col) - appendStringInfoString(&buf, ",\n"); - first_col = false; - - /* Column name */ - appendStringInfo(&buf, " %s", quote_identifier(NameStr(attr->attname))); - - /* Data type with type modifiers */ - typename_with_typemod = format_type_with_typemod(attr->atttypid, attr->atttypmod); - appendStringInfo(&buf, " %s", typename_with_typemod); - - /* Collation */ - if (OidIsValid(attr->attcollation)) - { - HeapTuple coll_tuple; - Form_pg_collation coll_form; - - coll_tuple = SearchSysCache1(COLLOID, ObjectIdGetDatum(attr->attcollation)); - if (HeapTupleIsValid(coll_tuple)) - { - /* Only show collation if it's not the default for the type */ - Oid default_collation = get_typcollation(attr->atttypid); - - coll_form = (Form_pg_collation) GETSTRUCT(coll_tuple); - - if (attr->attcollation != default_collation) - { - char *coll_namespace = get_namespace_name(coll_form->collnamespace); - if (coll_namespace && strcmp(coll_namespace, "pg_catalog") != 0) - appendStringInfo(&buf, " COLLATE %s.%s", - quote_identifier(coll_namespace), - quote_identifier(NameStr(coll_form->collname))); - else - appendStringInfo(&buf, " COLLATE %s", - quote_identifier(NameStr(coll_form->collname))); - } - ReleaseSysCache(coll_tuple); - } - } - - /* NOT NULL constraint */ - notnull = attr->attnotnull; - if (notnull) - appendStringInfoString(&buf, " NOT NULL"); - - /* Default expression */ - if (attr->atthasdef) - { - AttrDefault *defval = NULL; - - if (rel->rd_att->constr && rel->rd_att->constr->defval) - { - for (int j = 0; j < rel->rd_att->constr->num_defval; j++) - { - if (rel->rd_att->constr->defval[j].adnum == attr->attnum) - { - defval = &rel->rd_att->constr->defval[j]; - break; - } - } - } - - if (defval) - { + } + } + + /* Add local CHECK constraints */ + if (rel->rd_att->constr && rel->rd_att->constr->num_check > 0) + { + for (i = 0; i < rel->rd_att->constr->num_check; i++) + { + ConstrCheck *check = &rel->rd_att->constr->check[i]; + + if (!first_col) + appendStringInfoString(&buf, ",\n"); + first_col = false; + appendStringInfo(&buf, " CONSTRAINT %s CHECK (%s)", + quote_identifier(check->ccname), + text_to_cstring(pg_get_expr_worker(cstring_to_text(check->ccbin), relid, 0))); + } + } + + appendStringInfoString(&buf, "\n)"); + } + } + /* Handle regular tables and partitioned tables */ + else + { + appendStringInfoString(&buf, "\n(\n"); + + /* Process each column */ + for (i = 0; i < tupdesc->natts; i++) + { + Form_pg_attribute attr = TupleDescAttr(tupdesc, i); + char *typename_with_typemod; + bool notnull; + char storage_type; + + /* Skip dropped columns */ + if (attr->attisdropped) + continue; + + if (!first_col) + appendStringInfoString(&buf, ",\n"); + first_col = false; + + /* Column name */ + appendStringInfo(&buf, " %s", quote_identifier(NameStr(attr->attname))); + + /* Data type with type modifiers */ + typename_with_typemod = format_type_with_typemod(attr->atttypid, attr->atttypmod); + appendStringInfo(&buf, " %s", typename_with_typemod); + + /* Collation */ + if (OidIsValid(attr->attcollation)) + { + HeapTuple coll_tuple; + Form_pg_collation coll_form; + + coll_tuple = SearchSysCache1(COLLOID, ObjectIdGetDatum(attr->attcollation)); + if (HeapTupleIsValid(coll_tuple)) + { + /* + * Only show collation if it's not the default for the + * type + */ + Oid default_collation = get_typcollation(attr->atttypid); + + coll_form = (Form_pg_collation) GETSTRUCT(coll_tuple); + + if (attr->attcollation != default_collation) + { + char *coll_namespace = get_namespace_name(coll_form->collnamespace); + + if (coll_namespace && strcmp(coll_namespace, "pg_catalog") != 0) + appendStringInfo(&buf, " COLLATE %s.%s", + quote_identifier(coll_namespace), + quote_identifier(NameStr(coll_form->collname))); + else + appendStringInfo(&buf, " COLLATE %s", + quote_identifier(NameStr(coll_form->collname))); + } + ReleaseSysCache(coll_tuple); + } + } + + /* NOT NULL constraint */ + notnull = attr->attnotnull; + if (notnull) + appendStringInfoString(&buf, " NOT NULL"); + + /* Default expression */ + if (attr->atthasdef) + { + AttrDefault *defval = NULL; + + if (rel->rd_att->constr && rel->rd_att->constr->defval) + { + for (int j = 0; j < rel->rd_att->constr->num_defval; j++) + { + if (rel->rd_att->constr->defval[j].adnum == attr->attnum) + { + defval = &rel->rd_att->constr->defval[j]; + break; + } + } + } + + if (defval) + { appendStringInfo(&buf, " DEFAULT %s", text_to_cstring(pg_get_expr_worker(cstring_to_text(defval->adbin), relid, 0))); - } - } - - /* Storage type */ - storage_type = attr->attstorage; - if (storage_type != get_typstorage(attr->atttypid)) - { - switch (storage_type) - { - case 'p': - appendStringInfoString(&buf, " STORAGE PLAIN"); - break; - case 'e': - appendStringInfoString(&buf, " STORAGE EXTERNAL"); - break; - case 'm': - appendStringInfoString(&buf, " STORAGE MAIN"); - break; - case 'x': - appendStringInfoString(&buf, " STORAGE EXTENDED"); - break; - } - } - - /* Compression method (PostgreSQL 14+) */ - if (attr->attcompression != InvalidCompressionMethod) - { - const char *compression_name = GetCompressionMethodName(attr->attcompression); - if (compression_name) - appendStringInfo(&buf, " COMPRESSION %s", compression_name); - } - } - - /* Add table constraints (PRIMARY KEY, UNIQUE, CHECK, etc.) */ - if (rel->rd_att->constr && rel->rd_att->constr->num_check > 0) - { - for (i = 0; i < rel->rd_att->constr->num_check; i++) - { - ConstrCheck *check = &rel->rd_att->constr->check[i]; - appendStringInfo(&buf, ",\n CONSTRAINT %s CHECK (%s)", - quote_identifier(check->ccname), - text_to_cstring(pg_get_expr_worker(cstring_to_text(check->ccbin), relid, 0))); - } - } - - /* Add indexes as constraints (PRIMARY KEY, UNIQUE) */ - { - List *indexoidlist = RelationGetIndexList(rel); - ListCell *l; - - foreach(l, indexoidlist) - { - Oid indexoid = lfirst_oid(l); - Relation indexrel; - Form_pg_index indexform; - HeapTuple indexTuple; - - indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid)); - if (!HeapTupleIsValid(indexTuple)) - continue; - - indexform = (Form_pg_index) GETSTRUCT(indexTuple); - - if (indexform->indisprimary || indexform->indisunique) - { - indexrel = relation_open(indexoid, AccessShareLock); - - if (indexform->indisprimary) - appendStringInfoString(&buf, ",\n PRIMARY KEY ("); - else - appendStringInfoString(&buf, ",\n UNIQUE ("); - - /* Add index column names */ - for (int j = 0; j < indexform->indnatts; j++) - { - int16 attnum = indexform->indkey.values[j]; - char *attname = get_attname(relid, attnum, false); - - if (j > 0) - appendStringInfoString(&buf, ", "); - appendStringInfo(&buf, "%s", quote_identifier(attname)); - } - appendStringInfoString(&buf, ")"); - - relation_close(indexrel, AccessShareLock); - } - - ReleaseSysCache(indexTuple); - } - - list_free(indexoidlist); - } - - appendStringInfoString(&buf, "\n)"); - } - - /* Add INHERITS clause for inheritance (not partitioning) */ - if (rel->rd_rel->relhassubclass == false && rel->rd_rel->relispartition == false) - { - Relation catalogrel; - SysScanDesc scan; - ScanKeyData key; - HeapTuple inheritsTuple; - bool first_parent = true; - - catalogrel = table_open(InheritsRelationId, AccessShareLock); - - ScanKeyInit(&key, - Anum_pg_inherits_inhrelid, - BTEqualStrategyNumber, F_OIDEQ, - ObjectIdGetDatum(relid)); - - scan = systable_beginscan(catalogrel, InheritsRelidSeqnoIndexId, - true, NULL, 1, &key); - - while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) - { - Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(inheritsTuple); - char *parent_name; - char *parent_schema; - - if (first_parent) - { - appendStringInfoString(&buf, " INHERITS ("); - first_parent = false; - } - else - appendStringInfoString(&buf, ", "); - - parent_name = get_rel_name(inh->inhparent); - parent_schema = get_namespace_name(get_rel_namespace(inh->inhparent)); - - if (parent_schema && strcmp(parent_schema, "public") != 0) - appendStringInfo(&buf, "%s.%s", - quote_identifier(parent_schema), - quote_identifier(parent_name)); - else - appendStringInfo(&buf, "%s", quote_identifier(parent_name)); - } - - if (!first_parent) - appendStringInfoString(&buf, ")"); - - systable_endscan(scan); - table_close(catalogrel, AccessShareLock); - } - - /* Add PARTITION OF clause for partitioned tables */ - if (rel->rd_rel->relispartition) - { - char *parent_name; - char *parent_schema; - Oid parent_oid = get_partition_parent(relid, false); - HeapTuple reltuple; - text *boundDatum; + } + } + + /* Storage type */ + storage_type = attr->attstorage; + if (storage_type != get_typstorage(attr->atttypid)) + { + switch (storage_type) + { + case 'p': + appendStringInfoString(&buf, " STORAGE PLAIN"); + break; + case 'e': + appendStringInfoString(&buf, " STORAGE EXTERNAL"); + break; + case 'm': + appendStringInfoString(&buf, " STORAGE MAIN"); + break; + case 'x': + appendStringInfoString(&buf, " STORAGE EXTENDED"); + break; + } + } + + /* Compression method (PostgreSQL 14+) */ + if (attr->attcompression != InvalidCompressionMethod) + { + const char *compression_name = GetCompressionMethodName(attr->attcompression); + + if (compression_name) + appendStringInfo(&buf, " COMPRESSION %s", compression_name); + } + } + + /* Add table constraints (PRIMARY KEY, UNIQUE, CHECK, etc.) */ + if (rel->rd_att->constr && rel->rd_att->constr->num_check > 0) + { + for (i = 0; i < rel->rd_att->constr->num_check; i++) + { + ConstrCheck *check = &rel->rd_att->constr->check[i]; + + appendStringInfo(&buf, ",\n CONSTRAINT %s CHECK (%s)", + quote_identifier(check->ccname), + text_to_cstring(pg_get_expr_worker(cstring_to_text(check->ccbin), relid, 0))); + } + } + + /* Add indexes as constraints (PRIMARY KEY, UNIQUE) */ + { + List *indexoidlist = RelationGetIndexList(rel); + ListCell *l; + + foreach(l, indexoidlist) + { + Oid indexoid = lfirst_oid(l); + Relation indexrel; + Form_pg_index indexform; + HeapTuple indexTuple; + + indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid)); + if (!HeapTupleIsValid(indexTuple)) + continue; + + indexform = (Form_pg_index) GETSTRUCT(indexTuple); + + if (indexform->indisprimary || indexform->indisunique) + { + indexrel = relation_open(indexoid, AccessShareLock); + + if (indexform->indisprimary) + appendStringInfoString(&buf, ",\n PRIMARY KEY ("); + else + appendStringInfoString(&buf, ",\n UNIQUE ("); + + /* Add index column names */ + for (int j = 0; j < indexform->indnatts; j++) + { + int16 attnum = indexform->indkey.values[j]; + char *attname = get_attname(relid, attnum, false); + + if (j > 0) + appendStringInfoString(&buf, ", "); + appendStringInfo(&buf, "%s", quote_identifier(attname)); + } + appendStringInfoString(&buf, ")"); + + relation_close(indexrel, AccessShareLock); + } + + ReleaseSysCache(indexTuple); + } + + list_free(indexoidlist); + } + + appendStringInfoString(&buf, "\n)"); + } + + /* Add INHERITS clause for inheritance (not partitioning) */ + if (rel->rd_rel->relhassubclass == false && rel->rd_rel->relispartition == false) + { + Relation catalogrel; + SysScanDesc scan; + ScanKeyData key; + HeapTuple inheritsTuple; + bool first_parent = true; + + catalogrel = table_open(InheritsRelationId, AccessShareLock); + + ScanKeyInit(&key, + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + + scan = systable_beginscan(catalogrel, InheritsRelidSeqnoIndexId, + true, NULL, 1, &key); + + while (HeapTupleIsValid(inheritsTuple = systable_getnext(scan))) + { + Form_pg_inherits inh = (Form_pg_inherits) GETSTRUCT(inheritsTuple); + char *parent_name; + char *parent_schema; + + if (first_parent) + { + appendStringInfoString(&buf, " INHERITS ("); + first_parent = false; + } + else + appendStringInfoString(&buf, ", "); + + parent_name = get_rel_name(inh->inhparent); + parent_schema = get_namespace_name(get_rel_namespace(inh->inhparent)); + + if (parent_schema && strcmp(parent_schema, "public") != 0) + appendStringInfo(&buf, "%s.%s", + quote_identifier(parent_schema), + quote_identifier(parent_name)); + else + appendStringInfo(&buf, "%s", quote_identifier(parent_name)); + } + + if (!first_parent) + appendStringInfoString(&buf, ")"); + + systable_endscan(scan); + table_close(catalogrel, AccessShareLock); + } + + /* Add PARTITION OF clause for partitioned tables */ + if (rel->rd_rel->relispartition) + { + char *parent_name; + char *parent_schema; + Oid parent_oid = get_partition_parent(relid, false); + HeapTuple reltuple; + text *boundDatum; /* Get pg_class.relpartbound */ @@ -14198,176 +14213,183 @@ pg_get_table_ddl(PG_FUNCTION_ARGS) /* There must be a partition bound (XXX I think) */ boundDatum = DatumGetTextPP(SysCacheGetAttrNotNull(RELOID, reltuple, Anum_pg_class_relpartbound)); - if (OidIsValid(parent_oid)) - { - parent_name = get_rel_name(parent_oid); - parent_schema = get_namespace_name(get_rel_namespace(parent_oid)); + if (OidIsValid(parent_oid)) + { + parent_name = get_rel_name(parent_oid); + parent_schema = get_namespace_name(get_rel_namespace(parent_oid)); - appendStringInfoString(&buf, " PARTITION OF "); + appendStringInfoString(&buf, " PARTITION OF "); appendStringInfo(&buf, "%s.%s", quote_identifier(parent_schema), quote_identifier(parent_name)); appendStringInfo(&buf, " %s", text_to_cstring(pg_get_expr_worker(boundDatum, relid, 0))); - } + } ReleaseSysCache(reltuple); - } + } - /* Add PARTITION BY clause for partitioned tables */ + /* Add PARTITION BY clause for partitioned tables */ /* XXX check if this handles all cases */ - if (relform->relkind == RELKIND_PARTITIONED_TABLE) - { - PartitionKey partkey = RelationGetPartitionKey(rel); - char *parttype; - - switch (partkey->strategy) - { - case PARTITION_STRATEGY_RANGE: - parttype = "RANGE"; - break; - case PARTITION_STRATEGY_LIST: - parttype = "LIST"; - break; - case PARTITION_STRATEGY_HASH: - parttype = "HASH"; - break; - default: - parttype = "UNKNOWN"; - break; - } - - appendStringInfo(&buf, " PARTITION BY %s (", parttype); - - /* Add partition key columns */ - for (i = 0; i < partkey->partnatts; i++) - { - AttrNumber attnum = partkey->partattrs[i]; - char *attname; - - if (i > 0) - appendStringInfoString(&buf, ", "); - - if (attnum > 0) - { - /* Regular column */ - attname = get_attname(relid, attnum, false); - appendStringInfo(&buf, "%s", quote_identifier(attname)); - } - else - { - /* Expression */ - Node *partexpr = list_nth(partkey->partexprs, - partkey->partattrs[i] - 1 - FirstLowInvalidHeapAttributeNumber); - char *exprstr = deparse_expression(partexpr, - deparse_context_for(RelationGetRelationName(rel), relid), - false, false); - appendStringInfo(&buf, "(%s)", exprstr); - } - - /* Add collation if specified */ - if (OidIsValid(partkey->partcollation[i])) - { - char *collname = get_collation_name(partkey->partcollation[i]); - if (collname && strcmp(collname, "default") != 0) - appendStringInfo(&buf, " COLLATE %s", quote_identifier(collname)); - } - } - - appendStringInfoString(&buf, ")"); - } - - /* Add WITH options */ - { - bool first_option = true; - - /* Fill factor - use RelationGetFillFactor */ - if (relform->relkind == RELKIND_RELATION) - { - int fillfactor = RelationGetFillFactor(rel, HEAP_DEFAULT_FILLFACTOR); - if (fillfactor != HEAP_DEFAULT_FILLFACTOR) - { - if (first_option) - { - appendStringInfoString(&buf, " WITH ("); - first_option = false; - } - else - appendStringInfoString(&buf, ", "); - - appendStringInfo(&buf, "fillfactor=%d", fillfactor); - } - } - - /* Toast tuple target */ - if (relform->reltoastrelid != InvalidOid) - { - Relation toast_rel = relation_open(relform->reltoastrelid, AccessShareLock); - if (toast_rel->rd_options) - { - StdRdOptions *toast_options = (StdRdOptions *) toast_rel->rd_options; + if (relform->relkind == RELKIND_PARTITIONED_TABLE) + { + PartitionKey partkey = RelationGetPartitionKey(rel); + char *parttype; + + switch (partkey->strategy) + { + case PARTITION_STRATEGY_RANGE: + parttype = "RANGE"; + break; + case PARTITION_STRATEGY_LIST: + parttype = "LIST"; + break; + case PARTITION_STRATEGY_HASH: + parttype = "HASH"; + break; + default: + parttype = "UNKNOWN"; + break; + } + + appendStringInfo(&buf, " PARTITION BY %s (", parttype); + + /* Add partition key columns */ + for (i = 0; i < partkey->partnatts; i++) + { + AttrNumber attnum = partkey->partattrs[i]; + char *attname; + + if (i > 0) + appendStringInfoString(&buf, ", "); + + if (attnum > 0) + { + /* Regular column */ + attname = get_attname(relid, attnum, false); + appendStringInfo(&buf, "%s", quote_identifier(attname)); + } + else + { + /* Expression */ + Node *partexpr = list_nth(partkey->partexprs, + partkey->partattrs[i] - 1 - FirstLowInvalidHeapAttributeNumber); + char *exprstr = deparse_expression(partexpr, + deparse_context_for(RelationGetRelationName(rel), relid), + false, false); + + appendStringInfo(&buf, "(%s)", exprstr); + } + + /* Add collation if specified */ + if (OidIsValid(partkey->partcollation[i])) + { + char *collname = get_collation_name(partkey->partcollation[i]); + + if (collname && strcmp(collname, "default") != 0) + appendStringInfo(&buf, " COLLATE %s", quote_identifier(collname)); + } + } + + appendStringInfoString(&buf, ")"); + } + + /* Add WITH options */ + { + bool first_option = true; + + /* Fill factor - use RelationGetFillFactor */ + if (relform->relkind == RELKIND_RELATION) + { + int fillfactor = RelationGetFillFactor(rel, HEAP_DEFAULT_FILLFACTOR); + + if (fillfactor != HEAP_DEFAULT_FILLFACTOR) + { + if (first_option) + { + appendStringInfoString(&buf, " WITH ("); + first_option = false; + } + else + appendStringInfoString(&buf, ", "); + + appendStringInfo(&buf, "fillfactor=%d", fillfactor); + } + } + + /* Toast tuple target */ + if (relform->reltoastrelid != InvalidOid) + { + Relation toast_rel = relation_open(relform->reltoastrelid, AccessShareLock); + + if (toast_rel->rd_options) + { + StdRdOptions *toast_options = (StdRdOptions *) toast_rel->rd_options; /* XXX check if this is the right test */ - if (toast_options->toast_tuple_target != TOAST_TUPLE_TARGET) - { - if (first_option) - { - appendStringInfoString(&buf, " WITH ("); - first_option = false; - } - else - appendStringInfoString(&buf, ", "); - - appendStringInfo(&buf, "toast_tuple_target=%d", - toast_options->toast_tuple_target); - } - } - relation_close(toast_rel, AccessShareLock); - } - - if (!first_option) - appendStringInfoString(&buf, ")"); - } - - /* Add tablespace */ - if (OidIsValid(relform->reltablespace)) - { - tablespacename = get_tablespace_name(relform->reltablespace); - if (tablespacename) - appendStringInfo(&buf, " TABLESPACE %s", quote_identifier(tablespacename)); - } - - /* Add ON COMMIT clause for temporary tables */ - if (relform->relpersistence == RELPERSISTENCE_TEMP) - { - /* Get ON COMMIT from local oncommits structure */ - OnCommitAction oncommit = get_on_commit_action(relid); - - /* Only add ON COMMIT clause if we successfully determined the behavior */ - switch (oncommit) - { - case ONCOMMIT_DELETE_ROWS: - appendStringInfoString(&buf, " ON COMMIT DELETE ROWS"); - break; - case ONCOMMIT_PRESERVE_ROWS: - appendStringInfoString(&buf, " ON COMMIT PRESERVE ROWS"); - break; - case ONCOMMIT_DROP: - appendStringInfoString(&buf, " ON COMMIT DROP"); - break; - case ONCOMMIT_NOOP: - default: - /* Don't add anything for default behavior */ - break; - } - } + if (toast_options->toast_tuple_target != TOAST_TUPLE_TARGET) + { + if (first_option) + { + appendStringInfoString(&buf, " WITH ("); + first_option = false; + } + else + appendStringInfoString(&buf, ", "); + + appendStringInfo(&buf, "toast_tuple_target=%d", + toast_options->toast_tuple_target); + } + } + relation_close(toast_rel, AccessShareLock); + } + + if (!first_option) + appendStringInfoString(&buf, ")"); + } + + /* Add tablespace */ + if (OidIsValid(relform->reltablespace)) + { + tablespacename = get_tablespace_name(relform->reltablespace); + if (tablespacename) + appendStringInfo(&buf, " TABLESPACE %s", quote_identifier(tablespacename)); + } + + /* Add ON COMMIT clause for temporary tables */ + if (relform->relpersistence == RELPERSISTENCE_TEMP) + { + /* Get ON COMMIT from local oncommits structure */ + OnCommitAction oncommit = get_on_commit_action(relid); + + /* + * Only add ON COMMIT clause if we successfully determined the + * behavior + */ + switch (oncommit) + { + case ONCOMMIT_DELETE_ROWS: + appendStringInfoString(&buf, " ON COMMIT DELETE ROWS"); + break; + case ONCOMMIT_PRESERVE_ROWS: + appendStringInfoString(&buf, " ON COMMIT PRESERVE ROWS"); + break; + case ONCOMMIT_DROP: + appendStringInfoString(&buf, " ON COMMIT DROP"); + break; + case ONCOMMIT_NOOP: + default: + /* Don't add anything for default behavior */ + break; + } + } appendStringInfoString(&buf, ";"); - /* Close the relation */ - relation_close(rel, AccessShareLock); + /* Close the relation */ + relation_close(rel, AccessShareLock); - PG_RETURN_TEXT_P(cstring_to_text(buf.data)); + PG_RETURN_TEXT_P(cstring_to_text(buf.data)); }