From 275544b1a7f7e5d68f22c66ffd21b0c3e2dccc07 Mon Sep 17 00:00:00 2001 From: "houzj.fnst" Date: Fri, 27 May 2022 13:47:05 +0800 Subject: [PATCH] Functions to deparse DDL commands. This patch provides JSON blobs representing DDL commands, which can later be re-processed into plain strings by well-defined sprintf-like expansion. These JSON objects are intended to allow for machine-editing of the commands, by replacing certain nodes within the objects. Much of the information in the output blob actually comes from system catalogs, not from the command parse node, as it is impossible to reliably construct a fully-specified command (i.e. one not dependent on search_path etc) looking only at the parse node. This provides base for logical replication of DDL statements. Currently, this provides support for CREATE TABLE/ALTER TABLE/DROP TABLE specific functions. Note that some recently introduced DDLs(e.g. DDLs related to PARTITIONED TABLE) are unsupported. We can extend it as we need more functionality for DDL replication. --- src/backend/commands/Makefile | 2 + src/backend/commands/ddl_deparse.c | 2180 ++++++++++++++++++++++++++++++++++++ src/backend/commands/ddl_json.c | 749 +++++++++++++ src/backend/utils/adt/ruleutils.c | 9 + src/include/catalog/pg_proc.dat | 7 +- src/include/tcop/ddl_deparse.h | 10 + src/include/utils/ruleutils.h | 1 + 7 files changed, 2957 insertions(+), 1 deletion(-) create mode 100644 src/backend/commands/ddl_deparse.c create mode 100644 src/backend/commands/ddl_json.c create mode 100644 src/include/tcop/ddl_deparse.h diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile index 48f7348..171dfb2 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -29,6 +29,8 @@ OBJS = \ copyto.o \ createas.o \ dbcommands.o \ + ddl_deparse.o \ + ddl_json.o \ define.o \ discard.o \ dropcmds.o \ diff --git a/src/backend/commands/ddl_deparse.c b/src/backend/commands/ddl_deparse.c new file mode 100644 index 0000000..60c077f --- /dev/null +++ b/src/backend/commands/ddl_deparse.c @@ -0,0 +1,2180 @@ +/*------------------------------------------------------------------------- + * + * ddl_deparse.c + * Functions to convert utility commands to machine-parseable + * representation + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * NOTES + * + * This is intended to provide JSON blobs representing DDL commands, which can + * later be re-processed into plain strings by well-defined sprintf-like + * expansion. These JSON objects are intended to allow for machine-editing of + * the commands, by replacing certain nodes within the objects. + * + * Much of the information in the output blob actually comes from system + * catalogs, not from the command parse node, as it is impossible to reliably + * construct a fully-specified command (i.e. one not dependent on search_path + * etc) looking only at the parse node. + * + * IDENTIFICATION + * src/backend/commands/ddl_deparse.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "tcop/ddl_deparse.h" + +#include "access/amapi.h" +#include "access/table.h" +#include "access/relation.h" +#include "catalog/namespace.h" +#include "catalog/pg_attribute.h" +#include "catalog/pg_class.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_constraint.h" +#include "catalog/pg_inherits.h" +#include "catalog/pg_namespace.h" +#include "catalog/pg_type.h" +#include "commands/defrem.h" +#include "lib/ilist.h" +#include "rewrite/rewriteHandler.h" +#include "utils/builtins.h" +#include "utils/fmgroids.h" +#include "utils/jsonb.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/rel.h" +#include "utils/ruleutils.h" +#include "utils/syscache.h" + + +/* + * Before they are turned into JSONB representation, each command is + * represented as an object tree, using the structs below. + */ +typedef enum +{ + ObjTypeNull, + ObjTypeBool, + ObjTypeString, + ObjTypeArray, + ObjTypeInteger, + ObjTypeFloat, + ObjTypeObject +} ObjType; + +typedef struct ObjTree +{ + slist_head params; + int numParams; +} ObjTree; + +typedef struct ObjElem +{ + char *name; + ObjType objtype; + + union + { + bool boolean; + char *string; + int64 integer; + float8 flt; + ObjTree *object; + List *array; + } value; + slist_node node; +} ObjElem; + +static ObjElem *new_null_object(void); +static ObjElem *new_bool_object(bool value); +static ObjElem *new_string_object(char *value); +static ObjElem *new_object_object(ObjTree *value); +static ObjElem *new_array_object(List *array); +static ObjElem *new_integer_object(int64 value); +static ObjElem *new_float_object(float8 value); +static void append_null_object(ObjTree *tree, char *name); +static void append_bool_object(ObjTree *tree, char *name, bool value); +static void append_string_object(ObjTree *tree, char *name, char *value); +static void append_object_object(ObjTree *tree, char *name, ObjTree *value); +static void append_array_object(ObjTree *tree, char *name, List *array); +static inline void append_premade_object(ObjTree *tree, ObjElem *elem); +static JsonbValue *objtree_to_jsonb_rec(ObjTree *tree, JsonbParseState *state); +static void format_type_detailed(Oid type_oid, int32 typemod, + Oid *nspid, char **typname, char **typemodstr, + bool *typarray); +static char *printTypmod(const char *typname, int32 typmod, Oid typmodout); + +static char *RelationGetColumnDefault(Relation rel, AttrNumber attno, List *dpcontext); + +/* + * Similar to format_type_internal, except we return each bit of information + * separately: + * + * - nspid is the schema OID. For certain SQL-standard types which have weird + * typmod rules, we return InvalidOid; caller is expected to not schema- + * qualify the name nor add quotes to the type name in this case. + * + * - typename is set to the type name, without quotes + * + * - typmod is set to the typemod, if any, as a string with parens + * + * - typarray indicates whether []s must be added + * + * We don't try to decode type names to their standard-mandated names, except + * in the cases of types with unusual typmod rules. + */ +static void +format_type_detailed(Oid type_oid, int32 typemod, + Oid *nspid, char **typname, char **typemodstr, + bool *typarray) +{ + HeapTuple tuple; + Form_pg_type typeform; + Oid array_base_type; + + tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(type_oid)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for type %u", type_oid); + + typeform = (Form_pg_type) GETSTRUCT(tuple); + + /* + * Special-case crock for types with strange typmod rules. + */ + if (type_oid == INTERVALOID || + type_oid == TIMESTAMPOID || + type_oid == TIMESTAMPTZOID || + type_oid == TIMEOID || + type_oid == TIMETZOID) + { + *typarray = false; + +peculiar_typmod: + switch (type_oid) + { + case INTERVALOID: + *typname = pstrdup("INTERVAL"); + break; + case TIMESTAMPTZOID: + if (typemod < 0) + *typname = pstrdup("TIMESTAMP WITH TIME ZONE"); + else + *typname = pstrdup("TIMESTAMP"); + break; + /* otherwise, WITH TZ is added by typmod, so fall through */ + case TIMESTAMPOID: + *typname = pstrdup("TIMESTAMP"); + break; + case TIMETZOID: + if (typemod < 0) + *typname = pstrdup("TIME WITH TIME ZONE"); + else + *typname = pstrdup("TIME"); + break; + /* otherwise, WITH TZ is added by typmode, so fall through */ + case TIMEOID: + *typname = pstrdup("TIME"); + break; + } + *nspid = InvalidOid; + + if (typemod >= 0) + *typemodstr = printTypmod("", typemod, typeform->typmodout); + else + *typemodstr = pstrdup(""); + + ReleaseSysCache(tuple); + return; + } + + /* + * Check if it's a regular (variable length) array type. As above, + * fixed-length array types such as "name" shouldn't get deconstructed. + */ + array_base_type = typeform->typelem; + + if (array_base_type != InvalidOid && + typeform->typstorage != 'p') + { + /* Switch our attention to the array element type */ + ReleaseSysCache(tuple); + tuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(array_base_type)); + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for type %u", type_oid); + + typeform = (Form_pg_type) GETSTRUCT(tuple); + type_oid = array_base_type; + *typarray = true; + + /* + * If it's an array of one of the types with special typmod rules, + * have the element type be processed as above, but now with typarray + * set to true. + */ + if (type_oid == INTERVALOID || + type_oid == TIMESTAMPTZOID || + type_oid == TIMESTAMPOID || + type_oid == TIMETZOID || + type_oid == TIMEOID) + goto peculiar_typmod; + } + else + *typarray = false; + + *nspid = typeform->typnamespace; + *typname = pstrdup(NameStr(typeform->typname)); + + if (typemod >= 0) + *typemodstr = printTypmod("", typemod, typeform->typmodout); + else + *typemodstr = pstrdup(""); + + ReleaseSysCache(tuple); +} + +/* + * Add typmod decoration to the basic type name + */ +static char * +printTypmod(const char *typname, int32 typmod, Oid typmodout) +{ + char *res; + + /* Shouldn't be called if typmod is -1 */ + Assert(typmod >= 0); + + if (typmodout == InvalidOid) + { + /* Default behavior: just print the integer typmod with parens */ + res = psprintf("%s(%d)", typname, (int) typmod); + } + else + { + /* Use the type-specific typmodout procedure */ + char *tmstr; + + tmstr = DatumGetCString(OidFunctionCall1(typmodout, + Int32GetDatum(typmod))); + res = psprintf("%s%s", typname, tmstr); + } + + return res; +} + +/* + * Obtain the deparsed default value for the given column of the given table. + * + * Caller must have set a correct deparse context. + */ +static char * +RelationGetColumnDefault(Relation rel, AttrNumber attno, List *dpcontext) +{ + Node *defval; + char *defstr; + + defval = build_column_default(rel, attno); + defstr = deparse_expression(defval, dpcontext, false, false); + + return defstr; +} + +/* + * Allocate a new object tree to store parameter values. + */ +static ObjTree * +new_objtree(void) +{ + ObjTree *params; + + params = palloc(sizeof(ObjTree)); + params->numParams = 0; + slist_init(¶ms->params); + + return params; +} + +/* + * Allocate a new object tree to store parameter values -- varargs version. + * + * The "fmt" argument is used to append as a "fmt" element in the output blob. + * numobjs indicates the number of extra elements to append; for each one, a + * name (string), type (from the ObjType enum) and value must be supplied. The + * value must match the type given; for instance, ObjTypeInteger requires an + * int64, ObjTypeString requires a char *, ObjTypeArray requires a list (of + * ObjElem), ObjTypeObject requires an ObjTree, and so on. Each element type * + * must match the conversion specifier given in the format string, as described + * in ddl_deparse_expand_command, q.v. + * + * Note we don't have the luxury of sprintf-like compiler warnings for + * malformed argument lists. + */ +static ObjTree * +new_objtree_VA(char *fmt, int numobjs,...) +{ + ObjTree *tree; + va_list args; + int i; + + /* Set up the toplevel object and its "fmt" */ + tree = new_objtree(); + append_string_object(tree, "fmt", fmt); + + /* And process the given varargs */ + va_start(args, numobjs); + for (i = 0; i < numobjs; i++) + { + char *name; + ObjType type; + ObjElem *elem; + + name = va_arg(args, char *); + type = va_arg(args, ObjType); + + /* + * For all other param types there must be a value in the varargs. + * Fetch it and add the fully formed subobject into the main object. + */ + switch (type) + { + case ObjTypeBool: + elem = new_bool_object(va_arg(args, int)); + break; + case ObjTypeString: + elem = new_string_object(va_arg(args, char *)); + break; + case ObjTypeObject: + elem = new_object_object(va_arg(args, ObjTree *)); + break; + case ObjTypeArray: + elem = new_array_object(va_arg(args, List *)); + break; + case ObjTypeInteger: + elem = new_integer_object(va_arg(args, int64)); + break; + case ObjTypeFloat: + elem = new_float_object(va_arg(args, double)); + break; + case ObjTypeNull: + /* Null params don't have a value (obviously) */ + elem = new_null_object(); + break; + default: + elog(ERROR, "invalid ObjTree element type %d", type); + } + + elem->name = name; + append_premade_object(tree, elem); + } + + va_end(args); + return tree; +} + +/* Allocate a new parameter with a NULL value */ +static ObjElem * +new_null_object(void) +{ + ObjElem *param; + + param = palloc0(sizeof(ObjElem)); + + param->name = NULL; + param->objtype = ObjTypeNull; + + return param; +} + +/* Append a NULL object to a tree */ +static void +append_null_object(ObjTree *tree, char *name) +{ + ObjElem *param; + + param = new_null_object(); + param->name = name; + append_premade_object(tree, param); +} + +/* Allocate a new boolean parameter */ +static ObjElem * +new_bool_object(bool value) +{ + ObjElem *param; + + param = palloc0(sizeof(ObjElem)); + param->name = NULL; + param->objtype = ObjTypeBool; + param->value.boolean = value; + + return param; +} + +/* Append a boolean parameter to a tree */ +static void +append_bool_object(ObjTree *tree, char *name, bool value) +{ + ObjElem *param; + + param = new_bool_object(value); + param->name = name; + append_premade_object(tree, param); +} + +/* Allocate a new string object */ +static ObjElem * +new_string_object(char *value) +{ + ObjElem *param; + + Assert(value); + + param = palloc0(sizeof(ObjElem)); + param->name = NULL; + param->objtype = ObjTypeString; + param->value.string = value; + + return param; +} + +/* + * Append a string parameter to a tree. + */ +static void +append_string_object(ObjTree *tree, char *name, char *value) +{ + ObjElem *param; + + Assert(name); + param = new_string_object(value); + param->name = name; + append_premade_object(tree, param); +} + +static ObjElem * +new_integer_object(int64 value) +{ + ObjElem *param; + + param = palloc0(sizeof(ObjElem)); + param->name = NULL; + param->objtype = ObjTypeInteger; + param->value.integer = value; + + return param; +} + +static ObjElem * +new_float_object(float8 value) +{ + ObjElem *param; + + param = palloc0(sizeof(ObjElem)); + param->name = NULL; + param->objtype = ObjTypeFloat; + param->value.flt = value; + + return param; +} + +/* Allocate a new object parameter */ +static ObjElem * +new_object_object(ObjTree *value) +{ + ObjElem *param; + + param = palloc0(sizeof(ObjElem)); + param->name = NULL; + param->objtype = ObjTypeObject; + param->value.object = value; + + return param; +} + +/* Append an object parameter to a tree */ +static void +append_object_object(ObjTree *tree, char *name, ObjTree *value) +{ + ObjElem *param; + + Assert(name); + param = new_object_object(value); + param->name = name; + append_premade_object(tree, param); +} + +/* Allocate a new array parameter */ +static ObjElem * +new_array_object(List *array) +{ + ObjElem *param; + + param = palloc0(sizeof(ObjElem)); + param->name = NULL; + param->objtype = ObjTypeArray; + param->value.array = array; + + return param; +} + +/* Append an array parameter to a tree */ +static void +append_array_object(ObjTree *tree, char *name, List *array) +{ + ObjElem *param; + + param = new_array_object(array); + param->name = name; + append_premade_object(tree, param); +} + +/* Append a preallocated parameter to a tree */ +static inline void +append_premade_object(ObjTree *tree, ObjElem *elem) +{ + slist_push_head(&tree->params, &elem->node); + tree->numParams++; +} + +/* + * Helper for objtree_to_jsonb: process an individual element from an object or + * an array into the output parse state. + */ +static void +objtree_to_jsonb_element(JsonbParseState *state, ObjElem *object, + JsonbIteratorToken elem_token) +{ + ListCell *cell; + JsonbValue val; + + switch (object->objtype) + { + case ObjTypeNull: + val.type = jbvNull; + pushJsonbValue(&state, elem_token, &val); + break; + + case ObjTypeString: + val.type = jbvString; + val.val.string.len = strlen(object->value.string); + val.val.string.val = object->value.string; + pushJsonbValue(&state, elem_token, &val); + break; + + case ObjTypeInteger: + val.type = jbvNumeric; + val.val.numeric = (Numeric) + DatumGetNumeric(DirectFunctionCall1(int8_numeric, + object->value.integer)); + pushJsonbValue(&state, elem_token, &val); + break; + + case ObjTypeFloat: + val.type = jbvNumeric; + val.val.numeric = (Numeric) + DatumGetNumeric(DirectFunctionCall1(float8_numeric, + object->value.integer)); + pushJsonbValue(&state, elem_token, &val); + break; + + case ObjTypeBool: + val.type = jbvBool; + val.val.boolean = object->value.boolean; + pushJsonbValue(&state, elem_token, &val); + break; + + case ObjTypeObject: + /* recursively add the object into the existing parse state */ + objtree_to_jsonb_rec(object->value.object, state); + break; + + case ObjTypeArray: + pushJsonbValue(&state, WJB_BEGIN_ARRAY, NULL); + foreach(cell, object->value.array) + { + ObjElem *elem = lfirst(cell); + + objtree_to_jsonb_element(state, elem, WJB_ELEM); + } + pushJsonbValue(&state, WJB_END_ARRAY, NULL); + break; + + default: + elog(ERROR, "unrecognized object type %d", object->objtype); + break; + } +} + +/* + * Recursive helper for objtree_to_jsonb + */ +static JsonbValue * +objtree_to_jsonb_rec(ObjTree *tree, JsonbParseState *state) +{ + slist_iter iter; + + pushJsonbValue(&state, WJB_BEGIN_OBJECT, NULL); + + slist_foreach(iter, &tree->params) + { + ObjElem *object = slist_container(ObjElem, node, iter.cur); + JsonbValue key; + + /* Push the key first */ + key.type = jbvString; + key.val.string.len = strlen(object->name); + key.val.string.val = object->name; + pushJsonbValue(&state, WJB_KEY, &key); + + /* Then process the value according to its type */ + objtree_to_jsonb_element(state, object, WJB_VALUE); + } + + return pushJsonbValue(&state, WJB_END_OBJECT, NULL); +} + +/* + * Create a JSONB representation from an ObjTree. + */ +static Jsonb * +objtree_to_jsonb(ObjTree *tree) +{ + JsonbValue *value; + + value = objtree_to_jsonb_rec(tree, NULL); + return JsonbValueToJsonb(value); +} + +/* + * A helper routine to setup %{}T elements. + */ +static ObjTree * +new_objtree_for_type(Oid typeId, int32 typmod) +{ + ObjTree *typeParam; + Oid typnspid; + char *typnsp; + char *typename = NULL; + char *typmodstr; + bool typarray; + + format_type_detailed(typeId, typmod, + &typnspid, &typename, &typmodstr, &typarray); + + if (!OidIsValid(typnspid)) + typnsp = pstrdup(""); + else if (isAnyTempNamespace(typnspid)) + typnsp = pstrdup("pg_temp"); + else + typnsp = get_namespace_name(typnspid); + + /* We don't use new_objtree_VA here because types don't have a "fmt" */ + typeParam = new_objtree(); + append_string_object(typeParam, "schemaname", typnsp); + append_string_object(typeParam, "typename", typename); + append_string_object(typeParam, "typmod", typmodstr); + append_bool_object(typeParam, "typarray", typarray); + + return typeParam; +} + +/* + * A helper routine to setup %{}D and %{}O elements + * + * Elements "schemaname" and "objname" are set. If the namespace OID + * corresponds to a temp schema, that's set to "pg_temp". + * + * The difference between those two element types is whether the objname will + * be quoted as an identifier or not, which is not something that this routine + * concerns itself with; that will be up to the expand function. + */ +static ObjTree * +new_objtree_for_qualname(Oid nspid, char *name) +{ + ObjTree *qualified; + char *namespace; + + /* + * We don't use new_objtree_VA here because these names don't have a "fmt" + */ + qualified = new_objtree(); + if (isAnyTempNamespace(nspid)) + namespace = pstrdup("pg_temp"); + else + namespace = get_namespace_name(nspid); + append_string_object(qualified, "schemaname", namespace); + append_string_object(qualified, "objname", pstrdup(name)); + + return qualified; +} + +/* + * A helper routine to setup %{}D and %{}O elements, with the object specified + * by classId/objId + * + * Elements "schemaname" and "objname" are set. If the object is a temporary + * object, the schema name is set to "pg_temp". + */ +static ObjTree * +new_objtree_for_qualname_id(Oid classId, Oid objectId) +{ + ObjTree *qualified; + Relation catalog; + HeapTuple catobj; + Datum objnsp; + Datum objname; + AttrNumber Anum_name; + AttrNumber Anum_namespace; + AttrNumber Anum_oid = get_object_attnum_oid(classId); + bool isnull; + + catalog = table_open(classId, AccessShareLock); + + catobj = get_catalog_object_by_oid(catalog, Anum_oid, objectId); + if (!catobj) + elog(ERROR, "cache lookup failed for object %u of catalog \"%s\"", + objectId, RelationGetRelationName(catalog)); + Anum_name = get_object_attnum_name(classId); + Anum_namespace = get_object_attnum_namespace(classId); + + objnsp = heap_getattr(catobj, Anum_namespace, RelationGetDescr(catalog), + &isnull); + if (isnull) + elog(ERROR, "unexpected NULL namespace"); + objname = heap_getattr(catobj, Anum_name, RelationGetDescr(catalog), + &isnull); + if (isnull) + elog(ERROR, "unexpected NULL name"); + + qualified = new_objtree_for_qualname(DatumGetObjectId(objnsp), + NameStr(*DatumGetName(objname))); + table_close(catalog, AccessShareLock); + + return qualified; +} + +/* + * Return the string representation of the given RELPERSISTENCE value + */ +static char * +get_persistence_str(char persistence) +{ + switch (persistence) + { + case RELPERSISTENCE_TEMP: + return "TEMPORARY"; + case RELPERSISTENCE_UNLOGGED: + return "UNLOGGED"; + case RELPERSISTENCE_PERMANENT: + return ""; + default: + elog(ERROR, "unexpected persistence marking %c", persistence); + return ""; /* make compiler happy */ + } +} + +/* + * deparse_ColumnDef + * Subroutine for CREATE TABLE deparsing + * + * Deparse a ColumnDef node within a regular (non typed) table creation. + * + * NOT NULL constraints in the column definition are emitted directly in the + * column definition by this routine; other constraints must be emitted + * elsewhere (the info in the parse node is incomplete anyway.) + */ +static ObjTree * +deparse_ColumnDef(Relation relation, List *dpcontext, bool composite, + ColumnDef *coldef, bool is_alter) +{ + ObjTree *column; + ObjTree *tmp; + Oid relid = RelationGetRelid(relation); + HeapTuple attrTup; + Form_pg_attribute attrForm; + Oid typid; + int32 typmod; + Oid typcollation; + bool saw_notnull; + ListCell *cell; + + /* + * Inherited columns without local definitions must not be emitted. XXX -- + * maybe it is useful to have them with "present = false" or some such? + */ + if (!coldef->is_local) + return NULL; + + attrTup = SearchSysCacheAttName(relid, coldef->colname); + if (!HeapTupleIsValid(attrTup)) + elog(ERROR, "could not find cache entry for column \"%s\" of relation %u", + coldef->colname, relid); + attrForm = (Form_pg_attribute) GETSTRUCT(attrTup); + + get_atttypetypmodcoll(relid, attrForm->attnum, + &typid, &typmod, &typcollation); + + /* Composite types use a slightly simpler format string */ + if (composite) + column = new_objtree_VA("%{name}I %{coltype}T %{collation}s", + 3, + "type", ObjTypeString, "column", + "name", ObjTypeString, coldef->colname, + "coltype", ObjTypeObject, + new_objtree_for_type(typid, typmod)); + else + column = new_objtree_VA("%{name}I %{coltype}T %{default}s %{not_null}s %{collation}s", + 3, + "type", ObjTypeString, "column", + "name", ObjTypeString, coldef->colname, + "coltype", ObjTypeObject, + new_objtree_for_type(typid, typmod)); + + tmp = new_objtree_VA("COLLATE %{name}D", 0); + if (OidIsValid(typcollation)) + { + ObjTree *collname; + + collname = new_objtree_for_qualname_id(CollationRelationId, + typcollation); + append_object_object(tmp, "name", collname); + } + else + append_bool_object(tmp, "present", false); + append_object_object(column, "collation", tmp); + + if (!composite) + { + /* + * Emit a NOT NULL declaration if necessary. Note that we cannot trust + * pg_attribute.attnotnull here, because that bit is also set when + * primary keys are specified; and we must not emit a NOT NULL + * constraint in that case, unless explicitely specified. Therefore, + * we scan the list of constraints attached to this column to determine + * whether we need to emit anything. + * (Fortunately, NOT NULL constraints cannot be table constraints.) + * + * In the ALTER TABLE cases, we also add a NOT NULL if the colDef is + * marked is_not_null. + */ + saw_notnull = false; + foreach(cell, coldef->constraints) + { + Constraint *constr = (Constraint *) lfirst(cell); + + if (constr->contype == CONSTR_NOTNULL) + saw_notnull = true; + } + if (is_alter && coldef->is_not_null) + saw_notnull = true; + + if (saw_notnull) + append_string_object(column, "not_null", "NOT NULL"); + else + append_string_object(column, "not_null", ""); + + tmp = new_objtree_VA("DEFAULT %{default}s", 0); + if (attrForm->atthasdef) + { + char *defstr; + + defstr = RelationGetColumnDefault(relation, attrForm->attnum, + dpcontext); + + append_string_object(tmp, "default", defstr); + } + else + append_bool_object(tmp, "present", false); + append_object_object(column, "default", tmp); + } + + ReleaseSysCache(attrTup); + + return column; +} + +/* + * deparse_ColumnDef_Typed + * Subroutine for CREATE TABLE OF deparsing + * + * Deparse a ColumnDef node within a typed table creation. This is simpler + * than the regular case, because we don't have to emit the type declaration, + * collation, or default. Here we only return something if the column is being + * declared NOT NULL. + * + * As in deparse_ColumnDef, any other constraint is processed elsewhere. + * + * FIXME --- actually, what about default values? + */ +static ObjTree * +deparse_ColumnDef_typed(Relation relation, List *dpcontext, ColumnDef *coldef) +{ + ObjTree *column = NULL; + Oid relid = RelationGetRelid(relation); + HeapTuple attrTup; + Form_pg_attribute attrForm; + Oid typid; + int32 typmod; + Oid typcollation; + bool saw_notnull; + ListCell *cell; + + attrTup = SearchSysCacheAttName(relid, coldef->colname); + if (!HeapTupleIsValid(attrTup)) + elog(ERROR, "could not find cache entry for column \"%s\" of relation %u", + coldef->colname, relid); + attrForm = (Form_pg_attribute) GETSTRUCT(attrTup); + + get_atttypetypmodcoll(relid, attrForm->attnum, + &typid, &typmod, &typcollation); + + /* + * Search for a NOT NULL declaration. As in deparse_ColumnDef, we rely on + * finding a constraint on the column rather than coldef->is_not_null. + * (This routine is never used for ALTER cases.) + */ + saw_notnull = false; + foreach(cell, coldef->constraints) + { + Constraint *constr = (Constraint *) lfirst(cell); + + if (constr->contype == CONSTR_NOTNULL) + { + saw_notnull = true; + break; + } + } + + if (saw_notnull) + column = new_objtree_VA("%{name}I WITH OPTIONS NOT NULL", 2, + "type", ObjTypeString, "column_notnull", + "name", ObjTypeString, coldef->colname); + + ReleaseSysCache(attrTup); + + return column; +} + +/* + * deparseTableElements + * Subroutine for CREATE TABLE deparsing + * + * Deal with all the table elements (columns and constraints). + * + * Note we ignore constraints in the parse node here; they are extracted from + * system catalogs instead. + */ +static List * +deparseTableElements(Relation relation, List *tableElements, List *dpcontext, + bool typed, bool composite) +{ + List *elements = NIL; + ListCell *lc; + + foreach(lc, tableElements) + { + Node *elt = (Node *) lfirst(lc); + + switch (nodeTag(elt)) + { + case T_ColumnDef: + { + ObjTree *tree; + + tree = typed ? + deparse_ColumnDef_typed(relation, dpcontext, + (ColumnDef *) elt) : + deparse_ColumnDef(relation, dpcontext, + composite, (ColumnDef *) elt, + false); + if (tree != NULL) + { + ObjElem *column; + + column = new_object_object(tree); + elements = lappend(elements, column); + } + } + break; + case T_Constraint: + break; + default: + elog(ERROR, "invalid node type %d", nodeTag(elt)); + } + } + + return elements; +} + +/* + * obtainConstraints + * Subroutine for CREATE TABLE/CREATE DOMAIN deparsing + * + * Given a table OID or domain OID, obtain its constraints and append them to + * the given elements list. The updated list is returned. + * + * This works for typed tables, regular tables, and domains. + * + * Note that CONSTRAINT_FOREIGN constraints are always ignored. + */ +static List * +obtainConstraints(List *elements, Oid relationId, Oid domainId) +{ + Relation conRel; + ScanKeyData key; + SysScanDesc scan; + HeapTuple tuple; + ObjTree *tmp; + + /* only one may be valid */ + Assert(OidIsValid(relationId) ^ OidIsValid(domainId)); + + /* + * scan pg_constraint to fetch all constraints linked to the given + * relation. + */ + conRel = table_open(ConstraintRelationId, AccessShareLock); + if (OidIsValid(relationId)) + { + ScanKeyInit(&key, + Anum_pg_constraint_conrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relationId)); + scan = systable_beginscan(conRel, ConstraintRelidTypidNameIndexId, + true, NULL, 1, &key); + } + else + { + Assert(OidIsValid(domainId)); + ScanKeyInit(&key, + Anum_pg_constraint_contypid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(domainId)); + scan = systable_beginscan(conRel, ConstraintTypidIndexId, + true, NULL, 1, &key); + } + + /* + * For each constraint, add a node to the list of table elements. In + * these nodes we include not only the printable information ("fmt"), but + * also separate attributes to indicate the type of constraint, for + * automatic processing. + */ + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + Form_pg_constraint constrForm; + char *contype; + + constrForm = (Form_pg_constraint) GETSTRUCT(tuple); + + switch (constrForm->contype) + { + case CONSTRAINT_CHECK: + contype = "check"; + break; + case CONSTRAINT_FOREIGN: + continue; /* not here */ + case CONSTRAINT_PRIMARY: + contype = "primary key"; + break; + case CONSTRAINT_UNIQUE: + contype = "unique"; + break; + case CONSTRAINT_TRIGGER: + contype = "trigger"; + break; + case CONSTRAINT_EXCLUSION: + contype = "exclusion"; + break; + default: + elog(ERROR, "unrecognized constraint type"); + } + + /* + * "type" and "contype" are not part of the printable output, but are + * useful to programmatically distinguish these from columns and among + * different constraint types. + * + * XXX it might be useful to also list the column names in a PK, etc. + */ + tmp = new_objtree_VA("CONSTRAINT %{name}I %{definition}s", + 4, + "type", ObjTypeString, "constraint", + "contype", ObjTypeString, contype, + "name", ObjTypeString, NameStr(constrForm->conname), + "definition", ObjTypeString, + pg_get_constraintdef_command_simple(constrForm->oid)); + elements = lappend(elements, new_object_object(tmp)); + } + + systable_endscan(scan); + table_close(conRel, AccessShareLock); + + return elements; +} + +/* + * deparse the ON COMMMIT ... clause for CREATE ... TEMPORARY ... + */ +static ObjTree * +deparse_OnCommitClause(OnCommitAction option) +{ + ObjTree *tmp; + + tmp = new_objtree_VA("ON COMMIT %{on_commit_value}s", 0); + switch (option) + { + case ONCOMMIT_DROP: + append_string_object(tmp, "on_commit_value", "DROP"); + break; + + case ONCOMMIT_DELETE_ROWS: + append_string_object(tmp, "on_commit_value", "DELETE ROWS"); + break; + + case ONCOMMIT_PRESERVE_ROWS: + append_string_object(tmp, "on_commit_value", "PRESERVE ROWS"); + break; + + case ONCOMMIT_NOOP: + append_null_object(tmp, "on_commit_value"); + append_bool_object(tmp, "present", false); + break; + } + + return tmp; +} + +/* + * Deparse DefElems, as used e.g. by ALTER COLUMN ... SET, into a list of SET + * (...) or RESET (...) contents. + */ +static ObjTree * +deparse_DefElem(DefElem *elem, bool is_reset) +{ + ObjTree *set; + ObjTree *optname; + + if (elem->defnamespace != NULL) + optname = new_objtree_VA("%{schema}I.%{label}I", 1, + "schema", ObjTypeString, elem->defnamespace); + else + optname = new_objtree_VA("%{label}I", 0); + + append_string_object(optname, "label", elem->defname); + + if (is_reset) + set = new_objtree_VA("%{label}s", 0); + else + set = new_objtree_VA("%{label}s = %{value}L", 1, + "value", ObjTypeString, + elem->arg ? defGetString(elem) : + defGetBoolean(elem) ? "TRUE" : "FALSE"); + + append_object_object(set, "label", optname); + return set; +} + +/* + * ... ALTER COLUMN ... SET/RESET (...) + */ +static ObjTree * +deparse_ColumnSetOptions(AlterTableCmd *subcmd) +{ + List *sets = NIL; + ListCell *cell; + ObjTree *tmp; + bool is_reset = subcmd->subtype == AT_ResetOptions; + + if (is_reset) + tmp = new_objtree_VA("ALTER COLUMN %{column}I RESET (%{options:, }s)", 0); + else + tmp = new_objtree_VA("ALTER COLUMN %{column}I SET (%{options:, }s)", 0); + + append_string_object(tmp, "column", subcmd->name); + + foreach(cell, (List *) subcmd->def) + { + DefElem *elem; + ObjTree *set; + + elem = (DefElem *) lfirst(cell); + set = deparse_DefElem(elem, is_reset); + sets = lappend(sets, new_object_object(set)); + } + + append_array_object(tmp, "options", sets); + + return tmp; +} + +/* + * ... ALTER COLUMN ... SET/RESET (...) + */ +static ObjTree * +deparse_RelSetOptions(AlterTableCmd *subcmd) +{ + List *sets = NIL; + ListCell *cell; + ObjTree *tmp; + bool is_reset = subcmd->subtype == AT_ResetRelOptions; + + if (is_reset) + tmp = new_objtree_VA("RESET (%{options:, }s)", 0); + else + tmp = new_objtree_VA("SET (%{options:, }s)", 0); + + foreach(cell, (List *) subcmd->def) + { + DefElem *elem; + ObjTree *set; + + elem = (DefElem *) lfirst(cell); + set = deparse_DefElem(elem, is_reset); + sets = lappend(sets, new_object_object(set)); + } + + append_array_object(tmp, "options", sets); + + return tmp; +} + +/* + * deparse_CreateStmt + * Deparse a CreateStmt (CREATE TABLE) + * + * Given a table OID and the parsetree that created it, return an ObjTree + * representing the creation command. + */ +static ObjTree * +deparse_CreateStmt(Oid objectId, Node *parsetree) +{ + CreateStmt *node = (CreateStmt *) parsetree; + Relation relation = relation_open(objectId, AccessShareLock); + List *dpcontext; + ObjTree *createStmt; + ObjTree *tmp; + List *list; + ListCell *cell; + char *fmtstr; + + /* + * Typed tables use a slightly different format string: we must not put + * table_elements with parents directly in the fmt string, because if + * there are no options the parens must not be emitted; and also, typed + * tables do not allow for inheritance. + */ + if (node->ofTypename) + fmtstr = "CREATE %{persistence}s TABLE %{if_not_exists}s %{identity}D " + "OF %{of_type}T %{table_elements}s " + "%{with_clause}s %{on_commit}s %{tablespace}s"; + else + fmtstr = "CREATE %{persistence}s TABLE %{if_not_exists}s %{identity}D " + "(%{table_elements:, }s) %{inherits}s " + "%{with_clause}s %{on_commit}s %{tablespace}s"; + + createStmt = + new_objtree_VA(fmtstr, 1, + "persistence", ObjTypeString, + get_persistence_str(relation->rd_rel->relpersistence)); + + tmp = new_objtree_for_qualname(relation->rd_rel->relnamespace, + RelationGetRelationName(relation)); + append_object_object(createStmt, "identity", tmp); + + append_string_object(createStmt, "if_not_exists", + node->if_not_exists ? "IF NOT EXISTS" : ""); + + dpcontext = deparse_context_for(RelationGetRelationName(relation), + objectId); + + if (node->ofTypename) + { + List *tableelts = NIL; + + /* + * We can't put table elements directly in the fmt string as an array + * surrounded by parens here, because an empty clause would cause a + * syntax error. Therefore, we use an indirection element and set + * present=false when there are no elements. + */ + append_string_object(createStmt, "table_kind", "typed"); + + tmp = new_objtree_for_type(relation->rd_rel->reloftype, -1); + append_object_object(createStmt, "of_type", tmp); + + tableelts = deparseTableElements(relation, node->tableElts, dpcontext, + true, /* typed table */ + false); /* not composite */ + tableelts = obtainConstraints(tableelts, objectId, InvalidOid); + if (tableelts == NIL) + tmp = new_objtree_VA("", 1, + "present", ObjTypeBool, false); + else + tmp = new_objtree_VA("(%{elements:, }s)", 1, + "elements", ObjTypeArray, tableelts); + append_object_object(createStmt, "table_elements", tmp); + } + else + { + List *tableelts = NIL; + + /* + * There is no need to process LIKE clauses separately; they have + * already been transformed into columns and constraints. + */ + append_string_object(createStmt, "table_kind", "plain"); + + /* + * Process table elements: column definitions and constraints. Only + * the column definitions are obtained from the parse node itself. To + * get constraints we rely on pg_constraint, because the parse node + * might be missing some things such as the name of the constraints. + */ + tableelts = deparseTableElements(relation, node->tableElts, dpcontext, + false, /* not typed table */ + false); /* not composite */ + tableelts = obtainConstraints(tableelts, objectId, InvalidOid); + + append_array_object(createStmt, "table_elements", tableelts); + + /* + * Add inheritance specification. We cannot simply scan the list of + * parents from the parser node, because that may lack the actual + * qualified names of the parent relations. Rather than trying to + * re-resolve them from the information in the parse node, it seems + * more accurate and convenient to grab it from pg_inherits. + */ + tmp = new_objtree_VA("INHERITS (%{parents:, }D)", 0); + if (list_length(node->inhRelations) > 0) + { + List *parents = NIL; + Relation inhRel; + SysScanDesc scan; + ScanKeyData key; + HeapTuple tuple; + + inhRel = table_open(InheritsRelationId, RowExclusiveLock); + + ScanKeyInit(&key, + Anum_pg_inherits_inhrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(objectId)); + + scan = systable_beginscan(inhRel, InheritsRelidSeqnoIndexId, + true, NULL, 1, &key); + + while (HeapTupleIsValid(tuple = systable_getnext(scan))) + { + ObjTree *parent; + Form_pg_inherits formInh = (Form_pg_inherits) GETSTRUCT(tuple); + + parent = new_objtree_for_qualname_id(RelationRelationId, + formInh->inhparent); + parents = lappend(parents, new_object_object(parent)); + } + + systable_endscan(scan); + table_close(inhRel, RowExclusiveLock); + + append_array_object(tmp, "parents", parents); + } + else + { + append_null_object(tmp, "parents"); + append_bool_object(tmp, "present", false); + } + append_object_object(createStmt, "inherits", tmp); + } + + tmp = new_objtree_VA("TABLESPACE %{tablespace}I", 0); + if (node->tablespacename) + append_string_object(tmp, "tablespace", node->tablespacename); + else + { + append_null_object(tmp, "tablespace"); + append_bool_object(tmp, "present", false); + } + append_object_object(createStmt, "tablespace", tmp); + + append_object_object(createStmt, "on_commit", + deparse_OnCommitClause(node->oncommit)); + + /* WITH clause */ + tmp = new_objtree_VA("WITH (%{with:, }s)", 0); + list = NIL; + + foreach(cell, node->options) + { + ObjTree *tmp2; + DefElem *opt = (DefElem *) lfirst(cell); + + tmp2 = deparse_DefElem(opt, false); + list = lappend(list, new_object_object(tmp2)); + } + + if (list) + append_array_object(tmp, "with", list); + else + append_bool_object(tmp, "present", false); + + append_object_object(createStmt, "with_clause", tmp); + + relation_close(relation, AccessShareLock); + + return createStmt; +} + +static ObjTree * +deparse_AlterTableStmt(CollectedCommand *cmd) +{ + ObjTree *alterTableStmt; + ObjTree *tmp; + ObjTree *tmp2; + List *dpcontext; + Relation rel; + List *subcmds = NIL; + ListCell *cell; + char *fmtstr; + const char *reltype; + bool istype = false; + + Assert(cmd->type == SCT_AlterTable); + + rel = relation_open(cmd->d.alterTable.objectId, AccessShareLock); + dpcontext = deparse_context_for(RelationGetRelationName(rel), + cmd->d.alterTable.objectId); + + switch (rel->rd_rel->relkind) + { + case RELKIND_RELATION: + reltype = "TABLE"; + break; + case RELKIND_INDEX: + reltype = "INDEX"; + break; + case RELKIND_VIEW: + reltype = "VIEW"; + break; + case RELKIND_COMPOSITE_TYPE: + reltype = "TYPE"; + istype = true; + break; + case RELKIND_FOREIGN_TABLE: + reltype = "FOREIGN TABLE"; + break; + + /* TODO support for partitioned table */ + + default: + elog(ERROR, "unexpected relkind %d", rel->rd_rel->relkind); + reltype = NULL;; + } + + fmtstr = psprintf("ALTER %s %%{identity}D %%{subcmds:, }s", reltype); + alterTableStmt = new_objtree_VA(fmtstr, 0); + + tmp = new_objtree_for_qualname(rel->rd_rel->relnamespace, + RelationGetRelationName(rel)); + append_object_object(alterTableStmt, "identity", tmp); + + foreach(cell, cmd->d.alterTable.subcmds) + { + CollectedATSubcmd *sub = (CollectedATSubcmd *) lfirst(cell); + AlterTableCmd *subcmd = (AlterTableCmd *) sub->parsetree; + ObjTree *tree; + + Assert(IsA(subcmd, AlterTableCmd)); + + switch (subcmd->subtype) + { + case AT_AddColumn: + case AT_AddColumnRecurse: + /* XXX need to set the "recurse" bit somewhere? */ + Assert(IsA(subcmd->def, ColumnDef)); + tree = deparse_ColumnDef(rel, dpcontext, false, + (ColumnDef *) subcmd->def, true); + fmtstr = psprintf("ADD %s %%{definition}s", + istype ? "ATTRIBUTE" : "COLUMN"); + tmp = new_objtree_VA(fmtstr, 2, + "type", ObjTypeString, "add column", + "definition", ObjTypeObject, tree); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_AddIndexConstraint: + { + IndexStmt *istmt; + Relation idx; + const char *idxname; + Oid constrOid = sub->address.objectId; + + Assert(IsA(subcmd->def, IndexStmt)); + istmt = (IndexStmt *) subcmd->def; + + Assert(istmt->isconstraint && istmt->unique); + + idx = relation_open(istmt->indexOid, AccessShareLock); + idxname = RelationGetRelationName(idx); + + tmp = new_objtree_VA("ADD CONSTRAINT %{name}I %{constraint_type}s USING INDEX %index_name}I %{deferrable}s %{init_deferred}s", + 4, "type", ObjTypeString, "add constraint using index", + "name", ObjTypeString, get_constraint_name(constrOid), + "constraint_type", ObjTypeString, + istmt->primary ? "PRIMARY KEY" : "UNIQUE", + "index_name", ObjTypeString, idxname); + + append_string_object(tmp, "deferrable", istmt->deferrable ? + "DEFERRABLE" : "NOT DEFERRABLE"); + append_string_object(tmp, "init_deferred", istmt->initdeferred ? + "INITIALLY DEFERRED" : "INITIALLY IMMEDIATE"); + + subcmds = lappend(subcmds, new_object_object(tmp)); + + relation_close(idx, AccessShareLock); + } + break; + + case AT_ReAddIndex: + case AT_ReAddConstraint: + case AT_ReAddComment: + case AT_ReplaceRelOptions: + /* Subtypes used for internal operations; nothing to do here */ + break; + + case AT_AddColumnToView: + /* CREATE OR REPLACE VIEW -- nothing to do here */ + break; + + case AT_ColumnDefault: + if (subcmd->def == NULL) + { + tmp = new_objtree_VA("ALTER COLUMN %{column}I DROP DEFAULT", + 1, "type", ObjTypeString, "drop default"); + } + else + { + List *dpcontext; + HeapTuple attrtup; + AttrNumber attno; + + tmp = new_objtree_VA("ALTER COLUMN %{column}I SET DEFAULT %{definition}s", + 1, "type", ObjTypeString, "set default"); + + dpcontext = deparse_context_for(RelationGetRelationName(rel), + RelationGetRelid(rel)); + attrtup = SearchSysCacheAttName(RelationGetRelid(rel), subcmd->name); + attno = ((Form_pg_attribute) GETSTRUCT(attrtup))->attnum; + append_string_object(tmp, "definition", + RelationGetColumnDefault(rel, attno, dpcontext)); + ReleaseSysCache(attrtup); + } + append_string_object(tmp, "column", subcmd->name); + + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_DropNotNull: + tmp = new_objtree_VA("ALTER COLUMN %{column}I DROP NOT NULL", + 1, "type", ObjTypeString, "drop not null"); + append_string_object(tmp, "column", subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_SetNotNull: + tmp = new_objtree_VA("ALTER COLUMN %{column}I SET NOT NULL", + 1, "type", ObjTypeString, "set not null"); + append_string_object(tmp, "column", subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_SetStatistics: + { + Assert(IsA(subcmd->def, Integer)); + tmp = new_objtree_VA("ALTER COLUMN %{column}I SET STATISTICS %{statistics}n", + 3, "type", ObjTypeString, "set statistics", + "column", ObjTypeString, subcmd->name, + "statistics", ObjTypeInteger, + intVal((Integer *) subcmd->def)); + subcmds = lappend(subcmds, new_object_object(tmp)); + } + break; + + case AT_SetOptions: + case AT_ResetOptions: + subcmds = lappend(subcmds, new_object_object( + deparse_ColumnSetOptions(subcmd))); + break; + + case AT_SetStorage: + Assert(IsA(subcmd->def, String)); + tmp = new_objtree_VA("ALTER COLUMN %{column}I SET STORAGE %{storage}s", + 3, "type", ObjTypeString, "set storage", + "column", ObjTypeString, subcmd->name, + "storage", ObjTypeString, + strVal((String *) subcmd->def)); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_DropColumnRecurse: + case AT_DropColumn: + fmtstr = psprintf("DROP %s %%{column}I %%{cascade}s", + istype ? "ATTRIBUTE" : "COLUMN"); + tmp = new_objtree_VA(fmtstr, 2, + "type", ObjTypeString, "drop column", + "column", ObjTypeString, subcmd->name); + tmp2 = new_objtree_VA("CASCADE", 1, + "present", ObjTypeBool, subcmd->behavior); + append_object_object(tmp, "cascade", tmp2); + + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_AddIndex: + { + Oid idxOid = sub->address.objectId; + IndexStmt *istmt; + Relation idx; + const char *idxname; + Oid constrOid; + + Assert(IsA(subcmd->def, IndexStmt)); + istmt = (IndexStmt *) subcmd->def; + + if (!istmt->isconstraint) + break; + + idx = relation_open(idxOid, AccessShareLock); + idxname = RelationGetRelationName(idx); + + constrOid = get_relation_constraint_oid( + cmd->d.alterTable.objectId, idxname, false); + + tmp = new_objtree_VA("ADD CONSTRAINT %{name}I %{definition}s", + 3, "type", ObjTypeString, "add constraint", + "name", ObjTypeString, idxname, + "definition", ObjTypeString, + pg_get_constraintdef_command_simple(constrOid)); + subcmds = lappend(subcmds, new_object_object(tmp)); + + relation_close(idx, AccessShareLock); + } + break; + + case AT_AddConstraint: + case AT_AddConstraintRecurse: + { + /* XXX need to set the "recurse" bit somewhere? */ + Oid constrOid = sub->address.objectId; + + tmp = new_objtree_VA("ADD CONSTRAINT %{name}I %{definition}s", + 3, "type", ObjTypeString, "add constraint", + "name", ObjTypeString, get_constraint_name(constrOid), + "definition", ObjTypeString, + pg_get_constraintdef_command_simple(constrOid)); + subcmds = lappend(subcmds, new_object_object(tmp)); + } + break; + + case AT_AlterConstraint: + { + Oid constrOid = sub->address.objectId; + Constraint *c = (Constraint *) subcmd->def; + + /* if no constraint was altered, silently skip it */ + if (!OidIsValid(constrOid)) + break; + + Assert(IsA(c, Constraint)); + tmp = new_objtree_VA("ALTER CONSTRAINT %{name}I %{deferrable}s %{init_deferred}s", + 2, "type", ObjTypeString, "alter constraint", + "name", ObjTypeString, get_constraint_name(constrOid)); + append_string_object(tmp, "deferrable", c->deferrable ? + "DEFERRABLE" : "NOT DEFERRABLE"); + append_string_object(tmp, "init_deferred", c->initdeferred ? + "INITIALLY DEFERRED" : "INITIALLY IMMEDIATE"); + subcmds = lappend(subcmds, new_object_object(tmp)); + } + break; + + case AT_ValidateConstraintRecurse: + case AT_ValidateConstraint: + tmp = new_objtree_VA("VALIDATE CONSTRAINT %{constraint}I", 2, + "type", ObjTypeString, "validate constraint", + "constraint", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_DropConstraintRecurse: + case AT_DropConstraint: + tmp = new_objtree_VA("DROP CONSTRAINT %{constraint}I", 2, + "type", ObjTypeString, "drop constraint", + "constraint", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_AlterColumnType: + { + TupleDesc tupdesc = RelationGetDescr(rel); + Form_pg_attribute att; + ColumnDef *def; + + att = &(tupdesc->attrs[sub->address.objectSubId - 1]); + def = (ColumnDef *) subcmd->def; + Assert(IsA(def, ColumnDef)); + + fmtstr = psprintf("ALTER %s %%{column}I SET DATA TYPE %%{datatype}T %%{collation}s %s", + istype ? "ATTRIBUTE" : "COLUMN", + istype ? "%{cascade}s" : "%{using}s"); + + tmp = new_objtree_VA(fmtstr, 2, + "type", ObjTypeString, "alter column type", + "column", ObjTypeString, subcmd->name); + /* add the TYPE clause */ + append_object_object(tmp, "datatype", + new_objtree_for_type(att->atttypid, + att->atttypmod)); + + /* add a COLLATE clause, if needed */ + tmp2 = new_objtree_VA("COLLATE %{name}D", 0); + if (OidIsValid(att->attcollation)) + { + ObjTree *collname; + + collname = new_objtree_for_qualname_id(CollationRelationId, + att->attcollation); + append_object_object(tmp2, "name", collname); + } + else + append_bool_object(tmp2, "present", false); + append_object_object(tmp, "collation", tmp2); + + /* if not a composite type, add the USING clause */ + if (!istype) + { + /* + * If there's a USING clause, transformAlterTableStmt + * ran it through transformExpr and stored the + * resulting node in cooked_default, which we can use + * here. + */ + tmp2 = new_objtree_VA("USING %{expression}s", 0); + if (def->raw_default) + { + Datum deparsed; + char *defexpr; + + defexpr = nodeToString(def->cooked_default); + deparsed = DirectFunctionCall2(pg_get_expr, + CStringGetTextDatum(defexpr), + RelationGetRelid(rel)); + append_string_object(tmp2, "expression", + TextDatumGetCString(deparsed)); + } + else + append_bool_object(tmp2, "present", false); + append_object_object(tmp, "using", tmp2); + } + + /* if it's a composite type, add the CASCADE clause */ + if (istype) + { + tmp2 = new_objtree_VA("CASCADE", 0); + if (subcmd->behavior != DROP_CASCADE) + append_bool_object(tmp2, "present", false); + append_object_object(tmp, "cascade", tmp2); + } + + subcmds = lappend(subcmds, new_object_object(tmp)); + } + break; + +#ifdef TODOLIST + case AT_AlterColumnGenericOptions: + tmp = deparse_FdwOptions((List *) subcmd->def, + subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; +#endif + case AT_ChangeOwner: + tmp = new_objtree_VA("OWNER TO %{owner}I", + 2, "type", ObjTypeString, "change owner", + "owner", ObjTypeString, + get_rolespec_name(subcmd->newowner)); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_ClusterOn: + tmp = new_objtree_VA("CLUSTER ON %{index}I", 2, + "type", ObjTypeString, "cluster on", + "index", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_DropCluster: + tmp = new_objtree_VA("SET WITHOUT CLUSTER", 1, + "type", ObjTypeString, "set without cluster"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_SetLogged: + tmp = new_objtree_VA("SET LOGGED", 1, + "type", ObjTypeString, "set logged"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_SetUnLogged: + tmp = new_objtree_VA("SET UNLOGGED", 1, + "type", ObjTypeString, "set unlogged"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + +/* +removed feature + case AT_AddOidsRecurse: + case AT_AddOids: + tmp = new_objtree_VA("SET WITH OIDS", 1, + "type", ObjTypeString, "set with oids"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; +*/ + case AT_DropOids: + tmp = new_objtree_VA("SET WITHOUT OIDS", 1, + "type", ObjTypeString, "set without oids"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + case AT_SetAccessMethod: + tmp = new_objtree_VA("SET ACCESS METHOD %{access_method}I", 2, + "type", ObjTypeString, "set access method", + "access_method", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + case AT_SetTableSpace: + tmp = new_objtree_VA("SET TABLESPACE %{tablespace}I", 2, + "type", ObjTypeString, "set tablespace", + "tablespace", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_SetRelOptions: + case AT_ResetRelOptions: + subcmds = lappend(subcmds, new_object_object( + deparse_RelSetOptions(subcmd))); + break; + + case AT_EnableTrig: + tmp = new_objtree_VA("ENABLE TRIGGER %{trigger}I", 2, + "type", ObjTypeString, "enable trigger", + "trigger", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_EnableAlwaysTrig: + tmp = new_objtree_VA("ENABLE ALWAYS TRIGGER %{trigger}I", 2, + "type", ObjTypeString, "enable always trigger", + "trigger", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_EnableReplicaTrig: + tmp = new_objtree_VA("ENABLE REPLICA TRIGGER %{trigger}I", 2, + "type", ObjTypeString, "enable replica trigger", + "trigger", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_DisableTrig: + tmp = new_objtree_VA("DISABLE TRIGGER %{trigger}I", 2, + "type", ObjTypeString, "disable trigger", + "trigger", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_EnableTrigAll: + tmp = new_objtree_VA("ENABLE TRIGGER ALL", 1, + "type", ObjTypeString, "enable trigger all"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_DisableTrigAll: + tmp = new_objtree_VA("DISABLE TRIGGER ALL", 1, + "type", ObjTypeString, "disable trigger all"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_EnableTrigUser: + tmp = new_objtree_VA("ENABLE TRIGGER USER", 1, + "type", ObjTypeString, "enable trigger user"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_DisableTrigUser: + tmp = new_objtree_VA("DISABLE TRIGGER USER", 1, + "type", ObjTypeString, "disable trigger user"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_EnableRule: + tmp = new_objtree_VA("ENABLE RULE %{rule}I", 2, + "type", ObjTypeString, "enable rule", + "rule", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_EnableAlwaysRule: + tmp = new_objtree_VA("ENABLE ALWAYS RULE %{rule}I", 2, + "type", ObjTypeString, "enable always rule", + "rule", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_EnableReplicaRule: + tmp = new_objtree_VA("ENABLE REPLICA RULE %{rule}I", 2, + "type", ObjTypeString, "enable replica rule", + "rule", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_DisableRule: + tmp = new_objtree_VA("DISABLE RULE %{rule}I", 2, + "type", ObjTypeString, "disable rule", + "rule", ObjTypeString, subcmd->name); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_AddInherit: + tmp = new_objtree_VA("INHERIT %{parent}D", + 2, "type", ObjTypeString, "inherit", + "parent", ObjTypeObject, + new_objtree_for_qualname_id(RelationRelationId, + sub->address.objectId)); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_DropInherit: + tmp = new_objtree_VA("NO INHERIT %{parent}D", + 2, "type", ObjTypeString, "drop inherit", + "parent", ObjTypeObject, + new_objtree_for_qualname_id(RelationRelationId, + sub->address.objectId)); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_AddOf: + tmp = new_objtree_VA("OF %{type_of}T", + 2, "type", ObjTypeString, "add of", + "type_of", ObjTypeObject, + new_objtree_for_type(sub->address.objectId, -1)); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_DropOf: + tmp = new_objtree_VA("NOT OF", + 1, "type", ObjTypeString, "not of"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_ReplicaIdentity: + tmp = new_objtree_VA("REPLICA IDENTITY %{ident}s", 1, + "type", ObjTypeString, "replica identity"); + switch (((ReplicaIdentityStmt *) subcmd->def)->identity_type) + { + case REPLICA_IDENTITY_DEFAULT: + append_string_object(tmp, "ident", "DEFAULT"); + break; + case REPLICA_IDENTITY_FULL: + append_string_object(tmp, "ident", "FULL"); + break; + case REPLICA_IDENTITY_NOTHING: + append_string_object(tmp, "ident", "NOTHING"); + break; + case REPLICA_IDENTITY_INDEX: + tmp2 = new_objtree_VA("USING INDEX %{index}I", 1, + "index", ObjTypeString, + ((ReplicaIdentityStmt *) subcmd->def)->name); + append_object_object(tmp, "ident", tmp2); + break; + } + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_EnableRowSecurity: + tmp = new_objtree_VA("ENABLE ROW LEVEL SECURITY", 1, + "type", ObjTypeString, "enable row security"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; + + case AT_DisableRowSecurity: + tmp = new_objtree_VA("DISABLE ROW LEVEL SECURITY", 1, + "type", ObjTypeString, "disable row security"); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; +#ifdef TODOLIST + case AT_GenericOptions: + tmp = deparse_FdwOptions((List *) subcmd->def, NULL); + subcmds = lappend(subcmds, new_object_object(tmp)); + break; +#endif + default: + elog(WARNING, "unsupported alter table subtype %d", + subcmd->subtype); + break; + } + } + + table_close(rel, AccessShareLock); + + if (list_length(subcmds) == 0) + return NULL; + + append_array_object(alterTableStmt, "subcmds", subcmds); + return alterTableStmt; +} + +/* + * Handle deparsing of simple commands. + * + * This function should cover all cases handled in ProcessUtilitySlow. + */ +static ObjTree * +deparse_simple_command(CollectedCommand *cmd) +{ + Oid objectId; + Node *parsetree; + ObjTree *command; + + Assert(cmd->type == SCT_Simple); + + parsetree = cmd->parsetree; + objectId = cmd->d.simple.address.objectId; + + /* This switch needs to handle everything that ProcessUtilitySlow does */ + switch (nodeTag(parsetree)) + { + case T_CreateStmt: + command = deparse_CreateStmt(objectId, parsetree); + break; + + default: + command = NULL; + elog(LOG, "unrecognized node type: %d", + (int) nodeTag(parsetree)); + } + + return command; +} + +char * +deparse_drop_table(const char *objidentity) +{ + StringInfoData str; + char *command; + ObjTree *stmt; + char *fmt; + Jsonb *jsonb; + + initStringInfo(&str); + fmt = psprintf("DROP TABLE IF EXISTS %%{objidentity}s"); + + stmt = new_objtree_VA(fmt, 1, "objidentity", ObjTypeString, + objidentity); + jsonb = objtree_to_jsonb(stmt); + command = JsonbToCString(&str, &jsonb->root, 128); + + return command; +} + +char * +deparse_utility_command(CollectedCommand *cmd) +{ + OverrideSearchPath *overridePath; + MemoryContext oldcxt; + MemoryContext tmpcxt; + ObjTree *tree; + char *command; + StringInfoData str; + + /* + * Allocate everything done by the deparsing routines into a temp context, + * to avoid having to sprinkle them with memory handling code; but allocate + * the output StringInfo before switching. + */ + initStringInfo(&str); + tmpcxt = AllocSetContextCreate(CurrentMemoryContext, + "deparse ctx", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + ALLOCSET_DEFAULT_MAXSIZE); + oldcxt = MemoryContextSwitchTo(tmpcxt); + + /* + * Many routines underlying this one will invoke ruleutils.c functionality + * in order to obtain deparsed versions of expressions. In such results, + * we want all object names to be qualified, so that results are "portable" + * to environments with different search_path settings. Rather than inject + * what would be repetitive calls to override search path all over the + * place, we do it centrally here. + */ + overridePath = GetOverrideSearchPath(CurrentMemoryContext); + overridePath->schemas = NIL; + overridePath->addCatalog = false; + overridePath->addTemp = true; + PushOverrideSearchPath(overridePath); + + switch (cmd->type) + { + case SCT_Simple: + tree = deparse_simple_command(cmd); + break; + case SCT_AlterTable: + tree = deparse_AlterTableStmt(cmd); + break; + default: + elog(ERROR, "unexpected deparse node type %d", cmd->type); + } + + PopOverrideSearchPath(); + + if (tree) + { + Jsonb *jsonb; + + jsonb = objtree_to_jsonb(tree); + command = JsonbToCString(&str, &jsonb->root, 128); + } + else + command = NULL; + + /* + * Clean up. Note that since we created the StringInfo in the caller's + * context, the output string is not deleted here. + */ + MemoryContextSwitchTo(oldcxt); + MemoryContextDelete(tmpcxt); + + return command; +} + +/* + * Given a CollectedCommand, return a JSON representation of it. + * + * The command is expanded fully, so that there are no ambiguities even in the + * face of search_path changes. + */ +Datum +ddl_deparse_to_json(PG_FUNCTION_ARGS) +{ + CollectedCommand *cmd = (CollectedCommand *) PG_GETARG_POINTER(0); + char *command; + + command = deparse_utility_command(cmd); + + if (command) + PG_RETURN_TEXT_P(CStringGetTextDatum(command)); + else + PG_RETURN_NULL(); +} diff --git a/src/backend/commands/ddl_json.c b/src/backend/commands/ddl_json.c new file mode 100644 index 0000000..95ed18f --- /dev/null +++ b/src/backend/commands/ddl_json.c @@ -0,0 +1,749 @@ +/*------------------------------------------------------------------------- + * + * ddl_json.c + * JSON code related to DDL command deparsing + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/backend/commands/ddl_json.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "lib/stringinfo.h" +#include "utils/builtins.h" +#include "utils/jsonb.h" + + +typedef enum +{ + SpecTypename, + SpecOperatorname, + SpecDottedName, + SpecString, + SpecNumber, + SpecStringLiteral, + SpecIdentifier, + SpecRole +} convSpecifier; + +typedef enum +{ + tv_absent, + tv_true, + tv_false +} trivalue; + +static bool expand_one_jsonb_element(StringInfo out, char *param, + JsonbValue *jsonval, convSpecifier specifier, + const char *fmt); +static void expand_jsonb_array(StringInfo out, char *param, + JsonbValue *jsonarr, char *arraysep, + convSpecifier specifier, const char *fmt); +static void fmtstr_error_callback(void *arg); +char *ddl_deparse_json_to_string(char *jsonb); + +static trivalue +find_bool_in_jsonbcontainer(JsonbContainer *container, char *keyname) +{ + JsonbValue key; + JsonbValue *value; + trivalue result; + + key.type = jbvString; + key.val.string.val = keyname; + key.val.string.len = strlen(keyname); + value = findJsonbValueFromContainer(container, + JB_FOBJECT, &key); + if (value == NULL) + return tv_absent; + if (value->type != jbvBool) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("element \"%s\" is not of type boolean", + keyname))); + result = value->val.boolean ? tv_true : tv_false; + pfree(value); + + return result; +} + +/* + * Given a JsonbContainer, find the JsonbValue with the given key name in it. + * If it's of a type other than jbvString, an error is raised. If it doesn't + * exist, an error is raised if missing_ok; otherwise return NULL. + * + * If it exists and is a string, a freshly palloc'ed copy is returned. + * + * If *length is not NULL, it is set to the length of the string. + */ +static char * +find_string_in_jsonbcontainer(JsonbContainer *container, char *keyname, + bool missing_ok, int *length) +{ + JsonbValue key; + JsonbValue *value; + char *str; + + /* XXX verify that this is an object, not an array */ + + key.type = jbvString; + key.val.string.val = keyname; + key.val.string.len = strlen(keyname); + value = findJsonbValueFromContainer(container, + JB_FOBJECT, &key); + if (value == NULL) + { + if (missing_ok) + return NULL; + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("missing element \"%s\" in json object", keyname))); + } + + str = pnstrdup(value->val.string.val, value->val.string.len); + if (length) + *length = value->val.string.len; + pfree(value); + return str; +} + +#define ADVANCE_PARSE_POINTER(ptr,end_ptr) \ + do { \ + if (++(ptr) >= (end_ptr)) \ + ereport(ERROR, \ + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), \ + errmsg("unterminated format specifier"))); \ + } while (0) + +/* + * Recursive helper for pg_event_trigger_expand_command + * + * Find the "fmt" element in the given container, and expand it into the + * provided StringInfo. + */ +static void +expand_fmt_recursive(JsonbContainer *container, StringInfo out) +{ + JsonbValue key; + JsonbValue *value; + const char *cp; + const char *start_ptr; + const char *end_ptr; + int len; + + start_ptr = find_string_in_jsonbcontainer(container, "fmt", false, &len); + end_ptr = start_ptr + len; + + for (cp = start_ptr; cp < end_ptr; cp++) + { + convSpecifier specifier; + bool is_array; + char *param = NULL; + char *arraysep = NULL; + + if (*cp != '%') + { + appendStringInfoCharMacro(out, *cp); + continue; + } + + is_array = false; + + ADVANCE_PARSE_POINTER(cp, end_ptr); + + /* Easy case: %% outputs a single % */ + if (*cp == '%') + { + appendStringInfoCharMacro(out, *cp); + continue; + } + + /* + * Scan the mandatory element name. Allow for an array separator + * (which may be the empty string) to be specified after colon. + */ + if (*cp == '{') + { + StringInfoData parbuf; + StringInfoData arraysepbuf; + StringInfo appendTo; + + initStringInfo(&parbuf); + appendTo = &parbuf; + + ADVANCE_PARSE_POINTER(cp, end_ptr); + for (; cp < end_ptr;) + { + if (*cp == ':') + { + /* + * found array separator delimiter; element name is now + * complete, start filling the separator. + */ + initStringInfo(&arraysepbuf); + appendTo = &arraysepbuf; + is_array = true; + ADVANCE_PARSE_POINTER(cp, end_ptr); + continue; + } + + if (*cp == '}') + { + ADVANCE_PARSE_POINTER(cp, end_ptr); + break; + } + appendStringInfoCharMacro(appendTo, *cp); + ADVANCE_PARSE_POINTER(cp, end_ptr); + } + param = parbuf.data; + if (is_array) + arraysep = arraysepbuf.data; + } + if (param == NULL) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("missing conversion name in conversion specifier"))); + + switch (*cp) + { + case 'I': + specifier = SpecIdentifier; + break; + case 'D': + specifier = SpecDottedName; + break; + case 's': + specifier = SpecString; + break; + case 'L': + specifier = SpecStringLiteral; + break; + case 'T': + specifier = SpecTypename; + break; + case 'O': + specifier = SpecOperatorname; + break; + case 'n': + specifier = SpecNumber; + break; + case 'R': + specifier = SpecRole; + break; + default: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("invalid conversion specifier \"%c\"", *cp))); + } + + /* + * Obtain the element to be expanded. + */ + key.type = jbvString; + key.val.string.val = param; + key.val.string.len = strlen(param); + + value = findJsonbValueFromContainer(container, JB_FOBJECT, &key); + + /* Validate that we got an array if the format string specified one. */ + + /* And finally print out the data */ + if (is_array) + expand_jsonb_array(out, param, value, arraysep, specifier, start_ptr); + else + expand_one_jsonb_element(out, param, value, specifier, start_ptr); + } +} + +/* + * Expand a json value as an identifier. The value must be of type string. + */ +static void +expand_jsonval_identifier(StringInfo buf, JsonbValue *jsonval) +{ + char *str; + + Assert(jsonval->type == jbvString); + + str = pnstrdup(jsonval->val.string.val, + jsonval->val.string.len); + appendStringInfoString(buf, quote_identifier(str)); + pfree(str); +} + +/* + * Expand a json value as a dot-separated-name. The value must be of type + * object and must contain elements "schemaname" (optional), "objname" + * (mandatory), "attrname" (optional). Double quotes are added to each element + * as necessary, and dot separators where needed. + * + * One day we might need a "catalog" element as well, but no current use case + * needs that. + */ +static void +expand_jsonval_dottedname(StringInfo buf, JsonbValue *jsonval) +{ + char *str; + + str = find_string_in_jsonbcontainer(jsonval->val.binary.data, + "schemaname", true, NULL); + if (str) + { + appendStringInfo(buf, "%s.", quote_identifier(str)); + pfree(str); + } + + str = find_string_in_jsonbcontainer(jsonval->val.binary.data, + "objname", false, NULL); + appendStringInfo(buf, "%s", quote_identifier(str)); + pfree(str); + + str = find_string_in_jsonbcontainer(jsonval->val.binary.data, + "attrname", true, NULL); + if (str) + { + appendStringInfo(buf, ".%s", quote_identifier(str)); + pfree(str); + } +} + +/* + * expand a json value as a type name. + */ +static void +expand_jsonval_typename(StringInfo buf, JsonbValue *jsonval) +{ + char *schema = NULL; + char *typename; + char *typmodstr; + trivalue is_array; + char *array_decor; + + /* + * We omit schema-qualifying the output name if the schema element is + * either the empty string or NULL; the difference between those two cases + * is that in the latter we quote the type name, in the former we don't. + * This allows for types with special typmod needs, such as interval and + * timestamp (see format_type_detailed), while at the same time allowing + * for the schema name to be omitted from type names that require quotes + * but are to be obtained from a user schema. + */ + + schema = find_string_in_jsonbcontainer(jsonval->val.binary.data, + "schemaname", true, NULL); + typename = find_string_in_jsonbcontainer(jsonval->val.binary.data, + "typename", false, NULL); + typmodstr = find_string_in_jsonbcontainer(jsonval->val.binary.data, + "typmod", true, NULL); + is_array = find_bool_in_jsonbcontainer(jsonval->val.binary.data, + "typarray"); + switch (is_array) + { + default: + case tv_absent: + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("missing typarray element"))); + break; + case tv_true: + array_decor = "[]"; + break; + case tv_false: + array_decor = ""; + break; + } + + if (schema == NULL) + appendStringInfo(buf, "%s%s%s", + quote_identifier(typename), + typmodstr ? typmodstr : "", + array_decor); + else if (schema[0] == '\0') + appendStringInfo(buf, "%s%s%s", + typename, + typmodstr ? typmodstr : "", + array_decor); + else + appendStringInfo(buf, "%s.%s%s%s", + quote_identifier(schema), + quote_identifier(typename), + typmodstr ? typmodstr : "", + array_decor); +} + +/* + * Expand a json value as an operator name + */ +static void +expand_jsonval_operator(StringInfo buf, JsonbValue *jsonval) +{ + char *str; + + str = find_string_in_jsonbcontainer(jsonval->val.binary.data, + "schemaname", true, NULL); + /* schema might be NULL or empty */ + if (str != NULL && str[0] != '\0') + appendStringInfo(buf, "%s.", quote_identifier(str)); + + str = find_string_in_jsonbcontainer(jsonval->val.binary.data, + "objname", false, NULL); + appendStringInfoString(buf, str); +} + +/* + * Expand a json value as a string. The value must be of type string or of + * type object. In the latter case it must contain a "fmt" element which will + * be recursively expanded; also, if the object contains an element "present" + * and it is set to false, the expansion is the empty string. + * + * Returns false if no actual expansion was made due to the "present" flag + * being set to "false". + */ +static bool +expand_jsonval_string(StringInfo buf, JsonbValue *jsonval) +{ + if (jsonval->type == jbvString) + { + appendBinaryStringInfo(buf, jsonval->val.string.val, + jsonval->val.string.len); + } + else if (jsonval->type == jbvBinary) + { + trivalue present; + + present = find_bool_in_jsonbcontainer(jsonval->val.binary.data, + "present"); + /* + * If "present" is set to false, this element expands to empty; + * otherwise (either true or absent), fall through to expand "fmt". + */ + if (present == tv_false) + return false; + + expand_fmt_recursive(jsonval->val.binary.data, buf); + } + return true; +} + +/* + * Expand a json value as a string literal + */ +static void +expand_jsonval_strlit(StringInfo buf, JsonbValue *jsonval) +{ + char *str; + StringInfoData dqdelim; + static const char dqsuffixes[] = "_XYZZYX_"; + int dqnextchar = 0; + + str = pnstrdup(jsonval->val.string.val, jsonval->val.string.len); + + /* easy case: if there are no ' and no \, just use a single quote */ + if (strchr(str, '\'') == NULL && + strchr(str, '\\') == NULL) + { + appendStringInfo(buf, "'%s'", str); + pfree(str); + return; + } + + /* Otherwise need to find a useful dollar-quote delimiter */ + initStringInfo(&dqdelim); + appendStringInfoString(&dqdelim, "$"); + while (strstr(str, dqdelim.data) != NULL) + { + appendStringInfoChar(&dqdelim, dqsuffixes[dqnextchar++]); + dqnextchar %= sizeof(dqsuffixes) - 1; + } + /* add trailing $ */ + appendStringInfoChar(&dqdelim, '$'); + + /* And finally produce the quoted literal into the output StringInfo */ + appendStringInfo(buf, "%s%s%s", dqdelim.data, str, dqdelim.data); + pfree(dqdelim.data); + pfree(str); +} + +/* + * Expand a json value as an integer quantity + */ +static void +expand_jsonval_number(StringInfo buf, JsonbValue *jsonval) +{ + char *strdatum; + + strdatum = DatumGetCString(DirectFunctionCall1(numeric_out, + NumericGetDatum(jsonval->val.numeric))); + appendStringInfoString(buf, strdatum); +} + +/* + * Expand a json value as a role name. If the is_public element is set to + * true, PUBLIC is expanded (no quotes); otherwise, expand the given role name, + * quoting as an identifier. + */ +static void +expand_jsonval_role(StringInfo buf, JsonbValue *jsonval) +{ + trivalue is_public; + + is_public = find_bool_in_jsonbcontainer(jsonval->val.binary.data, + "is_public"); + if (is_public == tv_true) + appendStringInfoString(buf, "PUBLIC"); + else + { + char *rolename; + + rolename = find_string_in_jsonbcontainer(jsonval->val.binary.data, + "rolename", false, NULL); + appendStringInfoString(buf, quote_identifier(rolename)); + } +} + +/* + * Expand one json element into the output StringInfo according to the + * conversion specifier. The element type is validated, and an error is raised + * if it doesn't match what we expect for the conversion specifier. + * + * Returns false if no actual expansion was made (due to the "present" flag + * being set to "false" in formatted string expansion). + */ +static bool +expand_one_jsonb_element(StringInfo out, char *param, JsonbValue *jsonval, + convSpecifier specifier, const char *fmt) +{ + bool result = true; + ErrorContextCallback sqlerrcontext; + + /* If we were given a format string, setup an ereport() context callback */ + if (fmt) + { + sqlerrcontext.callback = fmtstr_error_callback; + sqlerrcontext.arg = (void *) fmt; + sqlerrcontext.previous = error_context_stack; + error_context_stack = &sqlerrcontext; + } + + if (!jsonval) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("element \"%s\" not found", param))); + + switch (specifier) + { + case SpecIdentifier: + if (jsonval->type != jbvString) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("expected JSON string for %%I element \"%s\", got %d", + param, jsonval->type))); + expand_jsonval_identifier(out, jsonval); + break; + + case SpecDottedName: + if (jsonval->type != jbvBinary) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("expected JSON object for %%D element \"%s\", got %d", + param, jsonval->type))); + expand_jsonval_dottedname(out, jsonval); + break; + + case SpecString: + if (jsonval->type != jbvString && + jsonval->type != jbvBinary) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("expected JSON string or object for %%s element \"%s\", got %d", + param, jsonval->type))); + result = expand_jsonval_string(out, jsonval); + break; + + case SpecStringLiteral: + if (jsonval->type != jbvString) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("expected JSON string for %%L element \"%s\", got %d", + param, jsonval->type))); + expand_jsonval_strlit(out, jsonval); + break; + + case SpecTypename: + if (jsonval->type != jbvBinary) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("expected JSON object for %%T element \"%s\", got %d", + param, jsonval->type))); + expand_jsonval_typename(out, jsonval); + break; + + case SpecOperatorname: + if (jsonval->type != jbvBinary) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("expected JSON object for %%O element \"%s\", got %d", + param, jsonval->type))); + expand_jsonval_operator(out, jsonval); + break; + + case SpecNumber: + if (jsonval->type != jbvNumeric) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("expected JSON numeric for %%n element \"%s\", got %d", + param, jsonval->type))); + expand_jsonval_number(out, jsonval); + break; + + case SpecRole: + if (jsonval->type != jbvBinary) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("expected JSON object for %%R element \"%s\", got %d", + param, jsonval->type))); + expand_jsonval_role(out, jsonval); + break; + } + + if (fmt) + error_context_stack = sqlerrcontext.previous; + + return result; +} + +/* + * Iterate on the elements of a JSON array, expanding each one into the output + * StringInfo per the given conversion specifier, separated by the given + * separator. + */ +static void +expand_jsonb_array(StringInfo out, char *param, + JsonbValue *jsonarr, char *arraysep, convSpecifier specifier, + const char *fmt) +{ + ErrorContextCallback sqlerrcontext; + JsonbContainer *container; + JsonbIterator *it; + JsonbValue v; + int type; + size_t arrayseplen; + bool first = true; + + /* If we were given a format string, setup an ereport() context callback */ + if (fmt) + { + sqlerrcontext.callback = fmtstr_error_callback; + sqlerrcontext.arg = (void *) fmt; + sqlerrcontext.previous = error_context_stack; + error_context_stack = &sqlerrcontext; + } + + if (jsonarr->type != jbvBinary) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("element \"%s\" is not a JSON array", param))); + + container = jsonarr->val.binary.data; + if ((container->header & JB_FARRAY) == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("element \"%s\" is not a JSON array", param))); + + arrayseplen = strlen(arraysep); + + it = JsonbIteratorInit(container); + while ((type = JsonbIteratorNext(&it, &v, true)) != WJB_DONE) + { + switch (type) + { + case WJB_ELEM: + if (!first) + appendStringInfoString(out, arraysep); + + if (expand_one_jsonb_element(out, param, &v, specifier, NULL)) + first = false; + else + { + if (!first) + { + /* remove the array separator */ + out->len -= arrayseplen; + out->data[out->len] = '\0'; + } + } + break; + } + } + + if (fmt) + error_context_stack = sqlerrcontext.previous; +} + +char * +ddl_deparse_json_to_string(char *json_str) +{ + Datum d; + Jsonb *jsonb; + StringInfo out = (StringInfo) palloc0(sizeof(StringInfoData)); + + initStringInfo(out); + + d = DirectFunctionCall1(jsonb_in, + PointerGetDatum(json_str)); + jsonb = (Jsonb *) DatumGetPointer(d); + + expand_fmt_recursive(&jsonb->root, out); + + return out->data; +} + +/*------ + * Returns a formatted string from a JSON object. + * + * The starting point is the element named "fmt" (which must be a string). + * This format string may contain zero or more %-escapes, which consist of an + * element name enclosed in { }, possibly followed by a conversion modifier, + * followed by a conversion specifier. Possible conversion specifiers are: + * + * % expand to a literal %. + * I expand as a single, non-qualified identifier + * D expand as a possibly-qualified identifier + * T expand as a type name + * O expand as an operator name + * L expand as a string literal (quote using single quotes) + * s expand as a simple string (no quoting) + * n expand as a simple number (no quoting) + * R expand as a role name (possibly quoted name, or PUBLIC) + * + * The element name may have an optional separator specification preceded + * by a colon. Its presence indicates that the element is expected to be + * an array; the specified separator is used to join the array elements. + *------ + */ +Datum +ddl_deparse_expand_command(PG_FUNCTION_ARGS) +{ + text *json = PG_GETARG_TEXT_P(0); + char *json_str; + + json_str = TextDatumGetCString(json); + + PG_RETURN_TEXT_P(CStringGetTextDatum(ddl_deparse_json_to_string(json_str))); +} + +/* + * Error context callback for JSON format string expansion. + * + * Possible improvement: indicate which element we're expanding, if applicable + */ +static void +fmtstr_error_callback(void *arg) +{ + errcontext("while expanding format string \"%s\"", (char *) arg); + +} diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index c3937a6..2c4bcdc 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -2143,6 +2143,15 @@ pg_get_constraintdef_ext(PG_FUNCTION_ARGS) } /* + * Internal version that returns definition of a CONSTRAINT command + */ +char * +pg_get_constraintdef_command_simple(Oid constraintId) +{ + return pg_get_constraintdef_worker(constraintId, false, 0, false); +} + +/* * Internal version that returns a full ALTER TABLE ... ADD CONSTRAINT command */ char * diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 87aa571..8aa636c 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -11884,5 +11884,10 @@ proname => 'brin_minmax_multi_summary_send', provolatile => 's', prorettype => 'bytea', proargtypes => 'pg_brin_minmax_multi_summary', prosrc => 'brin_minmax_multi_summary_send' }, - +{ oid => '4642', descr => 'ddl deparse', + proname => 'ddl_deparse_to_json', prorettype => 'text', + proargtypes => 'pg_ddl_command', prosrc => 'ddl_deparse_to_json' }, +{ oid => '4643', descr => 'json to string', + proname => 'ddl_deparse_expand_command', prorettype => 'text', + proargtypes => 'text', prosrc => 'ddl_deparse_expand_command' }, ] diff --git a/src/include/tcop/ddl_deparse.h b/src/include/tcop/ddl_deparse.h new file mode 100644 index 0000000..fb174d2 --- /dev/null +++ b/src/include/tcop/ddl_deparse.h @@ -0,0 +1,10 @@ +#ifndef DDL_DEPARSE_H +#define DDL_DEPARSE_H + +#include "tcop/deparse_utility.h" + +extern char *deparse_utility_command(CollectedCommand *cmd); +extern char *ddl_deparse_json_to_string(char *jsonb); +extern char *deparse_drop_table(const char *objidentity); + +#endif /* DDL_DEPARSE_H */ diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h index 7d48971..467de7c 100644 --- a/src/include/utils/ruleutils.h +++ b/src/include/utils/ruleutils.h @@ -29,6 +29,7 @@ extern char *pg_get_partkeydef_columns(Oid relid, bool pretty); extern char *pg_get_partconstrdef_string(Oid partitionId, char *aliasname); extern char *pg_get_constraintdef_command(Oid constraintId); +extern char *pg_get_constraintdef_command_simple(Oid constraintId); extern char *deparse_expression(Node *expr, List *dpcontext, bool forceprefix, bool showimplicit); extern List *deparse_context_for(const char *aliasname, Oid relid); -- 2.7.2.windows.1