diff --git a/src/backend/distributed/commands/create_distributed_table.c b/src/backend/distributed/commands/create_distributed_table.c index 44bcb0d45..7f10a40fb 100644 --- a/src/backend/distributed/commands/create_distributed_table.c +++ b/src/backend/distributed/commands/create_distributed_table.c @@ -99,6 +99,7 @@ static void EnsureTableCanBeColocatedWith(Oid relationId, char replicationModel, Oid sourceRelationId); static void EnsureLocalTableEmpty(Oid relationId); static void EnsureTableNotDistributed(Oid relationId); +static void EnsureRelationHasNoTriggers(Oid relationId); static Oid SupportFunctionForColumn(Var *partitionColumn, Oid accessMethodId, int16 supportFunctionNumber); static void EnsureLocalTableEmptyIfNecessary(Oid relationId, char distributionMethod, @@ -650,6 +651,7 @@ EnsureRelationCanBeDistributed(Oid relationId, Var *distributionColumn, EnsureTableNotDistributed(relationId); EnsureLocalTableEmptyIfNecessary(relationId, distributionMethod, viaDeprecatedAPI); EnsureReplicationSettings(InvalidOid, replicationModel); + EnsureRelationHasNoTriggers(relationId); /* we assume callers took necessary locks */ Relation relation = relation_open(relationId, NoLock); @@ -972,6 +974,31 @@ EnsureReplicationSettings(Oid relationId, char replicationModel) } +/* + * EnsureRelationHasNoTriggers errors out if the given table has triggers on + * it. See also GetExplicitTriggerIdList function's comment for the triggers this + * function errors out. + */ +static void +EnsureRelationHasNoTriggers(Oid relationId) +{ + List *explicitTriggerIds = GetExplicitTriggerIdList(relationId); + + if (list_length(explicitTriggerIds) > 0) + { + char *relationName = get_rel_name(relationId); + + Assert(relationName != NULL); + ereport(ERROR, (errmsg("cannot distribute relation \"%s\" because it has " + "triggers ", relationName), + errdetail("Citus does not support distributing tables with " + "triggers."), + errhint("Drop all the triggers on \"%s\" and retry.", + relationName))); + } +} + + /* * LookupDistributionMethod maps the oids of citus.distribution_type enum * values to pg_dist_partition.partmethod values. @@ -1176,7 +1203,7 @@ CreateTruncateTrigger(Oid relationId) CreateTrigStmt *trigger = makeNode(CreateTrigStmt); trigger->trigname = triggerName->data; trigger->relation = NULL; - trigger->funcname = SystemFuncName("citus_truncate_trigger"); + trigger->funcname = SystemFuncName(CITUS_TRUNCATE_TRIGGER_NAME); trigger->args = NIL; trigger->row = false; trigger->timing = TRIGGER_TYPE_AFTER; diff --git a/src/backend/distributed/commands/trigger.c b/src/backend/distributed/commands/trigger.c new file mode 100644 index 000000000..a2bb1cf3c --- /dev/null +++ b/src/backend/distributed/commands/trigger.c @@ -0,0 +1,186 @@ +/*------------------------------------------------------------------------- + * trigger.c + * + * This file contains functions to create and process trigger objects on + * citus tables. + * + * Copyright (c) Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" +#include "distributed/pg_version_constants.h" + +#include "access/genam.h" +#if PG_VERSION_NUM >= PG_VERSION_12 +#include "access/table.h" +#else +#include "access/heapam.h" +#include "access/htup_details.h" +#endif +#include "catalog/indexing.h" +#include "catalog/namespace.h" +#include "catalog/pg_trigger.h" +#include "distributed/citus_ruleutils.h" +#include "distributed/commands.h" +#include "distributed/listutils.h" +#include "distributed/metadata_cache.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" + +/* + * GetExplicitTriggerCommandList returns the list of DDL commands to create + * triggers that are explicitly created for the table with relationId. See + * comment of GetExplicitTriggerIdList function. + */ +List * +GetExplicitTriggerCommandList(Oid relationId) +{ + List *createTriggerCommandList = NIL; + + /* + * Set search_path to NIL so that all objects outside of pg_catalog will be + * schema-prefixed. pg_catalog will be added automatically when we call + * PushOverrideSearchPath(), since we set addCatalog to true; + */ + OverrideSearchPath *overridePath = GetOverrideSearchPath(CurrentMemoryContext); + overridePath->schemas = NIL; + overridePath->addCatalog = true; + PushOverrideSearchPath(overridePath); + + List *triggerIdList = GetExplicitTriggerIdList(relationId); + + Oid triggerId = InvalidOid; + foreach_oid(triggerId, triggerIdList) + { + char *createTriggerCommand = pg_get_triggerdef_command(triggerId); + + createTriggerCommandList = lappend(createTriggerCommandList, + createTriggerCommand); + } + + /* revert back to original search_path */ + PopOverrideSearchPath(); + + return createTriggerCommandList; +} + + +/* + * GetExplicitTriggerIdList returns a list of OIDs corresponding to the triggers + * that are explicitly created on the relation with relationId. That means, + * this function discards internal triggers implicitly created by postgres for + * foreign key constraint validation and the citus_truncate_trigger. + */ +List * +GetExplicitTriggerIdList(Oid relationId) +{ + List *triggerIdList = NIL; + + Relation pgTrigger = heap_open(TriggerRelationId, AccessShareLock); + + int scanKeyCount = 1; + ScanKeyData scanKey[1]; + + ScanKeyInit(&scanKey[0], Anum_pg_trigger_tgrelid, + BTEqualStrategyNumber, F_OIDEQ, relationId); + + bool useIndex = true; + SysScanDesc scanDescriptor = systable_beginscan(pgTrigger, TriggerRelidNameIndexId, + useIndex, NULL, scanKeyCount, + scanKey); + + HeapTuple heapTuple = systable_getnext(scanDescriptor); + while (HeapTupleIsValid(heapTuple)) + { + Form_pg_trigger triggerForm = (Form_pg_trigger) GETSTRUCT(heapTuple); + + /* + * Note that we mark truncate trigger that we create on citus tables as + * internal. Hence, below we discard citus_truncate_trigger as well as + * the implicit triggers created by postgres for foreign key validation. + */ + if (!triggerForm->tgisinternal) + { + Oid triggerId = get_relation_trigger_oid_compat(heapTuple); + triggerIdList = lappend_oid(triggerIdList, triggerId); + } + + heapTuple = systable_getnext(scanDescriptor); + } + + systable_endscan(scanDescriptor); + heap_close(pgTrigger, NoLock); + + return triggerIdList; +} + + +/* + * get_relation_trigger_oid_compat returns OID of the trigger represented + * by the constraintForm, which is passed as an heapTuple. OID of the + * trigger is already stored in the triggerForm struct if major PostgreSQL + * version is 12. However, in the older versions, we should utilize + * HeapTupleGetOid to deduce that OID with no cost. + */ +Oid +get_relation_trigger_oid_compat(HeapTuple heapTuple) +{ + Assert(HeapTupleIsValid(heapTuple)); + + Oid triggerOid = InvalidOid; + +#if PG_VERSION_NUM >= PG_VERSION_12 + Form_pg_trigger triggerForm = (Form_pg_trigger) GETSTRUCT(heapTuple); + triggerOid = triggerForm->oid; +#else + triggerOid = HeapTupleGetOid(heapTuple); +#endif + + return triggerOid; +} + + +/* + * ErrorIfUnsupportedCreateTriggerCommand errors out for the CREATE TRIGGER + * command that is run for a citus table if it is not citus_truncate_trigger. + * + * Note that internal triggers that are created implicitly by postgres for + * foreign key validation already wouldn't be executed via process utility, + * hence there is no need to check that case here. + */ +void +ErrorIfUnsupportedCreateTriggerCommand(CreateTrigStmt *createTriggerStmt) +{ + RangeVar *triggerRelation = createTriggerStmt->relation; + + bool missingOk = true; + Oid relationId = RangeVarGetRelid(triggerRelation, AccessShareLock, missingOk); + + if (!OidIsValid(relationId)) + { + /* + * standard_ProcessUtility would already error out if the given table + * does not exist + */ + return; + } + + if (!IsCitusTable(relationId)) + { + return; + } + + char *functionName = makeRangeVarFromNameList(createTriggerStmt->funcname)->relname; + if (strncmp(functionName, CITUS_TRUNCATE_TRIGGER_NAME, NAMEDATALEN) == 0) + { + return; + } + + char *relationName = triggerRelation->relname; + + Assert(relationName != NULL); + ereport(ERROR, (errmsg("cannot create trigger on relation \"%s\" because it " + "is either a distributed table or a reference table", + relationName))); +} diff --git a/src/backend/distributed/commands/utility_hook.c b/src/backend/distributed/commands/utility_hook.c index fbed95a5f..b8ad9bdb6 100644 --- a/src/backend/distributed/commands/utility_hook.c +++ b/src/backend/distributed/commands/utility_hook.c @@ -208,6 +208,13 @@ multi_ProcessUtility(PlannedStmt *pstmt, parsetree = ProcessCreateSubscriptionStmt(createSubStmt); } + if (IsA(parsetree, CreateTrigStmt)) + { + CreateTrigStmt *createTriggerStmt = (CreateTrigStmt *) parsetree; + + ErrorIfUnsupportedCreateTriggerCommand(createTriggerStmt); + } + if (IsA(parsetree, CallStmt)) { CallStmt *callStmt = (CallStmt *) parsetree; diff --git a/src/backend/distributed/deparser/ruleutils_11.c b/src/backend/distributed/deparser/ruleutils_11.c index 3a522c296..292bb8936 100644 --- a/src/backend/distributed/deparser/ruleutils_11.c +++ b/src/backend/distributed/deparser/ruleutils_11.c @@ -100,6 +100,7 @@ /* Pretty flags */ #define PRETTYFLAG_PAREN 0x0001 #define PRETTYFLAG_INDENT 0x0002 +#define PRETTYFLAG_SCHEMA 0x0004 /* Default line length for pretty-print wrapping: 0 means wrap always */ #define WRAP_COLUMN_DEFAULT 0 @@ -107,6 +108,7 @@ /* macros to test if pretty action needed */ #define PRETTY_PAREN(context) ((context)->prettyFlags & PRETTYFLAG_PAREN) #define PRETTY_INDENT(context) ((context)->prettyFlags & PRETTYFLAG_INDENT) +#define PRETTY_SCHEMA(context) ((context)->prettyFlags & PRETTYFLAG_SCHEMA) /* ---------- @@ -424,6 +426,8 @@ static void get_from_clause_coldeflist(RangeTblFunction *rtfunc, deparse_context *context); static void get_tablesample_def(TableSampleClause *tablesample, deparse_context *context); +static char *pg_get_triggerdef_worker(Oid trigid, bool pretty); +static void set_simple_column_names(deparse_namespace *dpns); static void get_opclass_name(Oid opclass, Oid actual_datatype, StringInfo buf); static Node *processIndirection(Node *node, deparse_context *context); @@ -7481,6 +7485,305 @@ get_tablesample_def(TableSampleClause *tablesample, deparse_context *context) } } +char * +pg_get_triggerdef_command(Oid triggerId) +{ + Assert(OidIsValid(triggerId)); + + /* no need to have pretty SQL command */ + bool prettyOutput = false; + return pg_get_triggerdef_worker(triggerId, prettyOutput); +} + +static char * +pg_get_triggerdef_worker(Oid trigid, bool pretty) +{ + HeapTuple ht_trig; + Form_pg_trigger trigrec; + StringInfoData buf; + Relation tgrel; + ScanKeyData skey[1]; + SysScanDesc tgscan; + int findx = 0; + char *tgname; + char *tgoldtable; + char *tgnewtable; + Oid argtypes[1]; /* dummy */ + Datum value; + bool isnull; + + /* + * Fetch the pg_trigger tuple by the Oid of the trigger + */ + tgrel = heap_open(TriggerRelationId, AccessShareLock); + + ScanKeyInit(&skey[0], + ObjectIdAttributeNumber, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(trigid)); + + tgscan = systable_beginscan(tgrel, TriggerOidIndexId, true, + NULL, 1, skey); + + ht_trig = systable_getnext(tgscan); + + if (!HeapTupleIsValid(ht_trig)) + { + systable_endscan(tgscan); + heap_close(tgrel, AccessShareLock); + return NULL; + } + + trigrec = (Form_pg_trigger) GETSTRUCT(ht_trig); + + /* + * Start the trigger definition. Note that the trigger's name should never + * be schema-qualified, but the trigger rel's name may be. + */ + initStringInfo(&buf); + + tgname = NameStr(trigrec->tgname); + appendStringInfo(&buf, "CREATE %sTRIGGER %s ", + OidIsValid(trigrec->tgconstraint) ? "CONSTRAINT " : "", + quote_identifier(tgname)); + + if (TRIGGER_FOR_BEFORE(trigrec->tgtype)) + appendStringInfoString(&buf, "BEFORE"); + else if (TRIGGER_FOR_AFTER(trigrec->tgtype)) + appendStringInfoString(&buf, "AFTER"); + else if (TRIGGER_FOR_INSTEAD(trigrec->tgtype)) + appendStringInfoString(&buf, "INSTEAD OF"); + else + elog(ERROR, "unexpected tgtype value: %d", trigrec->tgtype); + + if (TRIGGER_FOR_INSERT(trigrec->tgtype)) + { + appendStringInfoString(&buf, " INSERT"); + findx++; + } + if (TRIGGER_FOR_DELETE(trigrec->tgtype)) + { + if (findx > 0) + appendStringInfoString(&buf, " OR DELETE"); + else + appendStringInfoString(&buf, " DELETE"); + findx++; + } + if (TRIGGER_FOR_UPDATE(trigrec->tgtype)) + { + if (findx > 0) + appendStringInfoString(&buf, " OR UPDATE"); + else + appendStringInfoString(&buf, " UPDATE"); + findx++; + /* tgattr is first var-width field, so OK to access directly */ + if (trigrec->tgattr.dim1 > 0) + { + int i; + + appendStringInfoString(&buf, " OF "); + for (i = 0; i < trigrec->tgattr.dim1; i++) + { + char *attname; + + if (i > 0) + appendStringInfoString(&buf, ", "); + attname = get_attname(trigrec->tgrelid, + trigrec->tgattr.values[i], false); + appendStringInfoString(&buf, quote_identifier(attname)); + } + } + } + if (TRIGGER_FOR_TRUNCATE(trigrec->tgtype)) + { + if (findx > 0) + appendStringInfoString(&buf, " OR TRUNCATE"); + else + appendStringInfoString(&buf, " TRUNCATE"); + findx++; + } + + /* + * In non-pretty mode, always schema-qualify the target table name for + * safety. In pretty mode, schema-qualify only if not visible. + */ + appendStringInfo(&buf, " ON %s ", + pretty ? + generate_relation_name(trigrec->tgrelid, NIL) : + generate_qualified_relation_name(trigrec->tgrelid)); + + if (OidIsValid(trigrec->tgconstraint)) + { + if (OidIsValid(trigrec->tgconstrrelid)) + appendStringInfo(&buf, "FROM %s ", + generate_relation_name(trigrec->tgconstrrelid, NIL)); + if (!trigrec->tgdeferrable) + appendStringInfoString(&buf, "NOT "); + appendStringInfoString(&buf, "DEFERRABLE INITIALLY "); + if (trigrec->tginitdeferred) + appendStringInfoString(&buf, "DEFERRED "); + else + appendStringInfoString(&buf, "IMMEDIATE "); + } + + value = fastgetattr(ht_trig, Anum_pg_trigger_tgoldtable, + tgrel->rd_att, &isnull); + if (!isnull) + tgoldtable = NameStr(*DatumGetName(value)); + else + tgoldtable = NULL; + value = fastgetattr(ht_trig, Anum_pg_trigger_tgnewtable, + tgrel->rd_att, &isnull); + if (!isnull) + tgnewtable = NameStr(*DatumGetName(value)); + else + tgnewtable = NULL; + if (tgoldtable != NULL || tgnewtable != NULL) + { + appendStringInfoString(&buf, "REFERENCING "); + if (tgoldtable != NULL) + appendStringInfo(&buf, "OLD TABLE AS %s ", + quote_identifier(tgoldtable)); + if (tgnewtable != NULL) + appendStringInfo(&buf, "NEW TABLE AS %s ", + quote_identifier(tgnewtable)); + } + + if (TRIGGER_FOR_ROW(trigrec->tgtype)) + appendStringInfoString(&buf, "FOR EACH ROW "); + else + appendStringInfoString(&buf, "FOR EACH STATEMENT "); + + /* If the trigger has a WHEN qualification, add that */ + value = fastgetattr(ht_trig, Anum_pg_trigger_tgqual, + tgrel->rd_att, &isnull); + if (!isnull) + { + Node *qual; + char relkind; + deparse_context context; + deparse_namespace dpns; + RangeTblEntry *oldrte; + RangeTblEntry *newrte; + + appendStringInfoString(&buf, "WHEN ("); + + qual = stringToNode(TextDatumGetCString(value)); + + relkind = get_rel_relkind(trigrec->tgrelid); + + /* Build minimal OLD and NEW RTEs for the rel */ + oldrte = makeNode(RangeTblEntry); + oldrte->rtekind = RTE_RELATION; + oldrte->relid = trigrec->tgrelid; + oldrte->relkind = relkind; + oldrte->alias = makeAlias("old", NIL); + oldrte->eref = oldrte->alias; + oldrte->lateral = false; + oldrte->inh = false; + oldrte->inFromCl = true; + + newrte = makeNode(RangeTblEntry); + newrte->rtekind = RTE_RELATION; + newrte->relid = trigrec->tgrelid; + newrte->relkind = relkind; + newrte->alias = makeAlias("new", NIL); + newrte->eref = newrte->alias; + newrte->lateral = false; + newrte->inh = false; + newrte->inFromCl = true; + + /* Build two-element rtable */ + memset(&dpns, 0, sizeof(dpns)); + dpns.rtable = list_make2(oldrte, newrte); + dpns.ctes = NIL; + set_rtable_names(&dpns, NIL, NULL); + set_simple_column_names(&dpns); + + /* Set up context with one-deep namespace stack */ + context.buf = &buf; + context.namespaces = list_make1(&dpns); + context.windowClause = NIL; + context.windowTList = NIL; + context.varprefix = true; + context.prettyFlags = pretty ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) : PRETTYFLAG_INDENT; + context.wrapColumn = WRAP_COLUMN_DEFAULT; + context.indentLevel = PRETTYINDENT_STD; + context.special_exprkind = EXPR_KIND_NONE; + + get_rule_expr(qual, &context, false); + + appendStringInfoString(&buf, ") "); + } + + appendStringInfo(&buf, "EXECUTE PROCEDURE %s(", + generate_function_name(trigrec->tgfoid, 0, + NIL, argtypes, + false, NULL, EXPR_KIND_NONE)); + + if (trigrec->tgnargs > 0) + { + char *p; + int i; + + value = fastgetattr(ht_trig, Anum_pg_trigger_tgargs, + tgrel->rd_att, &isnull); + if (isnull) + elog(ERROR, "tgargs is null for trigger %u", trigid); + p = (char *) VARDATA_ANY(DatumGetByteaPP(value)); + for (i = 0; i < trigrec->tgnargs; i++) + { + if (i > 0) + appendStringInfoString(&buf, ", "); + simple_quote_literal(&buf, p); + /* advance p to next string embedded in tgargs */ + while (*p) + p++; + p++; + } + } + + /* We deliberately do not put semi-colon at end */ + appendStringInfoChar(&buf, ')'); + + /* Clean up */ + systable_endscan(tgscan); + + heap_close(tgrel, AccessShareLock); + + return buf.data; +} + +/* + * set_simple_column_names: fill in column aliases for non-query situations + * + * This handles EXPLAIN and cases where we only have relation RTEs. Without + * a join tree, we can't do anything smart about join RTEs, but we don't + * need to (note that EXPLAIN should never see join alias Vars anyway). + * If we do hit a join RTE we'll just process it like a non-table base RTE. + */ +static void +set_simple_column_names(deparse_namespace *dpns) +{ + ListCell *lc; + ListCell *lc2; + + /* Initialize dpns->rtable_columns to contain zeroed structs */ + dpns->rtable_columns = NIL; + while (list_length(dpns->rtable_columns) < list_length(dpns->rtable)) + dpns->rtable_columns = lappend(dpns->rtable_columns, + palloc0(sizeof(deparse_columns))); + + /* Assign unique column aliases within each RTE */ + forboth(lc, dpns->rtable, lc2, dpns->rtable_columns) + { + RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); + deparse_columns *colinfo = (deparse_columns *) lfirst(lc2); + + set_relation_column_names(dpns, rte, colinfo); + } +} + /* * get_opclass_name - fetch name of an index operator class * diff --git a/src/backend/distributed/deparser/ruleutils_12.c b/src/backend/distributed/deparser/ruleutils_12.c index 2e2af5ff0..f1e680a42 100644 --- a/src/backend/distributed/deparser/ruleutils_12.c +++ b/src/backend/distributed/deparser/ruleutils_12.c @@ -100,6 +100,7 @@ /* Pretty flags */ #define PRETTYFLAG_PAREN 0x0001 #define PRETTYFLAG_INDENT 0x0002 +#define PRETTYFLAG_SCHEMA 0x0004 /* Default line length for pretty-print wrapping: 0 means wrap always */ #define WRAP_COLUMN_DEFAULT 0 @@ -107,6 +108,7 @@ /* macros to test if pretty action needed */ #define PRETTY_PAREN(context) ((context)->prettyFlags & PRETTYFLAG_PAREN) #define PRETTY_INDENT(context) ((context)->prettyFlags & PRETTYFLAG_INDENT) +#define PRETTY_SCHEMA(context) ((context)->prettyFlags & PRETTYFLAG_SCHEMA) /* ---------- @@ -424,6 +426,8 @@ static void get_from_clause_coldeflist(RangeTblFunction *rtfunc, deparse_context *context); static void get_tablesample_def(TableSampleClause *tablesample, deparse_context *context); +static char *pg_get_triggerdef_worker(Oid trigid, bool pretty); +static void set_simple_column_names(deparse_namespace *dpns); static void get_opclass_name(Oid opclass, Oid actual_datatype, StringInfo buf); static Node *processIndirection(Node *node, deparse_context *context); @@ -7481,6 +7485,307 @@ get_tablesample_def(TableSampleClause *tablesample, deparse_context *context) } } +char * +pg_get_triggerdef_command(Oid triggerId) +{ + Assert(OidIsValid(triggerId)); + + /* no need to have pretty SQL command */ + bool prettyOutput = false; + return pg_get_triggerdef_worker(triggerId, prettyOutput); +} + +static char * +pg_get_triggerdef_worker(Oid trigid, bool pretty) +{ + HeapTuple ht_trig; + Form_pg_trigger trigrec; + StringInfoData buf; + Relation tgrel; + ScanKeyData skey[1]; + SysScanDesc tgscan; + int findx = 0; + char *tgname; + char *tgoldtable; + char *tgnewtable; + Oid argtypes[1]; /* dummy */ + Datum value; + bool isnull; + + /* + * Fetch the pg_trigger tuple by the Oid of the trigger + */ + tgrel = table_open(TriggerRelationId, AccessShareLock); + + ScanKeyInit(&skey[0], + Anum_pg_trigger_oid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(trigid)); + + tgscan = systable_beginscan(tgrel, TriggerOidIndexId, true, + NULL, 1, skey); + + ht_trig = systable_getnext(tgscan); + + if (!HeapTupleIsValid(ht_trig)) + { + systable_endscan(tgscan); + table_close(tgrel, AccessShareLock); + return NULL; + } + + trigrec = (Form_pg_trigger) GETSTRUCT(ht_trig); + + /* + * Start the trigger definition. Note that the trigger's name should never + * be schema-qualified, but the trigger rel's name may be. + */ + initStringInfo(&buf); + + tgname = NameStr(trigrec->tgname); + appendStringInfo(&buf, "CREATE %sTRIGGER %s ", + OidIsValid(trigrec->tgconstraint) ? "CONSTRAINT " : "", + quote_identifier(tgname)); + + if (TRIGGER_FOR_BEFORE(trigrec->tgtype)) + appendStringInfoString(&buf, "BEFORE"); + else if (TRIGGER_FOR_AFTER(trigrec->tgtype)) + appendStringInfoString(&buf, "AFTER"); + else if (TRIGGER_FOR_INSTEAD(trigrec->tgtype)) + appendStringInfoString(&buf, "INSTEAD OF"); + else + elog(ERROR, "unexpected tgtype value: %d", trigrec->tgtype); + + if (TRIGGER_FOR_INSERT(trigrec->tgtype)) + { + appendStringInfoString(&buf, " INSERT"); + findx++; + } + if (TRIGGER_FOR_DELETE(trigrec->tgtype)) + { + if (findx > 0) + appendStringInfoString(&buf, " OR DELETE"); + else + appendStringInfoString(&buf, " DELETE"); + findx++; + } + if (TRIGGER_FOR_UPDATE(trigrec->tgtype)) + { + if (findx > 0) + appendStringInfoString(&buf, " OR UPDATE"); + else + appendStringInfoString(&buf, " UPDATE"); + findx++; + /* tgattr is first var-width field, so OK to access directly */ + if (trigrec->tgattr.dim1 > 0) + { + int i; + + appendStringInfoString(&buf, " OF "); + for (i = 0; i < trigrec->tgattr.dim1; i++) + { + char *attname; + + if (i > 0) + appendStringInfoString(&buf, ", "); + attname = get_attname(trigrec->tgrelid, + trigrec->tgattr.values[i], false); + appendStringInfoString(&buf, quote_identifier(attname)); + } + } + } + if (TRIGGER_FOR_TRUNCATE(trigrec->tgtype)) + { + if (findx > 0) + appendStringInfoString(&buf, " OR TRUNCATE"); + else + appendStringInfoString(&buf, " TRUNCATE"); + findx++; + } + + /* + * In non-pretty mode, always schema-qualify the target table name for + * safety. In pretty mode, schema-qualify only if not visible. + */ + appendStringInfo(&buf, " ON %s ", + pretty ? + generate_relation_name(trigrec->tgrelid, NIL) : + generate_qualified_relation_name(trigrec->tgrelid)); + + if (OidIsValid(trigrec->tgconstraint)) + { + if (OidIsValid(trigrec->tgconstrrelid)) + appendStringInfo(&buf, "FROM %s ", + generate_relation_name(trigrec->tgconstrrelid, NIL)); + if (!trigrec->tgdeferrable) + appendStringInfoString(&buf, "NOT "); + appendStringInfoString(&buf, "DEFERRABLE INITIALLY "); + if (trigrec->tginitdeferred) + appendStringInfoString(&buf, "DEFERRED "); + else + appendStringInfoString(&buf, "IMMEDIATE "); + } + + value = fastgetattr(ht_trig, Anum_pg_trigger_tgoldtable, + tgrel->rd_att, &isnull); + if (!isnull) + tgoldtable = NameStr(*DatumGetName(value)); + else + tgoldtable = NULL; + value = fastgetattr(ht_trig, Anum_pg_trigger_tgnewtable, + tgrel->rd_att, &isnull); + if (!isnull) + tgnewtable = NameStr(*DatumGetName(value)); + else + tgnewtable = NULL; + if (tgoldtable != NULL || tgnewtable != NULL) + { + appendStringInfoString(&buf, "REFERENCING "); + if (tgoldtable != NULL) + appendStringInfo(&buf, "OLD TABLE AS %s ", + quote_identifier(tgoldtable)); + if (tgnewtable != NULL) + appendStringInfo(&buf, "NEW TABLE AS %s ", + quote_identifier(tgnewtable)); + } + + if (TRIGGER_FOR_ROW(trigrec->tgtype)) + appendStringInfoString(&buf, "FOR EACH ROW "); + else + appendStringInfoString(&buf, "FOR EACH STATEMENT "); + + /* If the trigger has a WHEN qualification, add that */ + value = fastgetattr(ht_trig, Anum_pg_trigger_tgqual, + tgrel->rd_att, &isnull); + if (!isnull) + { + Node *qual; + char relkind; + deparse_context context; + deparse_namespace dpns; + RangeTblEntry *oldrte; + RangeTblEntry *newrte; + + appendStringInfoString(&buf, "WHEN ("); + + qual = stringToNode(TextDatumGetCString(value)); + + relkind = get_rel_relkind(trigrec->tgrelid); + + /* Build minimal OLD and NEW RTEs for the rel */ + oldrte = makeNode(RangeTblEntry); + oldrte->rtekind = RTE_RELATION; + oldrte->relid = trigrec->tgrelid; + oldrte->relkind = relkind; + oldrte->rellockmode = AccessShareLock; + oldrte->alias = makeAlias("old", NIL); + oldrte->eref = oldrte->alias; + oldrte->lateral = false; + oldrte->inh = false; + oldrte->inFromCl = true; + + newrte = makeNode(RangeTblEntry); + newrte->rtekind = RTE_RELATION; + newrte->relid = trigrec->tgrelid; + newrte->relkind = relkind; + newrte->rellockmode = AccessShareLock; + newrte->alias = makeAlias("new", NIL); + newrte->eref = newrte->alias; + newrte->lateral = false; + newrte->inh = false; + newrte->inFromCl = true; + + /* Build two-element rtable */ + memset(&dpns, 0, sizeof(dpns)); + dpns.rtable = list_make2(oldrte, newrte); + dpns.ctes = NIL; + set_rtable_names(&dpns, NIL, NULL); + set_simple_column_names(&dpns); + + /* Set up context with one-deep namespace stack */ + context.buf = &buf; + context.namespaces = list_make1(&dpns); + context.windowClause = NIL; + context.windowTList = NIL; + context.varprefix = true; + context.prettyFlags = pretty ? (PRETTYFLAG_PAREN | PRETTYFLAG_INDENT | PRETTYFLAG_SCHEMA) : PRETTYFLAG_INDENT; + context.wrapColumn = WRAP_COLUMN_DEFAULT; + context.indentLevel = PRETTYINDENT_STD; + context.special_exprkind = EXPR_KIND_NONE; + + get_rule_expr(qual, &context, false); + + appendStringInfoString(&buf, ") "); + } + + appendStringInfo(&buf, "EXECUTE FUNCTION %s(", + generate_function_name(trigrec->tgfoid, 0, + NIL, argtypes, + false, NULL, EXPR_KIND_NONE)); + + if (trigrec->tgnargs > 0) + { + char *p; + int i; + + value = fastgetattr(ht_trig, Anum_pg_trigger_tgargs, + tgrel->rd_att, &isnull); + if (isnull) + elog(ERROR, "tgargs is null for trigger %u", trigid); + p = (char *) VARDATA_ANY(DatumGetByteaPP(value)); + for (i = 0; i < trigrec->tgnargs; i++) + { + if (i > 0) + appendStringInfoString(&buf, ", "); + simple_quote_literal(&buf, p); + /* advance p to next string embedded in tgargs */ + while (*p) + p++; + p++; + } + } + + /* We deliberately do not put semi-colon at end */ + appendStringInfoChar(&buf, ')'); + + /* Clean up */ + systable_endscan(tgscan); + + table_close(tgrel, AccessShareLock); + + return buf.data; +} + +/* + * set_simple_column_names: fill in column aliases for non-query situations + * + * This handles EXPLAIN and cases where we only have relation RTEs. Without + * a join tree, we can't do anything smart about join RTEs, but we don't + * need to (note that EXPLAIN should never see join alias Vars anyway). + * If we do hit a join RTE we'll just process it like a non-table base RTE. + */ +static void +set_simple_column_names(deparse_namespace *dpns) +{ + ListCell *lc; + ListCell *lc2; + + /* Initialize dpns->rtable_columns to contain zeroed structs */ + dpns->rtable_columns = NIL; + while (list_length(dpns->rtable_columns) < list_length(dpns->rtable)) + dpns->rtable_columns = lappend(dpns->rtable_columns, + palloc0(sizeof(deparse_columns))); + + /* Assign unique column aliases within each RTE */ + forboth(lc, dpns->rtable, lc2, dpns->rtable_columns) + { + RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc); + deparse_columns *colinfo = (deparse_columns *) lfirst(lc2); + + set_relation_column_names(dpns, rte, colinfo); + } +} + /* * get_opclass_name - fetch name of an index operator class * diff --git a/src/backend/distributed/master/master_node_protocol.c b/src/backend/distributed/master/master_node_protocol.c index e4d0f0257..1d03aab34 100644 --- a/src/backend/distributed/master/master_node_protocol.c +++ b/src/backend/distributed/master/master_node_protocol.c @@ -522,7 +522,7 @@ ResolveRelationId(text *relationName, bool missingOk) * DEFAULT clauses for columns getting their default values from a sequence. * These DDL commands are all palloced; and include the table's schema * definition, optional column storage and statistics definitions, and index - * and constraint definitions. + * constraint and trigger definitions. */ List * GetTableDDLEvents(Oid relationId, bool includeSequenceDefaults) @@ -542,6 +542,9 @@ GetTableDDLEvents(Oid relationId, bool includeSequenceDefaults) List *policyCommands = CreatePolicyCommands(relationId); tableDDLEventList = list_concat(tableDDLEventList, policyCommands); + List *triggerCommands = GetExplicitTriggerCommandList(relationId); + tableDDLEventList = list_concat(tableDDLEventList, triggerCommands); + return tableDDLEventList; } diff --git a/src/include/distributed/citus_ruleutils.h b/src/include/distributed/citus_ruleutils.h index 4b99553ed..c1680f783 100644 --- a/src/include/distributed/citus_ruleutils.h +++ b/src/include/distributed/citus_ruleutils.h @@ -48,6 +48,7 @@ extern void pg_get_query_def(Query *query, StringInfo buffer); char * pg_get_rule_expr(Node *expression); extern void deparse_shard_query(Query *query, Oid distrelid, int64 shardid, StringInfo buffer); +extern char * pg_get_triggerdef_command(Oid triggerId); extern char * generate_relation_name(Oid relid, List *namespaces); extern char * generate_qualified_relation_name(Oid relid); extern char * generate_operator_name(Oid operid, Oid arg1, Oid arg2); diff --git a/src/include/distributed/commands.h b/src/include/distributed/commands.h index 86513e6b2..95ac1c094 100644 --- a/src/include/distributed/commands.h +++ b/src/include/distributed/commands.h @@ -44,6 +44,8 @@ typedef struct DistributeObjectOps ObjectAddress (*address)(Node *, bool); } DistributeObjectOps; +#define CITUS_TRUNCATE_TRIGGER_NAME "citus_truncate_trigger" + const DistributeObjectOps * GetDistributeObjectOps(Node *node); /* cluster.c - forward declarations */ @@ -274,6 +276,12 @@ extern ObjectWithArgs * ObjectWithArgsFromOid(Oid funcOid); /* vacuum.c - forward declarations */ extern void PostprocessVacuumStmt(VacuumStmt *vacuumStmt, const char *vacuumCommand); +/* trigger.c - forward declarations */ +extern List * GetExplicitTriggerCommandList(Oid relationId); +extern List * GetExplicitTriggerIdList(Oid relationId); +extern Oid get_relation_trigger_oid_compat(HeapTuple heapTuple); +extern void ErrorIfUnsupportedCreateTriggerCommand(CreateTrigStmt *createTriggerStmt); + extern bool ShouldPropagateSetCommand(VariableSetStmt *setStmt); extern void PostprocessVariableSetStmt(VariableSetStmt *setStmt, const char *setCommand); diff --git a/src/test/regress/expected/create_table_triggers.out b/src/test/regress/expected/create_table_triggers.out new file mode 100644 index 000000000..b2eda9363 --- /dev/null +++ b/src/test/regress/expected/create_table_triggers.out @@ -0,0 +1,101 @@ +-- This test file includes tests to show that we do not allow triggers +-- on citus tables. Note that in other regression tests, we already test +-- the successfull citus table creation cases. +\set VERBOSITY terse +SET citus.next_shard_id TO 1505000; +CREATE SCHEMA table_triggers_schema; +SET search_path TO table_triggers_schema; +--------------------------------------------------------------------- +-- show that we do not allow trigger creation on citus tables +--------------------------------------------------------------------- +-- create a simple function to be invoked by triggers +CREATE FUNCTION update_value() RETURNS trigger AS $update_value$ +BEGIN + NEW.value := value+1 ; + RETURN NEW; +END; +$update_value$ LANGUAGE plpgsql; +CREATE TABLE distributed_table (value int); +SELECT create_distributed_table('distributed_table', 'value'); + create_distributed_table +--------------------------------------------------------------------- + +(1 row) + +CREATE TABLE reference_table (value int); +SELECT create_reference_table('reference_table'); + create_reference_table +--------------------------------------------------------------------- + +(1 row) + +-- below two should fail +CREATE TRIGGER update_value_dist +AFTER INSERT ON distributed_table +FOR EACH ROW EXECUTE PROCEDURE update_value(); +ERROR: cannot create trigger on relation "distributed_table" because it is either a distributed table or a reference table +CREATE TRIGGER update_value_ref +AFTER INSERT ON reference_table +FOR EACH ROW EXECUTE PROCEDURE update_value(); +ERROR: cannot create trigger on relation "reference_table" because it is either a distributed table or a reference table +--------------------------------------------------------------------- +-- show that we do not allow creating citus tables if the +-- table has already triggers +--------------------------------------------------------------------- +CREATE TABLE distributed_table_1 (value int); +CREATE TRIGGER update_value_dist +AFTER INSERT ON distributed_table_1 +FOR EACH ROW EXECUTE PROCEDURE update_value(); +CREATE TABLE reference_table_1 (value int); +CREATE TRIGGER update_value_ref +AFTER INSERT ON reference_table_1 +FOR EACH ROW EXECUTE PROCEDURE update_value(); +-- below two should fail +SELECT create_distributed_table('distributed_table_1', 'value'); +ERROR: cannot distribute relation "distributed_table_1" because it has triggers +SELECT create_reference_table('reference_table_1'); +ERROR: cannot distribute relation "reference_table_1" because it has triggers +--------------------------------------------------------------------- +-- test deparse logic for CREATE TRIGGER commands +-- via master_get_table_ddl_events +--------------------------------------------------------------------- +CREATE TABLE test_table ( + id int, + text_number text, + text_col text +); +CREATE FUNCTION test_table_trigger_function() RETURNS trigger AS $test_table_trigger_function$ +BEGIN + RAISE EXCEPTION 'a meaningless exception'; +END; +$test_table_trigger_function$ LANGUAGE plpgsql; +-- in below two, use constraint triggers to test DEFERRABLE | NOT DEFERRABLE syntax +CREATE CONSTRAINT TRIGGER test_table_update + AFTER UPDATE OF id ON test_table + NOT DEFERRABLE + FOR EACH ROW + WHEN (OLD.* IS NOT DISTINCT FROM NEW.* AND OLD.text_number IS NOT NULL) + EXECUTE FUNCTION test_table_trigger_function(); +CREATE CONSTRAINT TRIGGER test_table_insert + AFTER INSERT ON test_table + DEFERRABLE INITIALLY IMMEDIATE + FOR EACH ROW + WHEN (NEW.id > 5 OR NEW.text_col IS NOT NULL AND NEW.id < to_number(NEW.text_number, '9999')) + EXECUTE FUNCTION test_table_trigger_function(); +CREATE TRIGGER test_table_delete + AFTER DELETE ON test_table + FOR EACH STATEMENT + EXECUTE FUNCTION test_table_trigger_function(); +SELECT master_get_table_ddl_events('test_table'); + master_get_table_ddl_events +--------------------------------------------------------------------- + CREATE TABLE table_triggers_schema.test_table (id integer, text_number text, text_col text) + ALTER TABLE table_triggers_schema.test_table OWNER TO postgres + CREATE TRIGGER test_table_delete AFTER DELETE ON table_triggers_schema.test_table FOR EACH STATEMENT EXECUTE FUNCTION table_triggers_schema.test_table_trigger_function() + CREATE CONSTRAINT TRIGGER test_table_insert AFTER INSERT ON table_triggers_schema.test_table DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW WHEN (((new.id OPERATOR(pg_catalog.>) 5) OR ((new.text_col IS NOT NULL) AND ((new.id)::numeric OPERATOR(pg_catalog.<) to_number(new.text_number, '9999'::text))))) EXECUTE FUNCTION table_triggers_schema.test_table_trigger_function() + CREATE CONSTRAINT TRIGGER test_table_update AFTER UPDATE OF id ON table_triggers_schema.test_table NOT DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW WHEN (((NOT (old.* IS DISTINCT FROM new.*)) AND (old.text_number IS NOT NULL))) EXECUTE FUNCTION table_triggers_schema.test_table_trigger_function() +(5 rows) + +-- cleanup at exit +DROP SCHEMA table_triggers_schema CASCADE; +NOTICE: drop cascades to 7 other objects diff --git a/src/test/regress/expected/create_table_triggers_0.out b/src/test/regress/expected/create_table_triggers_0.out new file mode 100644 index 000000000..780bb6b4b --- /dev/null +++ b/src/test/regress/expected/create_table_triggers_0.out @@ -0,0 +1,101 @@ +-- This test file includes tests to show that we do not allow triggers +-- on citus tables. Note that in other regression tests, we already test +-- the successfull citus table creation cases. +\set VERBOSITY terse +SET citus.next_shard_id TO 1505000; +CREATE SCHEMA table_triggers_schema; +SET search_path TO table_triggers_schema; +--------------------------------------------------------------------- +-- show that we do not allow trigger creation on citus tables +--------------------------------------------------------------------- +-- create a simple function to be invoked by triggers +CREATE FUNCTION update_value() RETURNS trigger AS $update_value$ +BEGIN + NEW.value := value+1 ; + RETURN NEW; +END; +$update_value$ LANGUAGE plpgsql; +CREATE TABLE distributed_table (value int); +SELECT create_distributed_table('distributed_table', 'value'); + create_distributed_table +--------------------------------------------------------------------- + +(1 row) + +CREATE TABLE reference_table (value int); +SELECT create_reference_table('reference_table'); + create_reference_table +--------------------------------------------------------------------- + +(1 row) + +-- below two should fail +CREATE TRIGGER update_value_dist +AFTER INSERT ON distributed_table +FOR EACH ROW EXECUTE PROCEDURE update_value(); +ERROR: cannot create trigger on relation "distributed_table" because it is either a distributed table or a reference table +CREATE TRIGGER update_value_ref +AFTER INSERT ON reference_table +FOR EACH ROW EXECUTE PROCEDURE update_value(); +ERROR: cannot create trigger on relation "reference_table" because it is either a distributed table or a reference table +--------------------------------------------------------------------- +-- show that we do not allow creating citus tables if the +-- table has already triggers +--------------------------------------------------------------------- +CREATE TABLE distributed_table_1 (value int); +CREATE TRIGGER update_value_dist +AFTER INSERT ON distributed_table_1 +FOR EACH ROW EXECUTE PROCEDURE update_value(); +CREATE TABLE reference_table_1 (value int); +CREATE TRIGGER update_value_ref +AFTER INSERT ON reference_table_1 +FOR EACH ROW EXECUTE PROCEDURE update_value(); +-- below two should fail +SELECT create_distributed_table('distributed_table_1', 'value'); +ERROR: cannot distribute relation "distributed_table_1" because it has triggers +SELECT create_reference_table('reference_table_1'); +ERROR: cannot distribute relation "reference_table_1" because it has triggers +--------------------------------------------------------------------- +-- test deparse logic for CREATE TRIGGER commands +-- via master_get_table_ddl_events +--------------------------------------------------------------------- +CREATE TABLE test_table ( + id int, + text_number text, + text_col text +); +CREATE FUNCTION test_table_trigger_function() RETURNS trigger AS $test_table_trigger_function$ +BEGIN + RAISE EXCEPTION 'a meaningless exception'; +END; +$test_table_trigger_function$ LANGUAGE plpgsql; +-- in below two, use constraint triggers to test DEFERRABLE | NOT DEFERRABLE syntax +CREATE CONSTRAINT TRIGGER test_table_update + AFTER UPDATE OF id ON test_table + NOT DEFERRABLE + FOR EACH ROW + WHEN (OLD.* IS NOT DISTINCT FROM NEW.* AND OLD.text_number IS NOT NULL) + EXECUTE FUNCTION test_table_trigger_function(); +CREATE CONSTRAINT TRIGGER test_table_insert + AFTER INSERT ON test_table + DEFERRABLE INITIALLY IMMEDIATE + FOR EACH ROW + WHEN (NEW.id > 5 OR NEW.text_col IS NOT NULL AND NEW.id < to_number(NEW.text_number, '9999')) + EXECUTE FUNCTION test_table_trigger_function(); +CREATE TRIGGER test_table_delete + AFTER DELETE ON test_table + FOR EACH STATEMENT + EXECUTE FUNCTION test_table_trigger_function(); +SELECT master_get_table_ddl_events('test_table'); + master_get_table_ddl_events +--------------------------------------------------------------------- + CREATE TABLE table_triggers_schema.test_table (id integer, text_number text, text_col text) + ALTER TABLE table_triggers_schema.test_table OWNER TO postgres + CREATE TRIGGER test_table_delete AFTER DELETE ON table_triggers_schema.test_table FOR EACH STATEMENT EXECUTE PROCEDURE table_triggers_schema.test_table_trigger_function() + CREATE CONSTRAINT TRIGGER test_table_insert AFTER INSERT ON table_triggers_schema.test_table DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW WHEN (((new.id OPERATOR(pg_catalog.>) 5) OR ((new.text_col IS NOT NULL) AND ((new.id)::numeric OPERATOR(pg_catalog.<) to_number(new.text_number, '9999'::text))))) EXECUTE PROCEDURE table_triggers_schema.test_table_trigger_function() + CREATE CONSTRAINT TRIGGER test_table_update AFTER UPDATE OF id ON table_triggers_schema.test_table NOT DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW WHEN (((NOT (old.* IS DISTINCT FROM new.*)) AND (old.text_number IS NOT NULL))) EXECUTE PROCEDURE table_triggers_schema.test_table_trigger_function() +(5 rows) + +-- cleanup at exit +DROP SCHEMA table_triggers_schema CASCADE; +NOTICE: drop cascades to 7 other objects diff --git a/src/test/regress/multi_schedule b/src/test/regress/multi_schedule index 1d6ce622e..0b327b394 100644 --- a/src/test/regress/multi_schedule +++ b/src/test/regress/multi_schedule @@ -45,7 +45,7 @@ test: multi_create_table_constraints multi_master_protocol multi_load_data multi test: multi_behavioral_analytics_basics multi_behavioral_analytics_single_shard_queries multi_insert_select_non_pushable_queries multi_insert_select multi_behavioral_analytics_create_table_superuser test: multi_shard_update_delete recursive_dml_with_different_planners_executors test: insert_select_repartition window_functions dml_recursive multi_insert_select_window -test: multi_insert_select_conflict +test: multi_insert_select_conflict create_table_triggers test: multi_row_insert # following should not run in parallel because it relies on connection counts to workers diff --git a/src/test/regress/sql/create_table_triggers.sql b/src/test/regress/sql/create_table_triggers.sql new file mode 100644 index 000000000..8b5964fa1 --- /dev/null +++ b/src/test/regress/sql/create_table_triggers.sql @@ -0,0 +1,100 @@ +-- This test file includes tests to show that we do not allow triggers +-- on citus tables. Note that in other regression tests, we already test +-- the successfull citus table creation cases. + +\set VERBOSITY terse + +SET citus.next_shard_id TO 1505000; + +CREATE SCHEMA table_triggers_schema; +SET search_path TO table_triggers_schema; + +------------------------------------------------------------- +-- show that we do not allow trigger creation on citus tables +------------------------------------------------------------- + +-- create a simple function to be invoked by triggers +CREATE FUNCTION update_value() RETURNS trigger AS $update_value$ +BEGIN + NEW.value := value+1 ; + RETURN NEW; +END; +$update_value$ LANGUAGE plpgsql; + +CREATE TABLE distributed_table (value int); +SELECT create_distributed_table('distributed_table', 'value'); + +CREATE TABLE reference_table (value int); +SELECT create_reference_table('reference_table'); + +-- below two should fail +CREATE TRIGGER update_value_dist +AFTER INSERT ON distributed_table +FOR EACH ROW EXECUTE PROCEDURE update_value(); + +CREATE TRIGGER update_value_ref +AFTER INSERT ON reference_table +FOR EACH ROW EXECUTE PROCEDURE update_value(); + +--------------------------------------------------------- +-- show that we do not allow creating citus tables if the +-- table has already triggers +--------------------------------------------------------- + +CREATE TABLE distributed_table_1 (value int); + +CREATE TRIGGER update_value_dist +AFTER INSERT ON distributed_table_1 +FOR EACH ROW EXECUTE PROCEDURE update_value(); + +CREATE TABLE reference_table_1 (value int); + +CREATE TRIGGER update_value_ref +AFTER INSERT ON reference_table_1 +FOR EACH ROW EXECUTE PROCEDURE update_value(); + +-- below two should fail +SELECT create_distributed_table('distributed_table_1', 'value'); +SELECT create_reference_table('reference_table_1'); + +------------------------------------------------- +-- test deparse logic for CREATE TRIGGER commands +-- via master_get_table_ddl_events +------------------------------------------------- + +CREATE TABLE test_table ( + id int, + text_number text, + text_col text +); + +CREATE FUNCTION test_table_trigger_function() RETURNS trigger AS $test_table_trigger_function$ +BEGIN + RAISE EXCEPTION 'a meaningless exception'; +END; +$test_table_trigger_function$ LANGUAGE plpgsql; + +-- in below two, use constraint triggers to test DEFERRABLE | NOT DEFERRABLE syntax +CREATE CONSTRAINT TRIGGER test_table_update + AFTER UPDATE OF id ON test_table + NOT DEFERRABLE + FOR EACH ROW + WHEN (OLD.* IS NOT DISTINCT FROM NEW.* AND OLD.text_number IS NOT NULL) + EXECUTE FUNCTION test_table_trigger_function(); + +CREATE CONSTRAINT TRIGGER test_table_insert + AFTER INSERT ON test_table + DEFERRABLE INITIALLY IMMEDIATE + FOR EACH ROW + WHEN (NEW.id > 5 OR NEW.text_col IS NOT NULL AND NEW.id < to_number(NEW.text_number, '9999')) + EXECUTE FUNCTION test_table_trigger_function(); + +CREATE TRIGGER test_table_delete + AFTER DELETE ON test_table + FOR EACH STATEMENT + EXECUTE FUNCTION test_table_trigger_function(); + +SELECT master_get_table_ddl_events('test_table'); + +-- cleanup at exit +DROP SCHEMA table_triggers_schema CASCADE;