From 6bbab97146a8b4e0f05cde346b44d1c20b81c783 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
Date: Tue, 28 Jul 2026 21:49:43 -0700
Subject: [PATCH v1 5/5] Expose FOR PORTION OF to pltcl triggers

It is helpful for triggers to see what the FOR PORTION OF clause
specified: both the column name and the targeted bounds. We already pass
this information to plpgsql trigger functions; this commit does the same
for pltcl, using the new $TG_period_name and $TG_period_bounds variables.

These are also set when inserting temporal leftovers, so that triggers can
distinguish regular inserts from those automatic ones. Since an INSERT can't
use FOR PORTION OF itself, there is no ambiguity.

Author: Paul A. Jungwirth <pj@illuminatedcomputing.com>
---
 doc/src/sgml/pltcl.sgml               | 31 +++++++++
 src/pl/tcl/expected/pltcl_trigger.out | 90 +++++++++++++++++++++++++++
 src/pl/tcl/pltcl.c                    | 28 ++++++++-
 src/pl/tcl/sql/pltcl_trigger.sql      | 35 +++++++++++
 4 files changed, 183 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/pltcl.sgml b/doc/src/sgml/pltcl.sgml
index 9fd008a99d7..62dd04d589e 100644
--- a/doc/src/sgml/pltcl.sgml
+++ b/doc/src/sgml/pltcl.sgml
@@ -678,6 +678,37 @@ SELECT 'doesn''t' AS ret
        </listitem>
       </varlistentry>
 
+      <varlistentry>
+       <term><varname>$TG_period_name</varname></term>
+       <listitem>
+        <para>
+         The column name used in a <literal>FOR PORTION OF</literal> clause,
+         or else the empty string. Also set for the implicit
+         <command>INSERT</command> statements to add the
+         <glossterm linkend="glossary-temporal-leftovers">temporal
+         leftovers</glossterm>.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry>
+       <term><varname>$TG_period_bounds</varname></term>
+       <listitem>
+        <para>
+         The range/multirange given as the bounds of a
+         <literal>FOR PORTION OF</literal> clause, either directly (with
+         parens syntax) or computed from the <literal>FROM</literal> and
+         <literal>TO</literal> bounds. The empty string if
+         <literal>FOR PORTION OF</literal> was not used. Also set for the
+         implicit <command>INSERT</command> statements to add the
+         <glossterm linkend="glossary-temporal-leftovers">temporal
+         leftovers</glossterm>. This is a text value based on the type's
+         output function, since the type can't be known at function creation
+         time.
+        </para>
+       </listitem>
+      </varlistentry>
+
       <varlistentry>
        <term><varname>$NEW</varname></term>
        <listitem>
diff --git a/src/pl/tcl/expected/pltcl_trigger.out b/src/pl/tcl/expected/pltcl_trigger.out
index 5298e50a5ec..516e8f03340 100644
--- a/src/pl/tcl/expected/pltcl_trigger.out
+++ b/src/pl/tcl/expected/pltcl_trigger.out
@@ -625,6 +625,8 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: STATEMENT
 NOTICE:  TG_name: statement_trigger
 NOTICE:  TG_op: INSERT
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v {} test_skip test_return_null test_argisnull}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test
@@ -636,6 +638,8 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig
 NOTICE:  TG_op: INSERT
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v {} test_skip test_return_null test_argisnull}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test
@@ -648,6 +652,8 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_before
 NOTICE:  TG_op: INSERT
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
@@ -659,6 +665,8 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_after
 NOTICE:  TG_op: INSERT
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
@@ -671,6 +679,8 @@ NOTICE:  OLD: {i: 1, j: 2}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_before
 NOTICE:  TG_op: UPDATE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
@@ -682,6 +692,8 @@ NOTICE:  OLD: {i: 1, j: 2}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_after
 NOTICE:  TG_op: UPDATE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
@@ -694,6 +706,8 @@ NOTICE:  OLD: {i: 11, j: 22}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_before
 NOTICE:  TG_op: DELETE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
@@ -705,6 +719,8 @@ NOTICE:  OLD: {i: 11, j: 22}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig_after
 NOTICE:  TG_op: DELETE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i j k}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_generated
@@ -717,6 +733,8 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_view_trig
 NOTICE:  TG_op: INSERT
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_view
@@ -729,6 +747,8 @@ NOTICE:  OLD: {i: 1, v: insert}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_view_trig
 NOTICE:  TG_op: UPDATE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_view
@@ -741,6 +761,8 @@ NOTICE:  OLD: {i: 1, v: insert}
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_view_trig
 NOTICE:  TG_op: DELETE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test_view
@@ -753,6 +775,8 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: STATEMENT
 NOTICE:  TG_name: statement_trigger
 NOTICE:  TG_op: UPDATE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v {} test_skip test_return_null test_argisnull}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test
@@ -766,6 +790,8 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: STATEMENT
 NOTICE:  TG_name: statement_trigger
 NOTICE:  TG_op: UPDATE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v {} test_skip test_return_null test_argisnull}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test
@@ -777,6 +803,8 @@ NOTICE:  OLD: {i: 1, test_argisnull: f, test_return_null: f, test_skip: f, v: in
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig
 NOTICE:  TG_op: UPDATE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v {} test_skip test_return_null test_argisnull}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test
@@ -789,6 +817,8 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: STATEMENT
 NOTICE:  TG_name: statement_trigger
 NOTICE:  TG_op: DELETE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v {} test_skip test_return_null test_argisnull}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test
@@ -800,6 +830,8 @@ NOTICE:  OLD: {i: 1, test_argisnull: f, test_return_null: f, test_skip: f, v: up
 NOTICE:  TG_level: ROW
 NOTICE:  TG_name: show_trigger_data_trig
 NOTICE:  TG_op: DELETE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v {} test_skip test_return_null test_argisnull}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test
@@ -812,6 +844,8 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: STATEMENT
 NOTICE:  TG_name: statement_trigger
 NOTICE:  TG_op: TRUNCATE
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v {} test_skip test_return_null test_argisnull}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test
@@ -827,6 +861,8 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: STATEMENT
 NOTICE:  TG_name: statement_trigger
 NOTICE:  TG_op: INSERT
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v {} test_skip test_return_null test_argisnull}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test
@@ -841,6 +877,8 @@ NOTICE:  OLD: {}
 NOTICE:  TG_level: STATEMENT
 NOTICE:  TG_name: statement_trigger
 NOTICE:  TG_op: INSERT
+NOTICE:  TG_period_bounds: {}
+NOTICE:  TG_period_name: {}
 NOTICE:  TG_relatts: {{} i v {} test_skip test_return_null test_argisnull}
 NOTICE:  TG_relid: bogus:12345
 NOTICE:  TG_table_name: trigger_test
@@ -887,3 +925,55 @@ SELECT * FROM trigger_test_generated;
 ---+---+---
 (0 rows)
 
+-- test FOR PORTION OF
+CREATE TABLE temporal_trigger_test (
+    id int,
+    valid_at daterange,
+    v text
+);
+INSERT INTO temporal_trigger_test VALUES (1, '[2018-01-01,2020-01-01)', 'one');
+CREATE FUNCTION for_portion_of_trigger_func() RETURNS trigger
+LANGUAGE pltcl
+AS $$
+elog NOTICE "$TG_when $TG_op: TG_period_name = '$TG_period_name', TG_period_bounds = '$TG_period_bounds'"
+return OK
+$$;
+CREATE TRIGGER for_portion_of_trigger_trig
+  BEFORE INSERT OR UPDATE OR DELETE ON temporal_trigger_test
+  FOR EACH ROW EXECUTE PROCEDURE for_portion_of_trigger_func();
+UPDATE temporal_trigger_test
+  FOR PORTION OF valid_at FROM '2019-01-01' TO '2019-06-01'
+  SET v = 'updated';
+NOTICE:  BEFORE UPDATE: TG_period_name = 'valid_at', TG_period_bounds = '[01-01-2019,06-01-2019)'
+NOTICE:  BEFORE INSERT: TG_period_name = 'valid_at', TG_period_bounds = '[01-01-2019,06-01-2019)'
+NOTICE:  BEFORE INSERT: TG_period_name = 'valid_at', TG_period_bounds = '[01-01-2019,06-01-2019)'
+SELECT * FROM temporal_trigger_test ORDER BY valid_at;
+ id |        valid_at         |    v    
+----+-------------------------+---------
+  1 | [01-01-2018,01-01-2019) | one
+  1 | [01-01-2019,06-01-2019) | updated
+  1 | [06-01-2019,01-01-2020) | one
+(3 rows)
+
+DELETE FROM temporal_trigger_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-09-01';
+NOTICE:  BEFORE DELETE: TG_period_name = 'valid_at', TG_period_bounds = '[06-01-2018,09-01-2018)'
+NOTICE:  BEFORE INSERT: TG_period_name = 'valid_at', TG_period_bounds = '[06-01-2018,09-01-2018)'
+NOTICE:  BEFORE INSERT: TG_period_name = 'valid_at', TG_period_bounds = '[06-01-2018,09-01-2018)'
+SELECT * FROM temporal_trigger_test ORDER BY valid_at;
+ id |        valid_at         |    v    
+----+-------------------------+---------
+  1 | [01-01-2018,06-01-2018) | one
+  1 | [09-01-2018,01-01-2019) | one
+  1 | [01-01-2019,06-01-2019) | updated
+  1 | [06-01-2019,01-01-2020) | one
+(4 rows)
+
+-- no FOR PORTION OF, so the variables are empty
+UPDATE temporal_trigger_test SET v = 'all of it';
+NOTICE:  BEFORE UPDATE: TG_period_name = '', TG_period_bounds = ''
+NOTICE:  BEFORE UPDATE: TG_period_name = '', TG_period_bounds = ''
+NOTICE:  BEFORE UPDATE: TG_period_name = '', TG_period_bounds = ''
+NOTICE:  BEFORE UPDATE: TG_period_name = '', TG_period_bounds = ''
+DROP TABLE temporal_trigger_test;
+DROP FUNCTION for_portion_of_trigger_func();
diff --git a/src/pl/tcl/pltcl.c b/src/pl/tcl/pltcl.c
index e2c6d99a6de..aa6b2fcf32b 100644
--- a/src/pl/tcl/pltcl.c
+++ b/src/pl/tcl/pltcl.c
@@ -1249,6 +1249,32 @@ pltcl_trigger_handler(PG_FUNCTION_ARGS, pltcl_call_state *call_state,
 		else
 			elog(ERROR, "unrecognized LEVEL tg_event: %u", trigdata->tg_event);
 
+		/*
+		 * The FOR PORTION OF details for TG_period_name and
+		 * TG_period_bounds.  The bounds could be any range or multirange
+		 * type, so the best we can offer is their text representation.
+		 */
+		if (trigdata->tg_temporal)
+		{
+			ForPortionOfState *fpo = trigdata->tg_temporal;
+			Oid			funcid;
+			bool		varlena;
+
+			Tcl_ListObjAppendElement(NULL, tcl_cmd,
+									 Tcl_NewStringObj(utf_e2u(fpo->fp_rangeName), -1));
+
+			getTypeOutputInfo(fpo->fp_rangeType, &funcid, &varlena);
+			stroid = OidOutputFunctionCall(funcid, fpo->fp_targetRange);
+			Tcl_ListObjAppendElement(NULL, tcl_cmd,
+									 Tcl_NewStringObj(utf_e2u(stroid), -1));
+			pfree(stroid);
+		}
+		else
+		{
+			Tcl_ListObjAppendElement(NULL, tcl_cmd, Tcl_NewObj());
+			Tcl_ListObjAppendElement(NULL, tcl_cmd, Tcl_NewObj());
+		}
+
 		/* Finally append the arguments from CREATE TRIGGER */
 		for (i = 0; i < trigdata->tg_trigger->tgnargs; i++)
 			Tcl_ListObjAppendElement(NULL, tcl_cmd,
@@ -1703,7 +1729,7 @@ compile_pltcl_function(Oid fn_oid, Oid tgreloid,
 		{
 			/* trigger procedure has fixed args */
 			Tcl_DStringAppend(&proc_internal_args,
-							  "TG_name TG_relid TG_table_name TG_table_schema TG_relatts TG_when TG_level TG_op __PLTcl_Tup_NEW __PLTcl_Tup_OLD args",
+							  "TG_name TG_relid TG_table_name TG_table_schema TG_relatts TG_when TG_level TG_op __PLTcl_Tup_NEW __PLTcl_Tup_OLD TG_period_name TG_period_bounds args",
 							  -1);
 		}
 		else if (is_event_trigger)
diff --git a/src/pl/tcl/sql/pltcl_trigger.sql b/src/pl/tcl/sql/pltcl_trigger.sql
index 0ed00f49526..9152eaeb0f6 100644
--- a/src/pl/tcl/sql/pltcl_trigger.sql
+++ b/src/pl/tcl/sql/pltcl_trigger.sql
@@ -602,3 +602,38 @@ FOR EACH ROW EXECUTE PROCEDURE generated_test_func1();
 TRUNCATE trigger_test_generated;
 INSERT INTO trigger_test_generated (i) VALUES (1);
 SELECT * FROM trigger_test_generated;
+
+-- test FOR PORTION OF
+
+CREATE TABLE temporal_trigger_test (
+    id int,
+    valid_at daterange,
+    v text
+);
+INSERT INTO temporal_trigger_test VALUES (1, '[2018-01-01,2020-01-01)', 'one');
+
+CREATE FUNCTION for_portion_of_trigger_func() RETURNS trigger
+LANGUAGE pltcl
+AS $$
+elog NOTICE "$TG_when $TG_op: TG_period_name = '$TG_period_name', TG_period_bounds = '$TG_period_bounds'"
+return OK
+$$;
+
+CREATE TRIGGER for_portion_of_trigger_trig
+  BEFORE INSERT OR UPDATE OR DELETE ON temporal_trigger_test
+  FOR EACH ROW EXECUTE PROCEDURE for_portion_of_trigger_func();
+
+UPDATE temporal_trigger_test
+  FOR PORTION OF valid_at FROM '2019-01-01' TO '2019-06-01'
+  SET v = 'updated';
+SELECT * FROM temporal_trigger_test ORDER BY valid_at;
+
+DELETE FROM temporal_trigger_test
+  FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-09-01';
+SELECT * FROM temporal_trigger_test ORDER BY valid_at;
+
+-- no FOR PORTION OF, so the variables are empty
+UPDATE temporal_trigger_test SET v = 'all of it';
+
+DROP TABLE temporal_trigger_test;
+DROP FUNCTION for_portion_of_trigger_func();
-- 
2.47.3

