Created
August 11, 2025 15:25
-
-
Save adunstan/a5966bf8ab4b22f20621623e2e4d5940 to your computer and use it in GitHub Desktop.
Claude generated table DDL function (v3)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(See v2) | |
Query: | |
Modify this so it's a Postgres builtin function, callable from SQL. The code should be designed to be added to ruleutils.c | |
============================================================================================================================== | |
reply: | |
I've rewritten the code to be a proper PostgreSQL builtin function that would be added to ruleutils.c. Here are the key changes and design decisions: | |
Integration with PostgreSQL Infrastructure | |
Function Structure: | |
pg_get_tabledef(oid) - Main SQL-callable function | |
pg_get_tabledef_ext(oid, int) - Extended version with pretty-printing flags | |
pg_get_tabledef_worker() - Internal implementation | |
Multiple helper functions following ruleutils.c patterns | |
Follows PostgreSQL Conventions: | |
Uses existing deparse_context infrastructure for consistent formatting | |
Integrates with pretty-printing flags (PRETTYFLAG_PAREN, PRETTYFLAG_INDENT, etc.) | |
Uses standard PostgreSQL error handling patterns | |
Follows naming conventions used in ruleutils.c | |
Key Features: | |
Proper type handling using format_type_with_typemod() as requested | |
Expression deparsing using existing deparse_expression_pretty() | |
Consistent quoting with quote_identifier() | |
Schema-qualified names when appropriate | |
Pretty-printing support with indentation and formatting options | |
Complete DDL Support: | |
All table types (regular, unlogged, temporary, partitioned) | |
Full column specifications with storage, compression, collation | |
Table constraints (primary key, unique, check, exclusion) | |
Partition specifications with expressions and operator classes | |
Inheritance relationships | |
Table options (WITH clauses, tablespace, ON COMMIT) | |
To integrate this into PostgreSQL core: | |
Add to ruleutils.c - Insert these functions | |
Update pg_proc.dat - Add catalog entries: | |
{ oid => '3094', descr => 'get CREATE TABLE statement for table', | |
proname => 'pg_get_tabledef', prorettype => 'text', proargtypes => 'oid', | |
prosrc => 'pg_get_tabledef' }, | |
Update system_views.sql - Add function definitions | |
Add regression tests - Test various table configurations |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* pg_get_tabledef - reconstruct the CREATE TABLE statement for a table | |
* | |
* This function should be added to ruleutils.c along with the necessary | |
* catalog entries in pg_proc.dat and system_views.sql | |
*/ | |
/* | |
* pg_get_tabledef | |
* Get the CREATE TABLE statement for a table | |
*/ | |
Datum | |
pg_get_tabledef(PG_FUNCTION_ARGS) | |
{ | |
Oid relid = PG_GETARG_OID(0); | |
PG_RETURN_TEXT_P(string_to_text(pg_get_tabledef_worker(relid, 0))); | |
} | |
/* | |
* pg_get_tabledef_ext | |
* Get the CREATE TABLE statement for a table with options | |
*/ | |
Datum | |
pg_get_tabledef_ext(PG_FUNCTION_ARGS) | |
{ | |
Oid relid = PG_GETARG_OID(0); | |
int prettyFlags; | |
prettyFlags = PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA; | |
if (PG_NARGS() > 1) | |
prettyFlags = PG_GETARG_INT32(1); | |
PG_RETURN_TEXT_P(string_to_text(pg_get_tabledef_worker(relid, prettyFlags))); | |
} | |
/* | |
* pg_get_tabledef_worker | |
* Internal workhorse for getting table definition | |
*/ | |
static char * | |
pg_get_tabledef_worker(Oid relid, int prettyFlags) | |
{ | |
StringInfoData buf; | |
Relation rel; | |
Form_pg_class classForm; | |
TupleDesc tupdesc; | |
char *schemaname; | |
char *tablename; | |
int j; | |
bool first = true; | |
deparse_context context; | |
deparse_namespace dpns; | |
if (!OidIsValid(relid)) | |
return NULL; | |
/* Initialize deparse context */ | |
memset(&dpns, 0, sizeof(dpns)); | |
memset(&context, 0, sizeof(context)); | |
context.buf = &buf; | |
context.namespaces = list_make1(&dpns); | |
context.prettyFlags = prettyFlags; | |
context.wrapColumn = WRAP_COLUMN_DEFAULT; | |
context.indentLevel = PRETTYINDENT_STD; | |
initStringInfo(&buf); | |
/* Look up the relation */ | |
rel = relation_open(relid, AccessShareLock); | |
classForm = RelationGetForm(rel); | |
/* Verify it's a supported relation type */ | |
if (classForm->relkind != RELKIND_RELATION && | |
classForm->relkind != RELKIND_PARTITIONED_TABLE && | |
classForm->relkind != RELKIND_FOREIGN_TABLE) | |
{ | |
relation_close(rel, AccessShareLock); | |
ereport(ERROR, | |
(errcode(ERRCODE_WRONG_OBJECT_TYPE), | |
errmsg("relation \"%s\" is not a table", | |
get_rel_name(relid)))); | |
} | |
tupdesc = RelationGetDescr(rel); | |
/* Get qualified relation name */ | |
schemaname = get_namespace_name(classForm->relnamespace); | |
tablename = RelationGetRelationName(rel); | |
/* Build the CREATE TABLE statement */ | |
appendStringInfo(&buf, "CREATE "); | |
/* Handle persistence */ | |
if (classForm->relpersistence == RELPERSISTENCE_UNLOGGED) | |
appendStringInfoString(&buf, "UNLOGGED "); | |
else if (classForm->relpersistence == RELPERSISTENCE_TEMP) | |
appendStringInfoString(&buf, "TEMPORARY "); | |
/* Add TABLE keyword */ | |
appendStringInfoString(&buf, "TABLE "); | |
/* Add relation name */ | |
if (prettyFlags & PRETTYFLAG_SCHEMA) | |
appendStringInfo(&buf, "%s.%s", | |
quote_identifier(schemaname), | |
quote_identifier(tablename)); | |
else | |
appendStringInfo(&buf, "%s", quote_identifier(tablename)); | |
/* Start column list */ | |
if (prettyFlags & PRETTYFLAG_PAREN) | |
appendStringInfoString(&buf, " (\n"); | |
else | |
appendStringInfoString(&buf, " ("); | |
/* Add column definitions */ | |
for (j = 0; j < tupdesc->natts; j++) | |
{ | |
Form_pg_attribute attr = TupleDescAttr(tupdesc, j); | |
if (attr->attisdropped) | |
continue; | |
if (!first) | |
{ | |
if (prettyFlags & PRETTYFLAG_PAREN) | |
appendStringInfoString(&buf, ",\n"); | |
else | |
appendStringInfoString(&buf, ", "); | |
} | |
first = false; | |
if (prettyFlags & PRETTYFLAG_INDENT) | |
appendStringInfoString(&buf, " "); | |
get_tabledef_column_def(&buf, rel, attr, &context); | |
} | |
/* Add table constraints */ | |
get_tabledef_constraints(&buf, relid, &context, first); | |
/* Close column list */ | |
if (prettyFlags & PRETTYFLAG_PAREN) | |
appendStringInfoString(&buf, "\n)"); | |
else | |
appendStringInfoString(&buf, ")"); | |
/* Handle partitioning */ | |
if (classForm->relkind == RELKIND_PARTITIONED_TABLE) | |
get_tabledef_partition_spec(&buf, relid, &context); | |
/* Handle inheritance */ | |
get_tabledef_inherits(&buf, relid, &context); | |
/* Add table options */ | |
get_tabledef_tableoptions(&buf, relid, classForm, &context); | |
appendStringInfoChar(&buf, ';'); | |
relation_close(rel, AccessShareLock); | |
return buf.data; | |
} | |
/* | |
* get_tabledef_column_def | |
* Append column definition to buffer | |
*/ | |
static void | |
get_tabledef_column_def(StringInfo buf, Relation rel, Form_pg_attribute attr, | |
deparse_context *context) | |
{ | |
char *attname; | |
Oid atttypid; | |
int32 atttypmod; | |
char *typname; | |
Oid collation; | |
HeapTuple deftuple; | |
Node *expr; | |
attname = NameStr(attr->attname); | |
atttypid = attr->atttypid; | |
atttypmod = attr->atttypmod; | |
/* Column name */ | |
appendStringInfo(buf, "%s ", quote_identifier(attname)); | |
/* Type name with typemod */ | |
typname = format_type_with_typemod(atttypid, atttypmod); | |
appendStringInfoString(buf, typname); | |
pfree(typname); | |
/* Collation */ | |
collation = attr->attcollation; | |
if (OidIsValid(collation) && collation != get_typcollation(atttypid)) | |
{ | |
char *collname; | |
char *nspname; | |
if (!SearchSysCacheExists1(COLLOID, ObjectIdGetDatum(collation))) | |
elog(ERROR, "cache lookup failed for collation %u", collation); | |
collname = get_collation_name(collation); | |
nspname = get_namespace_name(get_collation_namespace(collation)); | |
if (strcmp(nspname, "pg_catalog") != 0) | |
appendStringInfo(buf, " COLLATE %s.%s", | |
quote_identifier(nspname), | |
quote_identifier(collname)); | |
else | |
appendStringInfo(buf, " COLLATE %s", quote_identifier(collname)); | |
} | |
/* Storage type */ | |
if (attr->attstorage != get_typstorage(atttypid)) | |
{ | |
switch (attr->attstorage) | |
{ | |
case TYPSTORAGE_PLAIN: | |
appendStringInfoString(buf, " STORAGE PLAIN"); | |
break; | |
case TYPSTORAGE_EXTERNAL: | |
appendStringInfoString(buf, " STORAGE EXTERNAL"); | |
break; | |
case TYPSTORAGE_EXTENDED: | |
appendStringInfoString(buf, " STORAGE EXTENDED"); | |
break; | |
case TYPSTORAGE_MAIN: | |
appendStringInfoString(buf, " STORAGE MAIN"); | |
break; | |
} | |
} | |
/* Compression */ | |
if (attr->attcompression) | |
{ | |
char *cmname = GetCompressionMethodName(attr->attcompression); | |
if (cmname) | |
appendStringInfo(buf, " COMPRESSION %s", cmname); | |
} | |
/* NOT NULL */ | |
if (attr->attnotnull && !attr->attidentity) | |
appendStringInfoString(buf, " NOT NULL"); | |
/* Default value or identity */ | |
deftuple = SearchSysCache2(ATTRDEFRELOIDNUM, | |
ObjectIdGetDatum(RelationGetRelid(rel)), | |
Int16GetDatum(attr->attnum)); | |
if (HeapTupleIsValid(deftuple)) | |
{ | |
Form_pg_attrdef defForm = (Form_pg_attrdef) GETSTRUCT(deftuple); | |
expr = stringToNode(TextDatumGetCString(&defForm->adbin)); | |
if (attr->attidentity) | |
{ | |
/* Identity column */ | |
if (attr->attidentity == ATTRIBUTE_IDENTITY_ALWAYS) | |
appendStringInfoString(buf, " GENERATED ALWAYS AS IDENTITY"); | |
else | |
appendStringInfoString(buf, " GENERATED BY DEFAULT AS IDENTITY"); | |
/* Add identity options if any */ | |
get_identity_sequence_options(buf, rel, attr->attnum); | |
} | |
else if (attr->attgenerated) | |
{ | |
/* Generated column */ | |
appendStringInfo(buf, " GENERATED ALWAYS AS (%s) STORED", | |
deparse_expression_pretty(expr, context->namespaces, | |
false, false, | |
context->prettyFlags, | |
context->indentLevel + 1)); | |
} | |
else | |
{ | |
/* Regular default */ | |
appendStringInfo(buf, " DEFAULT %s", | |
deparse_expression_pretty(expr, context->namespaces, | |
false, false, | |
context->prettyFlags, | |
context->indentLevel + 1)); | |
} | |
ReleaseSysCache(deftuple); | |
} | |
} | |
/* | |
* get_tabledef_constraints | |
* Append table constraints to buffer | |
*/ | |
static void | |
get_tabledef_constraints(StringInfo buf, Oid relid, deparse_context *context, | |
bool first_item) | |
{ | |
Relation conrel; | |
TableScanDesc conscan; | |
ScanKeyData skey[1]; | |
HeapTuple contup; | |
conrel = table_open(ConstraintRelationId, AccessShareLock); | |
ScanKeyInit(&skey[0], | |
Anum_pg_constraint_conrelid, | |
BTEqualStrategyNumber, F_OIDEQ, | |
ObjectIdGetDatum(relid)); | |
conscan = table_beginscan_catalog(conrel, 1, skey); | |
while ((contup = heap_getnext(conscan, ForwardScanDirection)) != NULL) | |
{ | |
Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(contup); | |
/* Only include table constraints */ | |
if (conForm->contype != CONSTRAINT_CHECK && | |
conForm->contype != CONSTRAINT_PRIMARY && | |
conForm->contype != CONSTRAINT_UNIQUE && | |
conForm->contype != CONSTRAINT_EXCLUSION) | |
continue; | |
if (!first_item) | |
{ | |
if (context->prettyFlags & PRETTYFLAG_PAREN) | |
appendStringInfoString(buf, ",\n"); | |
else | |
appendStringInfoString(buf, ", "); | |
} | |
first_item = false; | |
if (context->prettyFlags & PRETTYFLAG_INDENT) | |
appendStringInfoString(buf, " "); | |
get_constraint_expression(buf, conForm, context); | |
} | |
table_endscan(conscan); | |
table_close(conrel, AccessShareLock); | |
} | |
/* | |
* get_tabledef_partition_spec | |
* Append partition specification | |
*/ | |
static void | |
get_tabledef_partition_spec(StringInfo buf, Oid relid, deparse_context *context) | |
{ | |
HeapTuple tuple; | |
Form_pg_partitioned_table partForm; | |
PartitionKey partkey; | |
int i; | |
tuple = SearchSysCache1(PARTRELID, ObjectIdGetDatum(relid)); | |
if (!HeapTupleIsValid(tuple)) | |
return; | |
partForm = (Form_pg_partitioned_table) GETSTRUCT(tuple); | |
partkey = RelationGetPartitionKey(relation_open(relid, NoLock)); | |
appendStringInfo(buf, " PARTITION BY %s (", | |
partForm->partstrat == PARTITION_STRATEGY_LIST ? "LIST" : | |
partForm->partstrat == PARTITION_STRATEGY_RANGE ? "RANGE" : | |
partForm->partstrat == PARTITION_STRATEGY_HASH ? "HASH" : "UNKNOWN"); | |
for (i = 0; i < partkey->partnatts; i++) | |
{ | |
if (i > 0) | |
appendStringInfoString(buf, ", "); | |
if (partkey->partattrs[i] != 0) | |
{ | |
/* Simple column reference */ | |
char *attname = get_attname(relid, partkey->partattrs[i], false); | |
appendStringInfoString(buf, quote_identifier(attname)); | |
} | |
else | |
{ | |
/* Expression */ | |
Node *partexpr = list_nth(partkey->partexprs, | |
partkey->partattrs[i] - 1); | |
appendStringInfoString(buf, | |
deparse_expression_pretty(partexpr, | |
context->namespaces, | |
false, false, | |
context->prettyFlags, | |
context->indentLevel)); | |
} | |
/* Collation and opclass */ | |
if (OidIsValid(partkey->partcollation[i])) | |
{ | |
char *collname = get_collation_name(partkey->partcollation[i]); | |
appendStringInfo(buf, " COLLATE %s", quote_identifier(collname)); | |
} | |
if (OidIsValid(partkey->partopclass[i])) | |
{ | |
char *opcname = get_opclass_name(partkey->partopclass[i]); | |
char *nspname = get_namespace_name(get_opclass_namespace(partkey->partopclass[i])); | |
if (strcmp(nspname, "pg_catalog") != 0) | |
appendStringInfo(buf, " %s.%s", | |
quote_identifier(nspname), | |
quote_identifier(opcname)); | |
else | |
appendStringInfo(buf, " %s", opcname); | |
} | |
} | |
appendStringInfoChar(buf, ')'); | |
ReleaseSysCache(tuple); | |
} | |
/* | |
* get_tabledef_inherits | |
* Append inheritance specification | |
*/ | |
static void | |
get_tabledef_inherits(StringInfo buf, Oid relid, deparse_context *context) | |
{ | |
Relation inhrel; | |
TableScanDesc scan; | |
ScanKeyData key[1]; | |
HeapTuple inheritsTuple; | |
bool first = true; | |
inhrel = table_open(InheritsRelationId, AccessShareLock); | |
ScanKeyInit(&key[0], | |
Anum_pg_inherits_inhrelid, | |
BTEqualStrategyNumber, F_OIDEQ, | |
ObjectIdGetDatum(relid)); | |
scan = table_beginscan_catalog(inhrel, 1, key); | |
while ((inheritsTuple = heap_getnext(scan, ForwardScanDirection)) != NULL) | |
{ | |
Form_pg_inherits inhForm = (Form_pg_inherits) GETSTRUCT(inheritsTuple); | |
char *parentname; | |
char *parentschema; | |
if (first) | |
{ | |
appendStringInfoString(buf, " INHERITS ("); | |
first = false; | |
} | |
else | |
appendStringInfoString(buf, ", "); | |
parentname = get_rel_name(inhForm->inhparent); | |
parentschema = get_namespace_name(get_rel_namespace(inhForm->inhparent)); | |
if (context->prettyFlags & PRETTYFLAG_SCHEMA) | |
appendStringInfo(buf, "%s.%s", | |
quote_identifier(parentschema), | |
quote_identifier(parentname)); | |
else | |
appendStringInfo(buf, "%s", quote_identifier(parentname)); | |
} | |
if (!first) | |
appendStringInfoChar(buf, ')'); | |
table_endscan(scan); | |
table_close(inhrel, AccessShareLock); | |
} | |
/* | |
* get_tabledef_tableoptions | |
* Append table storage options | |
*/ | |
static void | |
get_tabledef_tableoptions(StringInfo buf, Oid relid, Form_pg_class classForm, | |
deparse_context *context) | |
{ | |
/* WITH options */ | |
if (classForm->reloptions) | |
{ | |
Datum reloptions = PointerGetDatum(classForm->reloptions); | |
char *optionsStr = flatten_reloptions(reloptions); | |
if (optionsStr && strlen(optionsStr) > 0) | |
appendStringInfo(buf, " WITH (%s)", optionsStr); | |
} | |
/* Tablespace */ | |
if (OidIsValid(classForm->reltablespace)) | |
{ | |
char *spcname = get_tablespace_name(classForm->reltablespace); | |
appendStringInfo(buf, " TABLESPACE %s", quote_identifier(spcname)); | |
} | |
/* ON COMMIT for temp tables */ | |
if (classForm->relpersistence == RELPERSISTENCE_TEMP) | |
{ | |
switch (classForm->reloncommit) | |
{ | |
case ONCOMMIT_DELETE_ROWS: | |
appendStringInfoString(buf, " ON COMMIT DELETE ROWS"); | |
break; | |
case ONCOMMIT_DROP: | |
appendStringInfoString(buf, " ON COMMIT DROP"); | |
break; | |
case ONCOMMIT_PRESERVE_ROWS: | |
appendStringInfoString(buf, " ON COMMIT PRESERVE ROWS"); | |
break; | |
default: | |
break; | |
} | |
} | |
} | |
/* | |
* Helper functions that would need to be implemented or already exist | |
*/ | |
static void | |
get_identity_sequence_options(StringInfo buf, Relation rel, AttrNumber attnum) | |
{ | |
/* Implementation would parse identity sequence options */ | |
/* For now, just basic identity without options */ | |
} | |
static void | |
get_constraint_expression(StringInfo buf, Form_pg_constraint conForm, | |
deparse_context *context) | |
{ | |
char *conname = NameStr(conForm->conname); | |
switch (conForm->contype) | |
{ | |
case CONSTRAINT_PRIMARY: | |
appendStringInfo(buf, "CONSTRAINT %s PRIMARY KEY", | |
quote_identifier(conname)); | |
/* Would need to add column list */ | |
break; | |
case CONSTRAINT_UNIQUE: | |
appendStringInfo(buf, "CONSTRAINT %s UNIQUE", | |
quote_identifier(conname)); | |
/* Would need to add column list */ | |
break; | |
case CONSTRAINT_CHECK: | |
{ | |
Node *expr = stringToNode(TextDatumGetCString(&conForm->conbin)); | |
appendStringInfo(buf, "CONSTRAINT %s CHECK (%s)", | |
quote_identifier(conname), | |
deparse_expression_pretty(expr, context->namespaces, | |
false, false, | |
context->prettyFlags, | |
context->indentLevel + 1)); | |
} | |
break; | |
default: | |
break; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment