(to be clear I only skimmed the end of this thread and did not look at all the previous messages)
I took a quick look at the first patch (about deparsing table ddl) and it seems like this would also be very useful for a SHOW CREATE TABLE, like command. Which was suggested in this thread: https://www.postgresql.org/message-id/flat/CAFEN2wxsDSSuOvrU03CE33ZphVLqtyh9viPp6huODCDx2UQkYA%40mail.gmail.com On that thread I sent a patch with Citus its CREATE TABLE deparsing as starting point. It looks like this thread went quite a different route with some JSON intermediary representation. Still it might be useful to look at the patch with Citus its logic for some inspiration/copying things. I re-attached that patch here for ease of finding it.
From ddb375afc74339bd0eaf0c272d06805637fd85cc Mon Sep 17 00:00:00 2001 From: Jelte Fennema <jelte.fennema@microsoft.com> Date: Mon, 5 Jun 2023 12:32:12 +0200 Subject: [PATCH v1] Add initial code to generate table definition SQL This patch adds various functions which generate table DDL statements based on a table oid. These functions originated in Citus source code, but are now contributed to upstream Postgres by means of this patch. It currently contains no wrappers around these C functions that can be called from SQL. --- src/backend/utils/adt/acl.c | 2 +- src/backend/utils/adt/ruleutils.c | 1187 ++++++++++++++++++++++++++++- src/include/utils/acl.h | 1 + src/include/utils/ruleutils.h | 51 ++ src/tools/pgindent/typedefs.list | 2 + 5 files changed, 1240 insertions(+), 3 deletions(-) diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index c660fd3e701..abe329d4c83 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -1702,7 +1702,7 @@ convert_any_priv_string(text *priv_type_text, } -static const char * +const char * convert_aclright_to_string(int aclright) { switch (aclright) diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index d3a973d86b7..30bb4e7ffd9 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -24,12 +24,16 @@ #include "access/relation.h" #include "access/sysattr.h" #include "access/table.h" +#include "access/toast_compression.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_extension.h" +#include "catalog/pg_foreign_data_wrapper.h" #include "catalog/pg_language.h" #include "catalog/pg_opclass.h" #include "catalog/pg_operator.h" @@ -39,10 +43,13 @@ #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" #include "commands/defrem.h" +#include "commands/extension.h" +#include "commands/sequence.h" #include "commands/tablespace.h" #include "common/keywords.h" #include "executor/spi.h" #include "funcapi.h" +#include "foreign/foreign.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "nodes/makefuncs.h" @@ -59,6 +66,7 @@ #include "rewrite/rewriteHandler.h" #include "rewrite/rewriteManip.h" #include "rewrite/rewriteSupport.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/fmgroids.h" @@ -507,7 +515,6 @@ static Node *processIndirection(Node *node, deparse_context *context); static void printSubscripts(SubscriptingRef *sbsref, deparse_context *context); static char *get_relation_name(Oid relid); static char *generate_relation_name(Oid relid, List *namespaces); -static char *generate_qualified_relation_name(Oid relid); static char *generate_function_name(Oid funcid, int nargs, List *argnames, Oid *argtypes, bool has_variadic, bool *use_variadic_p, @@ -521,6 +528,10 @@ static void get_reloptions(StringInfo buf, Datum reloptions); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") +#define CREATE_SEQUENCE_COMMAND \ + "CREATE %sSEQUENCE IF NOT EXISTS %s AS %s INCREMENT BY " INT64_FORMAT \ + " MINVALUE " INT64_FORMAT " MAXVALUE " INT64_FORMAT \ + " START WITH " INT64_FORMAT " CACHE " INT64_FORMAT " %sCYCLE" /* ---------- * pg_get_ruledef - Do it all and return a text @@ -12110,7 +12121,7 @@ generate_relation_name(Oid relid, List *namespaces) * * As above, but unconditionally schema-qualify the name. */ -static char * +char * generate_qualified_relation_name(Oid relid) { HeapTuple tp; @@ -12606,3 +12617,1175 @@ get_range_partbound_string(List *bound_datums) return buf->data; } + +/* + * pg_get_extensiondef_string finds the foreign data wrapper that corresponds to + * the given foreign tableId, and checks if an extension owns this foreign data + * wrapper. If it does, the function returns the extension's definition. If not, + * the function returns null. + */ +char * +pg_get_extensiondef_string(Oid tableRelationId) +{ + ForeignTable *foreignTable = GetForeignTable(tableRelationId); + ForeignServer *server = GetForeignServer(foreignTable->serverid); + ForeignDataWrapper *foreignDataWrapper = GetForeignDataWrapper(server->fdwid); + StringInfoData buffer = {NULL, 0, 0, 0}; + + Oid classId = ForeignDataWrapperRelationId; + Oid objectId = server->fdwid; + + Oid extensionId = getExtensionOfObject(classId, objectId); + + if (OidIsValid(extensionId)) + { + char *extensionName = get_extension_name(extensionId); + Oid extensionSchemaId = get_extension_schema(extensionId); + char *extensionSchema = get_namespace_name(extensionSchemaId); + + initStringInfo(&buffer); + appendStringInfo(&buffer, "CREATE EXTENSION IF NOT EXISTS %s WITH SCHEMA %s", + quote_identifier(extensionName), + quote_identifier(extensionSchema)); + } + else + { + ereport(NOTICE, (errmsg("foreign-data wrapper \"%s\" does not have an " + "extension defined", foreignDataWrapper->fdwname))); + } + + return (buffer.data); +} + +/* + * get_extension_version - given an extension OID, fetch its extversion + * or NULL if not found. + */ +char * +get_extension_version(Oid extensionId) +{ + char *versionName = NULL; + + Relation relation = table_open(ExtensionRelationId, AccessShareLock); + SysScanDesc scanDesc; + HeapTuple tuple; + + ScanKeyData entry[1]; + + ScanKeyInit(&entry[0], + Anum_pg_extension_oid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(extensionId)); + + scanDesc = systable_beginscan(relation, ExtensionOidIndexId, true, + NULL, 1, entry); + + tuple = systable_getnext(scanDesc); + + /* We assume that there can be at most one matching tuple */ + if (HeapTupleIsValid(tuple)) + { + bool isNull = false; + Datum versionDatum = heap_getattr(tuple, Anum_pg_extension_extversion, + RelationGetDescr(relation), &isNull); + + if (!isNull) + { + versionName = text_to_cstring(DatumGetTextPP(versionDatum)); + } + } + + systable_endscan(scanDesc); + + table_close(relation, AccessShareLock); + + return versionName; +} + +/* + * pg_get_serverdef_string finds the foreign server that corresponds to the + * given foreign tableId, and returns this server's definition. + */ +char * +pg_get_serverdef_string(Oid tableRelationId) +{ + ForeignTable *foreignTable = GetForeignTable(tableRelationId); + ForeignServer *server = GetForeignServer(foreignTable->serverid); + ForeignDataWrapper *foreignDataWrapper = GetForeignDataWrapper(server->fdwid); + + StringInfoData buffer = {NULL, 0, 0, 0}; + + initStringInfo(&buffer); + + appendStringInfo(&buffer, "CREATE SERVER IF NOT EXISTS %s", + quote_identifier(server->servername)); + if (server->servertype != NULL) + { + appendStringInfo(&buffer, " TYPE %s", + quote_literal_cstr(server->servertype)); + } + if (server->serverversion != NULL) + { + appendStringInfo(&buffer, " VERSION %s", + quote_literal_cstr(server->serverversion)); + } + + appendStringInfo(&buffer, " FOREIGN DATA WRAPPER %s", + quote_identifier(foreignDataWrapper->fdwname)); + + /* append server options, if any */ + AppendOptionListToString(&buffer, server->options); + + return (buffer.data); +} + +/* + * pg_get_sequencedef_string returns the definition of a given sequence. This + * definition includes explicit values for all CREATE SEQUENCE options. + */ +char * +pg_get_sequencedef_string(Oid sequenceRelationId) +{ + Form_pg_sequence pgSequenceForm = pg_get_sequencedef(sequenceRelationId); + + /* build our DDL command */ + char *qualifiedSequenceName = generate_qualified_relation_name(sequenceRelationId); + char *typeName = format_type_be(pgSequenceForm->seqtypid); + + char *sequenceDef = psprintf(CREATE_SEQUENCE_COMMAND, + get_rel_persistence(sequenceRelationId) == + RELPERSISTENCE_UNLOGGED ? "UNLOGGED " : "", + qualifiedSequenceName, + typeName, + pgSequenceForm->seqincrement, pgSequenceForm->seqmin, + pgSequenceForm->seqmax, pgSequenceForm->seqstart, + pgSequenceForm->seqcache, + pgSequenceForm->seqcycle ? "" : "NO "); + + return sequenceDef; +} + +/* + * pg_get_sequencedef returns the Form_pg_sequence data about the sequence with the given + * object id. + */ +Form_pg_sequence +pg_get_sequencedef(Oid sequenceRelationId) +{ + Form_pg_sequence pgSequenceForm; + HeapTuple heapTuple = SearchSysCache1(SEQRELID, sequenceRelationId); + + if (!HeapTupleIsValid(heapTuple)) + { + elog(ERROR, "cache lookup failed for sequence %u", sequenceRelationId); + } + + pgSequenceForm = (Form_pg_sequence) GETSTRUCT(heapTuple); + + ReleaseSysCache(heapTuple); + + return pgSequenceForm; +} + +/* + * RegularTable function returns true if given table's relation kind is RELKIND_RELATION + * or RELKIND_PARTITIONED_TABLE otherwise it returns false. + */ +bool +RegularTable(Oid relationId) +{ + char relationKind = get_rel_relkind(relationId); + + if (relationKind == RELKIND_RELATION || relationKind == RELKIND_PARTITIONED_TABLE) + { + return true; + } + + return false; +} + +/* + * pg_get_tableschemadef_string returns the definition of a given table. This + * definition includes table's schema, default column values, not null and check + * constraints. The definition does not include constraints that trigger index + * creations; specifically, unique and primary key constraints are excluded. + * When includeSequenceDefaults is INCLUDE_SEQUENCE_DEFAULTS, the function also creates + * DEFAULT clauses for columns getting their default values from a sequence. + * When IncludeIdentities is NO_IDENTITY, the function does not include identity column + * specifications. When it's INCLUDE_IDENTITY it creates GENERATED .. AS IDENTIY clauses. + */ +char * +pg_get_tableschemadef_string(Oid tableRelationId, + IncludeSequenceDefaults includeSequenceDefaults, + IncludeIdentities includeIdentityDefaults, + char *accessMethod) +{ + bool firstAttributePrinted = false; + AttrNumber defaultValueIndex = 0; + AttrNumber constraintIndex = 0; + AttrNumber constraintCount = 0; + StringInfoData buffer = {NULL, 0, 0, 0}; + + /* + * Instead of retrieving values from system catalogs as other functions in + * ruleutils.c do, we follow an unusual approach here: we open the + * relation, and fetch the relation's tuple descriptor. We do this because + * the tuple descriptor already contains information harnessed from + * pg_attrdef, pg_attribute, pg_constraint, and pg_class; and therefore + * using the descriptor saves us from a lot of additional work. + */ + Relation relation = relation_open(tableRelationId, AccessShareLock); + char *relationName = generate_relation_name(tableRelationId, NIL); + char relationKind; + TupleDesc tupleDescriptor; + TupleConstr *tupleConstraints; + + /* TODO: Actually implement functionality */ + /* EnsureRelationKindSupported(tableRelationId); */ + + initStringInfo(&buffer); + + if (RegularTable(tableRelationId)) + { + appendStringInfoString(&buffer, "CREATE "); + + if (relation->rd_rel->relpersistence == RELPERSISTENCE_UNLOGGED) + { + appendStringInfoString(&buffer, "UNLOGGED "); + } + + appendStringInfo(&buffer, "TABLE %s (", relationName); + } + else + { + appendStringInfo(&buffer, "CREATE FOREIGN TABLE %s (", relationName); + } + + /* + * Iterate over the table's columns. If a particular column is not dropped + * and is not inherited from another table, print the column's name and + * its formatted type. + */ + tupleDescriptor = RelationGetDescr(relation); + tupleConstraints = tupleDescriptor->constr; + + for (int attributeIndex = 0; attributeIndex < tupleDescriptor->natts; + attributeIndex++) + { + Form_pg_attribute attributeForm = TupleDescAttr(tupleDescriptor, attributeIndex); + + /* + * We disregard the inherited attributes (i.e., attinhcount > 0) here. + * The reasoning behind this is that Citus implements declarative + * partitioning by creating the partitions first and then sending + * "ALTER TABLE parent_table ATTACH PARTITION .." command. This may + * not play well with regular inherited tables, which isn't a big + * concern from Citus' perspective. + */ + if (!attributeForm->attisdropped) + { + const char *attributeName = NameStr(attributeForm->attname); + + const char *attributeTypeName = format_type_with_typemod( + attributeForm->atttypid, + attributeForm-> + atttypmod); + + if (firstAttributePrinted) + { + appendStringInfoString(&buffer, ", "); + } + firstAttributePrinted = true; + + appendStringInfo(&buffer, "%s ", quote_identifier(attributeName)); + appendStringInfoString(&buffer, attributeTypeName); + + if (CompressionMethodIsValid(attributeForm->attcompression)) + { + appendStringInfo(&buffer, " COMPRESSION %s", + GetCompressionMethodName(attributeForm->attcompression)); + } + + if (attributeForm->attidentity && includeIdentityDefaults) + { + bool missing_ok = false; + Oid seqOid = getIdentitySequence(RelationGetRelid(relation), + attributeForm->attnum, missing_ok); + + if (includeIdentityDefaults == INCLUDE_IDENTITY) + { + Form_pg_sequence pgSequenceForm = pg_get_sequencedef(seqOid); + char *sequenceDef = psprintf( + " GENERATED %s AS IDENTITY (INCREMENT BY " INT64_FORMAT \ + " MINVALUE " INT64_FORMAT " MAXVALUE " + INT64_FORMAT \ + " START WITH " INT64_FORMAT " CACHE " + INT64_FORMAT " %sCYCLE)", + attributeForm->attidentity == ATTRIBUTE_IDENTITY_ALWAYS ? + "ALWAYS" : "BY DEFAULT", + pgSequenceForm->seqincrement, + pgSequenceForm->seqmin, + pgSequenceForm->seqmax, + pgSequenceForm->seqstart, + pgSequenceForm->seqcache, + pgSequenceForm->seqcycle ? "" : "NO "); + + appendStringInfo(&buffer, "%s", sequenceDef); + } + } + + /* if this column has a default value, append the default value */ + if (attributeForm->atthasdef) + { + List *defaultContext = NULL; + char *defaultString = NULL; + AttrDefault *defaultValueList; + AttrDefault *defaultValue; + Node *defaultNode; + + Assert(tupleConstraints != NULL); + + defaultValueList = tupleConstraints->defval; + Assert(defaultValueList != NULL); + + defaultValue = &(defaultValueList[defaultValueIndex]); + defaultValueIndex++; + + Assert(defaultValue->adnum == (attributeIndex + 1)); + Assert(defaultValueIndex <= tupleConstraints->num_defval); + + /* + * convert expression to node tree, and prepare deparse + * context + */ + defaultNode = (Node *) stringToNode(defaultValue->adbin); + + /* + * if column default value is explicitly requested, or it is + * not set from a sequence then we include DEFAULT clause for + * this column. + */ + if (includeSequenceDefaults == INCLUDE_SEQUENCE_DEFAULTS || + !contain_nextval_expression_walker(defaultNode, NULL)) + { + defaultContext = deparse_context_for(relationName, tableRelationId); + + /* deparse default value string */ + defaultString = deparse_expression(defaultNode, defaultContext, + false, false); + + if (attributeForm->attgenerated == ATTRIBUTE_GENERATED_STORED) + { + appendStringInfo(&buffer, " GENERATED ALWAYS AS (%s) STORED", + defaultString); + } + else + { + appendStringInfo(&buffer, " DEFAULT %s", defaultString); + } + } + } + + /* if this column has a not null constraint, append the constraint */ + if (attributeForm->attnotnull) + { + appendStringInfoString(&buffer, " NOT NULL"); + } + + if (attributeForm->attcollation != InvalidOid && + attributeForm->attcollation != DEFAULT_COLLATION_OID) + { + appendStringInfo(&buffer, " COLLATE %s", generate_collation_name( + attributeForm->attcollation)); + } + } + } + + /* + * Now check if the table has any constraints. If it does, set the number + * of check constraints here. Then iterate over all check constraints and + * print them. + */ + if (tupleConstraints != NULL) + { + constraintCount = tupleConstraints->num_check; + } + + for (constraintIndex = 0; constraintIndex < constraintCount; constraintIndex++) + { + ConstrCheck *checkConstraintList = tupleConstraints->check; + ConstrCheck *checkConstraint = &(checkConstraintList[constraintIndex]); + + /* convert expression to node tree, and prepare deparse context */ + Node *checkNode = (Node *) stringToNode(checkConstraint->ccbin); + List *checkContext = deparse_context_for(relationName, tableRelationId); + + /* deparse check constraint string */ + char *checkString = deparse_expression(checkNode, checkContext, false, false); + + /* if an attribute or constraint has been printed, format properly */ + if (firstAttributePrinted || constraintIndex > 0) + { + appendStringInfoString(&buffer, ", "); + } + + appendStringInfo(&buffer, "CONSTRAINT %s CHECK ", + quote_identifier(checkConstraint->ccname)); + + appendStringInfoString(&buffer, "("); + appendStringInfoString(&buffer, checkString); + appendStringInfoString(&buffer, ")"); + } + + /* close create table's outer parentheses */ + appendStringInfoString(&buffer, ")"); + + /* + * If the relation is a foreign table, append the server name and options + * to the create table statement. + */ + relationKind = relation->rd_rel->relkind; + if (relationKind == RELKIND_FOREIGN_TABLE) + { + ForeignTable *foreignTable = GetForeignTable(tableRelationId); + ForeignServer *foreignServer = GetForeignServer(foreignTable->serverid); + + char *serverName = foreignServer->servername; + + appendStringInfo(&buffer, " SERVER %s", quote_identifier(serverName)); + AppendOptionListToString(&buffer, foreignTable->options); + } + else if (relationKind == RELKIND_PARTITIONED_TABLE) + { + char *partitioningInformation = GeneratePartitioningInformation(tableRelationId); + + appendStringInfo(&buffer, " PARTITION BY %s ", partitioningInformation); + } + + /* + * Add table access methods when the table is configured with an access + * method + */ + if (accessMethod) + { + appendStringInfo(&buffer, " USING %s", quote_identifier(accessMethod)); + } + else if (OidIsValid(relation->rd_rel->relam)) + { + Form_pg_am amForm; + HeapTuple amTup = SearchSysCache1(AMOID, ObjectIdGetDatum( + relation->rd_rel->relam)); + + if (!HeapTupleIsValid(amTup)) + { + elog(ERROR, "cache lookup failed for access method %u", + relation->rd_rel->relam); + } + amForm = (Form_pg_am) GETSTRUCT(amTup); + appendStringInfo(&buffer, " USING %s", quote_identifier(NameStr(amForm->amname))); + ReleaseSysCache(amTup); + } + + /* + * Add any reloptions (storage parameters) defined on the table in a WITH + * clause. + */ + { + char *reloptions = flatten_reloptions(tableRelationId); + + if (reloptions) + { + appendStringInfo(&buffer, " WITH (%s)", reloptions); + pfree(reloptions); + } + } + + relation_close(relation, AccessShareLock); + + return (buffer.data); +} + +/* + * EnsureRelationKindSupported errors out if the given relation is not supported + * as a distributed relation. + */ +/* + * void + * EnsureRelationKindSupported(Oid relationId) + * { + * char relationKind = get_rel_relkind(relationId); + * if (!relationKind) + * { + * ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + * errmsg("relation with OID %d does not exist", + * relationId))); + * } + * + * bool supportedRelationKind = RegularTable(relationId) || + * relationKind == RELKIND_FOREIGN_TABLE; + * + * /\* + * * Citus doesn't support bare inherited tables (i.e., not a partition or + * * partitioned table) + * *\/ + * supportedRelationKind = supportedRelationKind && !(IsChildTable(relationId) || + * IsParentTable(relationId)); + * + * if (!supportedRelationKind) + * { + * char *relationName = get_rel_name(relationId); + * + * ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), + * errmsg("%s is not a regular, foreign or partitioned table", + * relationName))); + * } + * } + */ + +/* + * pg_get_tablecolumnoptionsdef_string returns column storage type and column + * statistics definitions for given table, _if_ these definitions differ from + * their default values. The function returns null if all columns use default + * values for their storage types and statistics. + */ +char * +pg_get_tablecolumnoptionsdef_string(Oid tableRelationId) +{ + List *columnOptionList = NIL; + ListCell *columnOptionCell = NULL; + bool firstOptionPrinted = false; + StringInfoData buffer = {NULL, 0, 0, 0}; + + /* + * Instead of retrieving values from system catalogs, we open the + * relation, and use the relation's tuple descriptor to access attribute + * information. This is primarily to maintain symmetry with + * pg_get_tableschemadef. + */ + Relation relation = relation_open(tableRelationId, AccessShareLock); + + /* TODO: Actually implement functionality */ + /* EnsureRelationKindSupported(tableRelationId); */ + + /* + * Iterate over the table's columns. If a particular column is not dropped + * and is not inherited from another table, check if column storage or + * statistics statements need to be printed. + */ + TupleDesc tupleDescriptor = RelationGetDescr(relation); + AttrNumber natts; + + if (tupleDescriptor->natts > MaxAttrNumber) + { + ereport(ERROR, (errmsg("bad number of tuple descriptor attributes"))); + } + + natts = tupleDescriptor->natts; + for (AttrNumber attributeIndex = 0; + attributeIndex < natts; + attributeIndex++) + { + Form_pg_attribute attributeForm = TupleDescAttr(tupleDescriptor, attributeIndex); + char *attributeName = NameStr(attributeForm->attname); + char defaultStorageType = get_typstorage(attributeForm->atttypid); + + if (!attributeForm->attisdropped && attributeForm->attinhcount == 0) + { + /* + * If the user changed the column's default storage type, create + * alter statement and add statement to a list for later + * processing. + */ + if (attributeForm->attstorage != defaultStorageType) + { + char *storageName = 0; + StringInfoData statement = {NULL, 0, 0, 0}; + + initStringInfo(&statement); + + switch (attributeForm->attstorage) + { + case 'p': + { + storageName = "PLAIN"; + break; + } + + case 'e': + { + storageName = "EXTERNAL"; + break; + } + + case 'm': + { + storageName = "MAIN"; + break; + } + + case 'x': + { + storageName = "EXTENDED"; + break; + } + + default: + { + ereport(ERROR, (errmsg("unrecognized storage type: %c", + attributeForm->attstorage))); + break; + } + } + + appendStringInfo(&statement, "ALTER COLUMN %s ", + quote_identifier(attributeName)); + appendStringInfo(&statement, "SET STORAGE %s", storageName); + + columnOptionList = lappend(columnOptionList, statement.data); + } + + /* + * If the user changed the column's statistics target, create + * alter statement and add statement to a list for later + * processing. + */ + if (attributeForm->attstattarget >= 0) + { + StringInfoData statement = {NULL, 0, 0, 0}; + + initStringInfo(&statement); + + appendStringInfo(&statement, "ALTER COLUMN %s ", + quote_identifier(attributeName)); + appendStringInfo(&statement, "SET STATISTICS %d", + attributeForm->attstattarget); + + columnOptionList = lappend(columnOptionList, statement.data); + } + } + } + + /* + * Iterate over column storage and statistics statements that we created, + * and append them to a single alter table statement. + */ + foreach(columnOptionCell, columnOptionList) + { + char *columnOptionStatement = (char *) lfirst(columnOptionCell); + + if (!firstOptionPrinted) + { + initStringInfo(&buffer); + appendStringInfo(&buffer, "ALTER TABLE ONLY %s ", + generate_relation_name(tableRelationId, NIL)); + } + else + { + appendStringInfoString(&buffer, ", "); + } + firstOptionPrinted = true; + appendStringInfoString(&buffer, columnOptionStatement); + + pfree(columnOptionStatement); + } + + list_free(columnOptionList); + relation_close(relation, AccessShareLock); + + return (buffer.data); +} + +/* + * pg_get_indexclusterdef_string returns the definition of a cluster statement + * for given index. The function returns null if the table is not clustered on + * given index. + */ +char * +pg_get_indexclusterdef_string(Oid indexRelationId) +{ + StringInfoData buffer = {NULL, 0, 0, 0}; + Form_pg_index indexForm; + Oid tableRelationId; + + HeapTuple indexTuple = SearchSysCache(INDEXRELID, ObjectIdGetDatum(indexRelationId), + 0, 0, 0); + + if (!HeapTupleIsValid(indexTuple)) + { + ereport(ERROR, (errmsg("cache lookup failed for index %u", indexRelationId))); + } + + indexForm = (Form_pg_index) GETSTRUCT(indexTuple); + tableRelationId = indexForm->indrelid; + + /* check if the table is clustered on this index */ + if (indexForm->indisclustered) + { + char *qualifiedRelationName = + generate_qualified_relation_name(tableRelationId); + char *indexName = get_rel_name(indexRelationId); /* needs to be quoted */ + + initStringInfo(&buffer); + appendStringInfo(&buffer, "ALTER TABLE %s CLUSTER ON %s", + qualifiedRelationName, quote_identifier(indexName)); + } + + ReleaseSysCache(indexTuple); + + return (buffer.data); +} + +/* + * pg_get_table_grants returns a list of sql statements which recreate the + * permissions for a specific table. + * + * This function is modeled after aclexplode(), don't change too heavily. + */ +List * +pg_get_table_grants(Oid relationId) +{ + StringInfoData buffer; + List *defs = NIL; + bool isNull = false; + + Relation relation = relation_open(relationId, AccessShareLock); + char *relationName = generate_relation_name(relationId, NIL); + HeapTuple classTuple; + Datum aclDatum; + + initStringInfo(&buffer); + + /* lookup all table level grants */ + classTuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relationId)); + if (!HeapTupleIsValid(classTuple)) + { + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("relation with OID %u does not exist", + relationId))); + } + + aclDatum = SysCacheGetAttr(RELOID, classTuple, Anum_pg_class_relacl, + &isNull); + + ReleaseSysCache(classTuple); + + if (!isNull) + { + Acl *acl = DatumGetAclP(aclDatum); + AclItem *aidat = ACL_DAT(acl); + + int offtype = -1; + int i = 0; + + /* + * First revoke all default permissions, so we can start adding the + * exact permissions from the master. Note that we only do so if there + * are any actual grants; an empty grant set signals default + * permissions. + * + * Note: This doesn't work correctly if default permissions have been + * changed with ALTER DEFAULT PRIVILEGES - but that's hard to fix + * properly currently. + */ + appendStringInfo(&buffer, "REVOKE ALL ON %s FROM PUBLIC", + relationName); + defs = lappend(defs, pstrdup(buffer.data)); + resetStringInfo(&buffer); + + /* iterate through the acl datastructure, emit GRANTs */ + while (i < ACL_NUM(acl)) + { + AclItem *aidata = NULL; + AclMode priv_bit = 0; + + offtype++; + + if (offtype == N_ACL_RIGHTS) + { + offtype = 0; + i++; + if (i >= ACL_NUM(acl)) /* done */ + { + break; + } + } + + aidata = &aidat[i]; + priv_bit = 1 << offtype; + + if (ACLITEM_GET_PRIVS(*aidata) & priv_bit) + { + const char *roleName = NULL; + const char *withGrant = ""; + + if (aidata->ai_grantee != 0) + { + + HeapTuple htup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(aidata->ai_grantee)); + + if (HeapTupleIsValid(htup)) + { + Form_pg_authid authForm = ((Form_pg_authid) GETSTRUCT(htup)); + + roleName = quote_identifier(NameStr(authForm->rolname)); + + ReleaseSysCache(htup); + } + else + { + elog(ERROR, "cache lookup failed for role %u", aidata->ai_grantee); + } + } + else + { + roleName = "PUBLIC"; + } + + if ((ACLITEM_GET_GOPTIONS(*aidata) & priv_bit) != 0) + { + withGrant = " WITH GRANT OPTION"; + } + + appendStringInfo(&buffer, "GRANT %s ON %s TO %s%s", + convert_aclright_to_string(priv_bit), + relationName, + roleName, + withGrant); + + defs = lappend(defs, pstrdup(buffer.data)); + + resetStringInfo(&buffer); + } + } + } + + resetStringInfo(&buffer); + + relation_close(relation, NoLock); + return defs; +} + +/* + * pg_get_replica_identity_command function returns the required ALTER .. TABLE + * command to define the replica identity. + */ +char * +pg_get_replica_identity_command(Oid tableRelationId) +{ + StringInfo buf = makeStringInfo(); + + Relation relation = table_open(tableRelationId, AccessShareLock); + + char replicaIdentity = relation->rd_rel->relreplident; + + char *relationName = generate_qualified_relation_name(tableRelationId); + + if (replicaIdentity == REPLICA_IDENTITY_INDEX) + { + Oid indexId = RelationGetReplicaIndex(relation); + + if (OidIsValid(indexId)) + { + appendStringInfo(buf, "ALTER TABLE %s REPLICA IDENTITY USING INDEX %s ", + relationName, + quote_identifier(get_rel_name(indexId))); + } + } + else if (replicaIdentity == REPLICA_IDENTITY_NOTHING) + { + appendStringInfo(buf, "ALTER TABLE %s REPLICA IDENTITY NOTHING", + relationName); + } + else if (replicaIdentity == REPLICA_IDENTITY_FULL) + { + appendStringInfo(buf, "ALTER TABLE %s REPLICA IDENTITY FULL", + relationName); + } + + table_close(relation, AccessShareLock); + + return (buf->len > 0) ? buf->data : NULL; +} + +/* + * pg_get_row_level_security_commands function returns the required ALTER .. TABLE + * commands to define the row level security settings for a relation. + */ +List * +pg_get_row_level_security_commands(Oid relationId) +{ + StringInfoData buffer; + List *commands = NIL; + Relation relation = table_open(relationId, AccessShareLock); + + initStringInfo(&buffer); + + if (relation->rd_rel->relrowsecurity) + { + char *relationName = generate_qualified_relation_name(relationId); + + appendStringInfo(&buffer, "ALTER TABLE %s ENABLE ROW LEVEL SECURITY", + relationName); + commands = lappend(commands, pstrdup(buffer.data)); + resetStringInfo(&buffer); + } + + if (relation->rd_rel->relforcerowsecurity) + { + char *relationName = generate_qualified_relation_name(relationId); + + appendStringInfo(&buffer, "ALTER TABLE %s FORCE ROW LEVEL SECURITY", + relationName); + commands = lappend(commands, pstrdup(buffer.data)); + resetStringInfo(&buffer); + } + + table_close(relation, AccessShareLock); + + return commands; +} + +/* + * contain_nextval_expression_walker walks over expression tree and returns + * true if it contains call to 'nextval' function or it has an identity column. + */ +bool +contain_nextval_expression_walker(Node *node, void *context) +{ + if (node == NULL) + { + return false; + } + + /* check if the node contains an identity column */ + if (IsA(node, NextValueExpr)) + { + return true; + } + + /* check if the node contains call to 'nextval' */ + if (IsA(node, FuncExpr)) + { + FuncExpr *funcExpr = (FuncExpr *) node; + + if (funcExpr->funcid == F_NEXTVAL) + { + return true; + } + } + return expression_tree_walker(node, contain_nextval_expression_walker, context); +} + + +/* + * AppendOptionListToString converts the option list to its textual format, and + * appends this text to the given string buffer. + */ +void +AppendOptionListToString(StringInfo stringBuffer, List *optionList) +{ + if (optionList != NIL) + { + ListCell *optionCell = NULL; + bool firstOptionPrinted = false; + + appendStringInfo(stringBuffer, " OPTIONS ("); + + foreach(optionCell, optionList) + { + DefElem *option = (DefElem *) lfirst(optionCell); + char *optionName = option->defname; + char *optionValue = defGetString(option); + + if (firstOptionPrinted) + { + appendStringInfo(stringBuffer, ", "); + } + firstOptionPrinted = true; + + appendStringInfo(stringBuffer, "%s ", quote_identifier(optionName)); + appendStringInfo(stringBuffer, "%s", quote_literal_cstr(optionValue)); + } + + appendStringInfo(stringBuffer, ")"); + } +} + +/* + * AppendStorageParametersToString converts the storage parameter list to its + * textual format, and appends this text to the given string buffer. + */ +void +AppendStorageParametersToString(StringInfo stringBuffer, List *optionList) +{ + ListCell *optionCell = NULL; + bool firstOptionPrinted = false; + + foreach(optionCell, optionList) + { + DefElem *option = (DefElem *) lfirst(optionCell); + char *optionName = option->defname; + char *optionValue = defGetString(option); + + if (firstOptionPrinted) + { + appendStringInfo(stringBuffer, ", "); + } + firstOptionPrinted = true; + + appendStringInfo(stringBuffer, "%s = %s ", + quote_identifier(optionName), + quote_literal_cstr(optionValue)); + } +} + +/* + * GenereatePartitioningInformation returns the partitioning type and partition column + * for the given parent table in the form of "PARTITION TYPE (partitioning column(s)/expression(s))". + */ +char * +GeneratePartitioningInformation(Oid parentTableId) +{ + char *partitionBoundCString = ""; + Datum partitionBoundDatum; + + if (!PartitionedTable(parentTableId)) + { + char *relationName = get_rel_name(parentTableId); + + ereport(ERROR, (errmsg("\"%s\" is not a parent table", relationName))); + } + + partitionBoundDatum = DirectFunctionCall1(pg_get_partkeydef, + ObjectIdGetDatum(parentTableId)); + + partitionBoundCString = TextDatumGetCString(partitionBoundDatum); + + return partitionBoundCString; +} + +/* + * Returns true if the given relation is a partitioned table. + */ +bool +PartitionedTable(Oid relationId) +{ + Relation rel = try_relation_open(relationId, AccessShareLock); + bool partitionedTable = false; + + /* don't error out for tables that are dropped */ + if (rel == NULL) + { + return false; + } + + if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) + { + partitionedTable = true; + } + + /* keep the lock */ + table_close(rel, NoLock); + + return partitionedTable; +} + +/* + * RoleSpecString resolves the role specification to its string form that is suitable for transport to a worker node. + * This function resolves the following identifiers from the current context so they are safe to transfer. + * + * CURRENT_USER - resolved to the user name of the current role being used + * SESSION_USER - resolved to the user name of the user that opened the session + * CURRENT_ROLE - same as CURRENT_USER, resolved to the user name of the current role being used + * Postgres treats CURRENT_ROLE is equivalent to CURRENT_USER, and we follow the same approach. + * + * withQuoteIdentifier is used, because if the results will be used in a query the quotes are needed but if not there + * should not be extra quotes. + */ +const char * +RoleSpecString(RoleSpec *spec, bool withQuoteIdentifier) +{ + switch (spec->roletype) + { + case ROLESPEC_CSTRING: + { + return withQuoteIdentifier ? + quote_identifier(spec->rolename) : + spec->rolename; + } + +#if PG_VERSION_NUM >= PG_VERSION_14 + case ROLESPEC_CURRENT_ROLE: +#endif + case ROLESPEC_CURRENT_USER: + { + return withQuoteIdentifier ? + quote_identifier(GetUserNameFromId(GetUserId(), false)) : + GetUserNameFromId(GetUserId(), false); + } + + case ROLESPEC_SESSION_USER: + { + return withQuoteIdentifier ? + quote_identifier(GetUserNameFromId(GetSessionUserId(), false)) : + GetUserNameFromId(GetSessionUserId(), false); + } + + case ROLESPEC_PUBLIC: + { + return "PUBLIC"; + } + + default: + { + elog(ERROR, "unexpected role type %d", spec->roletype); + } + } +} + +/* + * TableOwnerResetCommand generates a commands that can be executed + * to reset the table owner. + */ +char * +TableOwnerResetCommand(Oid relationId) +{ + StringInfo ownerResetCommand = makeStringInfo(); + char *qualifiedRelationName = generate_qualified_relation_name(relationId); + char *tableOwnerName = TableOwner(relationId); + + appendStringInfo(ownerResetCommand, + "ALTER TABLE %s OWNER TO %s", + qualifiedRelationName, + quote_identifier(tableOwnerName)); + + return ownerResetCommand->data; +} + +Oid +TableOwnerOid(Oid relationId) +{ + Oid userId; + HeapTuple tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relationId)); + + if (!HeapTupleIsValid(tuple)) + { + ereport(ERROR, (errcode(ERRCODE_UNDEFINED_TABLE), + errmsg("relation with OID %u does not exist", relationId))); + } + + userId = ((Form_pg_class) GETSTRUCT(tuple))->relowner; + + ReleaseSysCache(tuple); + return userId; +} + +/* + * Return a table's owner as a string. + */ +char * +TableOwner(Oid relationId) +{ + return GetUserNameFromId(TableOwnerOid(relationId), false); +} diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index f8e1238fa2c..c384a455950 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -275,5 +275,6 @@ extern void removeExtObjInitPriv(Oid objoid, Oid classoid); extern bool object_ownercheck(Oid classid, Oid objectid, Oid roleid); extern bool has_createrole_privilege(Oid roleid); extern bool has_bypassrls_privilege(Oid roleid); +extern const char *convert_aclright_to_string(int aclright); #endif /* ACL_H */ diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h index b006d9d475e..ea81a68545d 100644 --- a/src/include/utils/ruleutils.h +++ b/src/include/utils/ruleutils.h @@ -16,6 +16,7 @@ #include "nodes/nodes.h" #include "nodes/parsenodes.h" #include "nodes/pg_list.h" +#include "catalog/pg_sequence.h" struct Plan; /* avoid including plannodes.h here */ struct PlannedStmt; @@ -24,6 +25,27 @@ struct PlannedStmt; #define RULE_INDEXDEF_PRETTY 0x01 #define RULE_INDEXDEF_KEYS_ONLY 0x02 /* ignore included attributes */ +/* + * IncludeSequenceDefaults decides on inclusion of DEFAULT clauses for columns + * getting their default values from a sequence when creating the definition + * of a table. + */ +typedef enum IncludeSequenceDefaults +{ + NO_SEQUENCE_DEFAULTS = 0, /* don't include sequence defaults */ + INCLUDE_SEQUENCE_DEFAULTS = 1, /* include sequence defaults */ +} IncludeSequenceDefaults; + +/* + * IncludeIdentities decides on how we include identity information + * when creating the definition of a table. + */ +typedef enum IncludeIdentities +{ + NO_IDENTITY = 0, /* don't include identities */ + INCLUDE_IDENTITY = 1 /* include identities as-is */ +} IncludeIdentities; + extern char *pg_get_indexdef_string(Oid indexrelid); extern char *pg_get_indexdef_columns(Oid indexrelid, bool pretty); extern char *pg_get_indexdef_columns_extended(Oid indexrelid, @@ -43,10 +65,39 @@ extern List *set_deparse_context_plan(List *dpcontext, struct Plan *plan, List *ancestors); extern List *select_rtable_names_for_explain(List *rtable, Bitmapset *rels_used); +extern char *generate_qualified_relation_name(Oid collid); extern char *generate_collation_name(Oid collid); extern char *generate_opclass_name(Oid opclass); extern char *get_range_partbound_string(List *bound_datums); extern char *pg_get_statisticsobjdef_string(Oid statextid); +/* Function declarations for version independent Citus ruleutils wrapper functions */ +extern char *pg_get_extensiondef_string(Oid tableRelationId); +extern char *get_extension_version(Oid extensionId); +extern char *pg_get_serverdef_string(Oid tableRelationId); +extern char *pg_get_sequencedef_string(Oid sequenceRelid); +extern Form_pg_sequence pg_get_sequencedef(Oid sequenceRelationId); +extern char *pg_get_tableschemadef_string(Oid tableRelationId, + IncludeSequenceDefaults includeSequenceDefaults, + IncludeIdentities includeIdentityDefaults, + char *accessMethod); +extern char *pg_get_tablecolumnoptionsdef_string(Oid tableRelationId); +extern char *pg_get_indexclusterdef_string(Oid indexRelationId); +extern List *pg_get_table_grants(Oid relationId); +extern char *pg_get_replica_identity_command(Oid tableRelationId); +extern List *pg_get_row_level_security_commands(Oid relationId); +extern bool contain_nextval_expression_walker(Node *node, void *context); +extern void AppendOptionListToString(StringInfo stringData, List *options); +extern void AppendStorageParametersToString(StringInfo stringBuffer, + List *optionList); + +extern bool RegularTable(Oid relationId); +extern char *GeneratePartitioningInformation(Oid parentTableId); +extern bool PartitionedTable(Oid relationId); +extern const char *RoleSpecString(RoleSpec *spec, bool withQuoteIdentifier); +extern char *TableOwnerResetCommand(Oid relationId); +extern Oid TableOwnerOid(Oid relationId); +extern char *TableOwner(Oid relationId); + #endif /* RULEUTILS_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 260854747b4..241594e6333 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1141,6 +1141,8 @@ ImportForeignSchema_function ImportQual InProgressEnt IncludeWal +IncludeSequenceDefaults +IncludeIdentities InclusionOpaque IncrementVarSublevelsUp_context IncrementalSort base-commit: 8cddea9a539cbbdd1b316255c9f404323243fd37 -- 2.34.1