From 4b727366b7fbc395201e74fddccbf106bfac8fe6 Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <pj@illuminatedcomputing.com>
Date: Fri, 29 Dec 2023 14:36:27 -0800
Subject: [PATCH v28 4/9] Add GiST referencedagg support func

This is the thirteenth support function and lets us implement temporal
foreign keys without hardcoding the range_agg function. The support
function should be an aggregate function that takes a type matching the
foreign key's PERIOD element and returns a type that can appear on the
righthand side of a ContainedBy operator, so that for example we can say
`fkperiod <@ range_agg(pkperiod)`.
---
 doc/src/sgml/gist.sgml                 | 114 ++++++++++++++++++++++++-
 doc/src/sgml/xindex.sgml               |   8 +-
 src/backend/access/gist/gistvalidate.c |   7 +-
 src/backend/access/index/amvalidate.c  |  24 +++++-
 src/include/access/amvalidate.h        |   1 +
 src/include/access/gist.h              |   3 +-
 src/include/catalog/pg_amproc.dat      |   6 ++
 7 files changed, 156 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/gist.sgml b/doc/src/sgml/gist.sgml
index 8a19f156d83..e67dd4b859f 100644
--- a/doc/src/sgml/gist.sgml
+++ b/doc/src/sgml/gist.sgml
@@ -272,7 +272,7 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
 
  <para>
    There are five methods that an index operator class for
-   <acronym>GiST</acronym> must provide, and seven that are optional.
+   <acronym>GiST</acronym> must provide, and eight that are optional.
    Correctness of the index is ensured
    by proper implementation of the <function>same</function>, <function>consistent</function>
    and <function>union</function> methods, while efficiency (size and speed) of the
@@ -300,6 +300,10 @@ CREATE INDEX ON my_table USING GIST (my_inet_column inet_ops);
    <filename>src/include/access/stratnum.h</filename>) into strategy numbers
    used by the operator class.  This lets the core code look up operators for
    temporal constraint indexes.
+   The optional thirteenth method <function>referencedagg</function> is used by
+   temporal foreign keys to combined referenced rows with the same
+   non-<literal>WITHOUT OVERLAPS</literal> value(s) into one
+   <literal>WITHOUT OVERLAPS</literal> span to compare against referencing rows.
  </para>
 
  <variablelist>
@@ -1244,6 +1248,114 @@ my_stratnum(PG_FUNCTION_ARGS)
       </para>
      </listitem>
     </varlistentry>
+    <varlistentry>
+     <term><function>referencedagg</function></term>
+     <listitem>
+      <para>
+       An aggregate function. Given values of this opclass,
+       it returns a value combining them all. The return value
+       need not be the same type as the input, but it must be a
+       type that can appear on the right hand side of the "contained by"
+       operator. For example the built-in <literal>range_ops</literal>
+       opclass uses <literal>range_agg</literal> here, so that foreign
+       keys can check <literal>fkperiod @> range_agg(pkperiod)</literal>.
+      </para>
+      <para>
+       This is used for temporal foreign key constraints.
+       If you omit this support function, your type cannot be used
+       as the <literal>PERIOD</literal> part of a foreign key.
+      </para>
+
+      <para>
+       The <acronym>SQL</acronym> declaration of the function must look like
+       this (using <literal>my_range_agg</literal> as an example):
+
+<programlisting>
+CREATE OR REPLACE FUNCTION my_range_agg_transfn(internal, anyrange)
+RETURNS internal
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE OR REPLACE FUNCTION my_range_agg_finalfn(internal, anyrange)
+RETURNS anymultirange
+AS 'MODULE_PATHNAME'
+LANGUAGE C STRICT;
+
+CREATE AGGREGATE my_range_agg(anyrange) {
+  SFUNC = my_range_agg_transfn,
+  STYPE = internal,
+  FINALFUNC = my_range_agg_finalfn
+};
+</programlisting>
+      </para>
+
+       <para>
+        The matching code in the C module could then follow this example:
+
+<programlisting>
+Datum
+my_range_agg_transfn(PG_FUNCTION_ARGS)
+{
+    MemoryContext    aggContext;
+    Oid              rngtypoid;
+    ArrayBuildState *state;
+
+    if (!AggCheckCallContext(fcinfo, &amp;aggContext))
+        elog(ERROR, "range_agg_transfn called in non-aggregate context");
+
+    rngtypoid = get_fn_expr_argtype(fcinfo-&gt;flinfo, 1);
+    if (!type_is_range(rngtypoid))
+        elog(ERROR, "range_agg must be called with a range");
+
+    if (PG_ARGISNULL(0))
+        state = initArrayResult(rngtypoid, aggContext, false);
+    else
+        state = (ArrayBuildState *) PG_GETARG_POINTER(0);
+
+    /* skip NULLs */
+    if (!PG_ARGISNULL(1))
+        accumArrayResult(state, PG_GETARG_DATUM(1), false, rngtypoid, aggContext);
+
+    PG_RETURN_POINTER(state);
+}
+
+Datum
+my_range_agg_finalfn(PG_FUNCTION_ARGS)
+{
+    MemoryContext    aggContext;
+    Oid              mltrngtypoid;
+    TypeCacheEntry  *typcache;
+    ArrayBuildState *state;
+    int32            range_count;
+    RangeType      **ranges;
+    int              i;
+
+    if (!AggCheckCallContext(fcinfo, &amp;aggContext))
+        elog(ERROR, "range_agg_finalfn called in non-aggregate context");
+
+    state = PG_ARGISNULL(0) ? NULL : (ArrayBuildState *) PG_GETARG_POINTER(0);
+    if (state == NULL)
+        /* This shouldn't be possible, but just in case.... */
+        PG_RETURN_NULL();
+
+    /* Also return NULL if we had zero inputs, like other aggregates */
+    range_count = state-&gt;nelems;
+    if (range_count == 0)
+        PG_RETURN_NULL();
+
+    mltrngtypoid = get_fn_expr_rettype(fcinfo-&gt;flinfo);
+    typcache = multirange_get_typcache(fcinfo, mltrngtypoid);
+
+    ranges = palloc0(range_count * sizeof(RangeType *));
+    for (i = 0; i &lt; range_count; i++)
+        ranges[i] = DatumGetRangeTypeP(state-&gt;dvalues[i]);
+
+    PG_RETURN_MULTIRANGE_P(make_multirange(mltrngtypoid, typcache-&gt;rngtype, range_count, ranges));
+}
+</programlisting>
+      </para>
+     </listitem>
+    </varlistentry>
   </variablelist>
 
   <para>
diff --git a/doc/src/sgml/xindex.sgml b/doc/src/sgml/xindex.sgml
index 3a19dab15e0..93df136eba3 100644
--- a/doc/src/sgml/xindex.sgml
+++ b/doc/src/sgml/xindex.sgml
@@ -508,7 +508,7 @@
    </table>
 
   <para>
-   GiST indexes have twelve support functions, seven of which are optional,
+   GiST indexes have thirteen support functions, eight of which are optional,
    as shown in <xref linkend="xindex-gist-support-table"/>.
    (For more information see <xref linkend="gist"/>.)
   </para>
@@ -596,6 +596,12 @@
         used by the operator class (optional)</entry>
        <entry>12</entry>
       </row>
+      <row>
+       <entry><function>referencedagg</function></entry>
+       <entry>aggregates referenced rows' <literal>WITHOUT OVERLAPS</literal>
+        part</entry>
+       <entry>13</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/access/gist/gistvalidate.c b/src/backend/access/gist/gistvalidate.c
index 0901543a60a..96467567eaa 100644
--- a/src/backend/access/gist/gistvalidate.c
+++ b/src/backend/access/gist/gistvalidate.c
@@ -150,6 +150,11 @@ gistvalidate(Oid opclassoid)
 				ok = check_amproc_signature(procform->amproc, INT2OID, true,
 											1, 1, INT2OID);
 				break;
+			case GIST_REFERENCED_AGG_PROC:
+				ok = check_amproc_signature(procform->amproc, InvalidOid, false,
+											   1, 1, opcintype)
+					&& check_amproc_is_aggregate(procform->amproc);
+				break;
 			default:
 				ereport(INFO,
 						(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
@@ -271,7 +276,7 @@ gistvalidate(Oid opclassoid)
 		if (i == GIST_DISTANCE_PROC || i == GIST_FETCH_PROC ||
 			i == GIST_COMPRESS_PROC || i == GIST_DECOMPRESS_PROC ||
 			i == GIST_OPTIONS_PROC || i == GIST_SORTSUPPORT_PROC ||
-			i == GIST_STRATNUM_PROC)
+			i == GIST_STRATNUM_PROC || i == GIST_REFERENCED_AGG_PROC)
 			continue;			/* optional methods */
 		ereport(INFO,
 				(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
diff --git a/src/backend/access/index/amvalidate.c b/src/backend/access/index/amvalidate.c
index 32bb477f328..9eb1b172ae1 100644
--- a/src/backend/access/index/amvalidate.c
+++ b/src/backend/access/index/amvalidate.c
@@ -140,13 +140,30 @@ identify_opfamily_groups(CatCList *oprlist, CatCList *proclist)
 	return result;
 }
 
+bool
+check_amproc_is_aggregate(Oid funcid)
+{
+	bool		result;
+	HeapTuple	tp;
+	Form_pg_proc procform;
+
+	tp = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
+	if (!HeapTupleIsValid(tp))
+		elog(ERROR, "cache lookup failed for function %u", funcid);
+	procform = (Form_pg_proc) GETSTRUCT(tp);
+	result = procform->prokind == 'a';
+	ReleaseSysCache(tp);
+	return result;
+}
+
 /*
  * Validate the signature (argument and result types) of an opclass support
  * function.  Return true if OK, false if not.
  *
  * The "..." represents maxargs argument-type OIDs.  If "exact" is true, they
  * must match the function arg types exactly, else only binary-coercibly.
- * In any case the function result type must match restype exactly.
+ * In any case the function result type must match restype exactly,
+ * unless it is InvalidOid.
  */
 bool
 check_amproc_signature(Oid funcid, Oid restype, bool exact,
@@ -163,8 +180,9 @@ check_amproc_signature(Oid funcid, Oid restype, bool exact,
 		elog(ERROR, "cache lookup failed for function %u", funcid);
 	procform = (Form_pg_proc) GETSTRUCT(tp);
 
-	if (procform->prorettype != restype || procform->proretset ||
-		procform->pronargs < minargs || procform->pronargs > maxargs)
+	if ((procform->prorettype != restype && OidIsValid(restype))
+		|| procform->proretset || procform->pronargs < minargs
+		|| procform->pronargs > maxargs)
 		result = false;
 
 	va_start(ap, maxargs);
diff --git a/src/include/access/amvalidate.h b/src/include/access/amvalidate.h
index 424ab63fa5a..c795a4bc1bf 100644
--- a/src/include/access/amvalidate.h
+++ b/src/include/access/amvalidate.h
@@ -28,6 +28,7 @@ typedef struct OpFamilyOpFuncGroup
 
 /* Functions in access/index/amvalidate.c */
 extern List *identify_opfamily_groups(CatCList *oprlist, CatCList *proclist);
+extern bool check_amproc_is_aggregate(Oid funcid);
 extern bool check_amproc_signature(Oid funcid, Oid restype, bool exact,
 								   int minargs, int maxargs,...);
 extern bool check_amoptsproc_signature(Oid funcid);
diff --git a/src/include/access/gist.h b/src/include/access/gist.h
index 22dd04c1418..641677e191c 100644
--- a/src/include/access/gist.h
+++ b/src/include/access/gist.h
@@ -40,7 +40,8 @@
 #define GIST_OPTIONS_PROC				10
 #define GIST_SORTSUPPORT_PROC			11
 #define GIST_STRATNUM_PROC				12
-#define GISTNProcs					12
+#define GIST_REFERENCED_AGG_PROC		13
+#define GISTNProcs					13
 
 /*
  * Page opaque data in a GiST index page.
diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 352558c1f06..1d3d5fcf4d8 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -610,6 +610,9 @@
 { amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
   amprocrighttype => 'anyrange', amprocnum => '12',
   amproc => 'gist_stratnum_identity' },
+{ amprocfamily => 'gist/range_ops', amproclefttype => 'anyrange',
+  amprocrighttype => 'anyrange', amprocnum => '13',
+  amproc => 'range_agg(anyrange)' },
 { amprocfamily => 'gist/network_ops', amproclefttype => 'inet',
   amprocrighttype => 'inet', amprocnum => '1',
   amproc => 'inet_gist_consistent' },
@@ -650,6 +653,9 @@
 { amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
   amprocrighttype => 'anymultirange', amprocnum => '12',
   amproc => 'gist_stratnum_identity' },
+{ amprocfamily => 'gist/multirange_ops', amproclefttype => 'anymultirange',
+  amprocrighttype => 'anymultirange', amprocnum => '13',
+  amproc => 'range_agg(anymultirange)' },
 
 # gin
 { amprocfamily => 'gin/array_ops', amproclefttype => 'anyarray',
-- 
2.42.0

