(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

Reply via email to