From a8b5a16d880995ac1982ac171408ef9e152f7c38 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
Date: Tue, 28 Jul 2026 21:48:18 -0700
Subject: [PATCH v1 3/5] Expose FOR PORTION OF to plperl 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 plperl, using the new $_TD->{period_name} and $_TD->{period_bounds}
elements.

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/plperl.sgml                  | 30 +++++++++++++
 src/pl/plperl/expected/plperl_trigger.out | 55 +++++++++++++++++++++++
 src/pl/plperl/plperl.c                    | 21 ++++++++-
 src/pl/plperl/sql/plperl_trigger.sql      | 38 ++++++++++++++++
 4 files changed, 143 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/plperl.sgml b/doc/src/sgml/plperl.sgml
index 6f018645f11..92b656c4d68 100644
--- a/doc/src/sgml/plperl.sgml
+++ b/doc/src/sgml/plperl.sgml
@@ -1290,6 +1290,36 @@ $$ LANGUAGE plperl;
      </listitem>
     </varlistentry>
 
+    <varlistentry>
+     <term><literal>$_TD-&gt;{period_name}</literal></term>
+     <listitem>
+      <para>
+       The column name used in a <literal>FOR PORTION OF</literal> clause,
+       or else <literal>undef</literal>. Also set for the implicit
+       <literal>INSERT</literal> statements to add the
+       <glossterm linkend="glossary-temporal-leftovers">temporal
+       leftovers</glossterm>.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>$_TD-&gt;{period_bounds}</literal></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. <literal>undef</literal> if
+       <literal>FOR PORTION OF</literal> was not used. Also set for the
+       implicit <literal>INSERT</literal> 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><literal>$_TD-&gt;{argc}</literal></term>
      <listitem>
diff --git a/src/pl/plperl/expected/plperl_trigger.out b/src/pl/plperl/expected/plperl_trigger.out
index 42c52ecbba8..0b02f2a027d 100644
--- a/src/pl/plperl/expected/plperl_trigger.out
+++ b/src/pl/plperl/expected/plperl_trigger.out
@@ -391,3 +391,58 @@ SELECT * FROM trigger_test_generated;
 ---+---+---
 (0 rows)
 
+-- test FOR PORTION OF
+CREATE TABLE trigger_test_temporal (
+    id int,
+    valid_at daterange,
+    v varchar
+);
+INSERT INTO trigger_test_temporal VALUES (1, '[2018-01-01,2020-01-01)', 'one');
+CREATE FUNCTION for_portion_of_trigger() RETURNS trigger LANGUAGE plperl AS $$
+    my $name = $_TD->{period_name};
+    my $bounds = $_TD->{period_bounds};
+    elog(NOTICE, "$_TD->{when} $_TD->{event}: period_name = "
+                 . (defined $name ? "'$name'" : 'undef')
+                 . ", period_bounds = "
+                 . (defined $bounds ? "'$bounds'" : 'undef'));
+    return;
+$$;
+CREATE TRIGGER for_portion_of_trig
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_temporal
+FOR EACH ROW EXECUTE PROCEDURE for_portion_of_trigger();
+UPDATE trigger_test_temporal
+    FOR PORTION OF valid_at FROM '2019-01-01' TO '2019-06-01'
+    SET v = 'updated';
+NOTICE:  BEFORE UPDATE: period_name = 'valid_at', period_bounds = '[01-01-2019,06-01-2019)'
+NOTICE:  BEFORE INSERT: period_name = 'valid_at', period_bounds = '[01-01-2019,06-01-2019)'
+NOTICE:  BEFORE INSERT: period_name = 'valid_at', period_bounds = '[01-01-2019,06-01-2019)'
+SELECT * FROM trigger_test_temporal 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 trigger_test_temporal
+    FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-09-01';
+NOTICE:  BEFORE DELETE: period_name = 'valid_at', period_bounds = '[06-01-2018,09-01-2018)'
+NOTICE:  BEFORE INSERT: period_name = 'valid_at', period_bounds = '[06-01-2018,09-01-2018)'
+NOTICE:  BEFORE INSERT: period_name = 'valid_at', period_bounds = '[06-01-2018,09-01-2018)'
+SELECT * FROM trigger_test_temporal 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 undef
+UPDATE trigger_test_temporal SET v = 'all of it';
+NOTICE:  BEFORE UPDATE: period_name = undef, period_bounds = undef
+NOTICE:  BEFORE UPDATE: period_name = undef, period_bounds = undef
+NOTICE:  BEFORE UPDATE: period_name = undef, period_bounds = undef
+NOTICE:  BEFORE UPDATE: period_name = undef, period_bounds = undef
+DROP TABLE trigger_test_temporal;
+DROP FUNCTION for_portion_of_trigger();
diff --git a/src/pl/plperl/plperl.c b/src/pl/plperl/plperl.c
index a59dbc94372..0d948936f9b 100644
--- a/src/pl/plperl/plperl.c
+++ b/src/pl/plperl/plperl.c
@@ -1629,7 +1629,7 @@ plperl_trigger_build_args(FunctionCallInfo fcinfo)
 	HV		   *hv;
 
 	hv = newHV();
-	hv_ksplit(hv, 12);			/* pre-grow the hash */
+	hv_ksplit(hv, 14);			/* pre-grow the hash */
 
 	tdata = (TriggerData *) fcinfo->context;
 	tupdesc = tdata->tg_relation->rd_att;
@@ -1705,6 +1705,25 @@ plperl_trigger_build_args(FunctionCallInfo fcinfo)
 	hv_store_string(hv, "table_schema",
 					cstr2sv(SPI_getnspname(tdata->tg_relation)));
 
+	/*
+	 * If the query used FOR PORTION OF, report the column name and the
+	 * targeted bounds.  The bounds could be any range or multirange type, so
+	 * the best we can offer is their text representation.
+	 */
+	if (tdata->tg_temporal)
+	{
+		ForPortionOfState *fpo = tdata->tg_temporal;
+		Oid			funcid;
+		bool		varlena;
+
+		hv_store_string(hv, "period_name", cstr2sv(fpo->fp_rangeName));
+
+		getTypeOutputInfo(fpo->fp_rangeType, &funcid, &varlena);
+		hv_store_string(hv, "period_bounds",
+						cstr2sv(OidOutputFunctionCall(funcid,
+													  fpo->fp_targetRange)));
+	}
+
 	if (TRIGGER_FIRED_BEFORE(tdata->tg_event))
 		when = "BEFORE";
 	else if (TRIGGER_FIRED_AFTER(tdata->tg_event))
diff --git a/src/pl/plperl/sql/plperl_trigger.sql b/src/pl/plperl/sql/plperl_trigger.sql
index 2798a02fa12..cf88658cc88 100644
--- a/src/pl/plperl/sql/plperl_trigger.sql
+++ b/src/pl/plperl/sql/plperl_trigger.sql
@@ -258,3 +258,41 @@ 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 trigger_test_temporal (
+    id int,
+    valid_at daterange,
+    v varchar
+);
+INSERT INTO trigger_test_temporal VALUES (1, '[2018-01-01,2020-01-01)', 'one');
+
+CREATE FUNCTION for_portion_of_trigger() RETURNS trigger LANGUAGE plperl AS $$
+    my $name = $_TD->{period_name};
+    my $bounds = $_TD->{period_bounds};
+    elog(NOTICE, "$_TD->{when} $_TD->{event}: period_name = "
+                 . (defined $name ? "'$name'" : 'undef')
+                 . ", period_bounds = "
+                 . (defined $bounds ? "'$bounds'" : 'undef'));
+    return;
+$$;
+
+CREATE TRIGGER for_portion_of_trig
+BEFORE INSERT OR UPDATE OR DELETE ON trigger_test_temporal
+FOR EACH ROW EXECUTE PROCEDURE for_portion_of_trigger();
+
+UPDATE trigger_test_temporal
+    FOR PORTION OF valid_at FROM '2019-01-01' TO '2019-06-01'
+    SET v = 'updated';
+SELECT * FROM trigger_test_temporal ORDER BY valid_at;
+
+DELETE FROM trigger_test_temporal
+    FOR PORTION OF valid_at FROM '2018-06-01' TO '2018-09-01';
+SELECT * FROM trigger_test_temporal ORDER BY valid_at;
+
+-- no FOR PORTION OF, so the variables are undef
+UPDATE trigger_test_temporal SET v = 'all of it';
+
+DROP TABLE trigger_test_temporal;
+DROP FUNCTION for_portion_of_trigger();
-- 
2.47.3

