From 8ac7bf10036d762003702e2c2f84df45051e82f6 Mon Sep 17 00:00:00 2001
From: "David E. Wheeler" <david@justatheory.com>
Date: Mon, 19 Feb 2024 21:55:33 -0500
Subject: [PATCH v6] Add to_regtypemod() SQL function

The `to_regtypemod()` function uses the underlying `parseTypeString()` C
function to parse a string representing a data type into a type ID
(which id discards) and a typmod, which it returns. This value is
suitable for passing  as the second argument to `format_type()`, which
allow one to derive the formal SQL typmod from a string:

    SELECT format_type(
      to_regtype('varchar(32)'),
      to_regtypemod('varchar(32)')
    );
          format_type
    -----------------------
     character varying(32)

This function also resolves types whose typmod is determined by the SQL
parser or some step after that, such as interval types where the stored
field option is encoded in the typmod:

    SELECT format_type(
      to_regtype('interval second(0)'),
      to_regtypemod('interval second(0)')
    );
        format_type
    --------------------
    interval second(0)

Useful for unit tests for against column data types, for example. Based
on code originally written by Erik Wienhold for use in pgTAP.
---
 doc/src/sgml/func.sgml                | 29 +++++++++++-
 src/backend/catalog/genbki.pl         |  2 +-
 src/backend/utils/adt/regproc.c       | 30 ++++++++++++
 src/include/catalog/pg_proc.dat       |  3 ++
 src/test/regress/expected/regproc.out | 66 +++++++++++++++++++++++++++
 src/test/regress/sql/regproc.sql      | 14 ++++++
 6 files changed, 141 insertions(+), 3 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index cf3de80394..57cfbe19f5 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -24764,7 +24764,7 @@ SELECT pg_type_is_visible('myschema.widget'::regtype);
 
      <tbody>
       <row>
-       <entry role="func_table_entry"><para role="func_signature">
+       <entry id="format_type" role="func_table_entry"><para role="func_signature">
         <indexterm>
          <primary>format_type</primary>
         </indexterm>
@@ -25462,7 +25462,7 @@ SELECT collation for ('foo' COLLATE "de_DE");
       </row>
 
       <row>
-       <entry role="func_table_entry"><para role="func_signature">
+       <entry id="to_regtype" role="func_table_entry"><para role="func_signature">
         <indexterm>
          <primary>to_regtype</primary>
         </indexterm>
@@ -25477,6 +25477,31 @@ SELECT collation for ('foo' COLLATE "de_DE");
         not found.
        </para></entry>
       </row>
+
+      <row>
+       <entry id="to_regtypemod" role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>to_regtypemod</primary>
+        </indexterm>
+        <function>to_regtypemod</function> ( <parameter>type</parameter> <type>text</type> )
+        <returnvalue>integer</returnvalue>
+       </para>
+       <para>
+        Parses a string representing an SQL data type, optionally schema-qualified, and
+        returns its typmod, modifier for the type. Complements <xref linkend="to_regtype"/>,
+        and can be passed to <xref linkend="format_type" />.
+       </para>
+       <para>
+        For example:
+<programlisting>
+SELECT format_type(to_regtype('varchar(32)'), to_regtypemod('varchar(32)'));
+      format_type
+-----------------------
+ character varying(32)
+</programlisting>
+       </para></entry>
+      </row>
+
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index 94afdc5491..a0c299f067 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -1098,7 +1098,7 @@ sub lookup_oids
 			{
 				warn sprintf
 				  "unresolved OID reference \"%s\" in %s.dat field %s line %s\n",
-				  $lookupname, $catname, $attname, $bki_values->{line_number};
+				  $lookupname || '', $catname || '', $attname || '', $bki_values->{line_number} || '';
 				$num_errors++;
 			}
 		}
diff --git a/src/backend/utils/adt/regproc.c b/src/backend/utils/adt/regproc.c
index 1e3bf3f5fd..0eb24ffe57 100644
--- a/src/backend/utils/adt/regproc.c
+++ b/src/backend/utils/adt/regproc.c
@@ -30,6 +30,7 @@
 #include "catalog/pg_ts_config.h"
 #include "catalog/pg_ts_dict.h"
 #include "catalog/pg_type.h"
+#include "funcapi.h"
 #include "lib/stringinfo.h"
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
@@ -1220,6 +1221,35 @@ to_regtype(PG_FUNCTION_ARGS)
 	PG_RETURN_DATUM(result);
 }
 
+
+/*
+ * to_regtypemod() complements to_regtype, returning the typmod for the type,
+ * if any.
+ *
+ * If the type name is not found, we return NULL.
+ *
+ * Internally it relies on the Postgres core parseTypeString() function defined
+ * in src/backend/parser/parse_type.c.
+ */
+Datum
+to_regtypemod(PG_FUNCTION_ARGS)
+{
+	char	   *typ_name = text_to_cstring(PG_GETARG_TEXT_PP(0));
+	Oid         typid;
+	int32       typmod;
+	ErrorSaveContext escontext = {T_ErrorSaveContext};
+
+	/*
+	 * Parse type-name argument to obtain the encoded typmod. Return NULL
+	 * on failure.
+	 */
+	if (!parseTypeString(typ_name, &typid, &typmod, (Node *) &escontext)) {
+		PG_RETURN_NULL();
+	}
+
+	PG_RETURN_INT32(typmod);
+}
+
 /*
  * regtypeout		- converts type OID to "typ_name"
  */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 9c120fc2b7..2e1dd92ffd 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7162,6 +7162,9 @@
 { oid => '3493', descr => 'convert type name to regtype',
   proname => 'to_regtype', provolatile => 's', prorettype => 'regtype',
   proargtypes => 'text', prosrc => 'to_regtype' },
+{ oid => '8401', descr => 'convert type name to type mod',
+  proname => 'to_regtypemod', provolatile => 's', prorettype => 'int4',
+  proargtypes => 'text', prosrc => 'to_regtypemod' },
 { oid => '1079', descr => 'convert text to regclass',
   proname => 'regclass', provolatile => 's', prorettype => 'regclass',
   proargtypes => 'text', prosrc => 'text_regclass' },
diff --git a/src/test/regress/expected/regproc.out b/src/test/regress/expected/regproc.out
index a9420850b8..2f99d22cc9 100644
--- a/src/test/regress/expected/regproc.out
+++ b/src/test/regress/expected/regproc.out
@@ -544,3 +544,69 @@ SELECT * FROM pg_input_error_info('way.too.many.names', 'regtype');
 ERROR:  improper qualified name (too many dotted names): way.too.many.names
 SELECT * FROM pg_input_error_info('no_such_catalog.schema.name', 'regtype');
 ERROR:  cross-database references are not implemented: no_such_catalog.schema.name
+-- Test to_regtypemod
+SELECT to_regtypemod('text');
+ to_regtypemod 
+---------------
+            -1
+(1 row)
+
+SELECT to_regtypemod('timestamp(4)');
+ to_regtypemod 
+---------------
+             4
+(1 row)
+
+SELECT to_regtypemod('interval(0)');
+ to_regtypemod 
+---------------
+    2147418112
+(1 row)
+
+SELECT to_regtypemod('interval second(0)');
+ to_regtypemod 
+---------------
+     268435456
+(1 row)
+
+SELECT to_regtypemod('timestamptz');
+ to_regtypemod 
+---------------
+            -1
+(1 row)
+
+SELECT to_regtypemod('timestamptz(6)');
+ to_regtypemod 
+---------------
+             6
+(1 row)
+
+SELECT to_regtypemod('varchar');
+ to_regtypemod 
+---------------
+            -1
+(1 row)
+
+SELECT to_regtypemod('varchar(128)');
+ to_regtypemod 
+---------------
+           132
+(1 row)
+
+SELECT to_regtypemod(NULL); -- null expected
+ to_regtypemod 
+---------------
+              
+(1 row)
+
+SELECT to_regtypemod('interval nonesuch'); -- grammar error expected
+ERROR:  syntax error at or near "nonesuch"
+LINE 1: SELECT to_regtypemod('interval nonesuch');
+                 ^
+CONTEXT:  invalid type name "interval nonesuch"
+SELECT to_regtypemod('year(4)'); -- grammar error expected
+ to_regtypemod 
+---------------
+              
+(1 row)
+
diff --git a/src/test/regress/sql/regproc.sql b/src/test/regress/sql/regproc.sql
index de2aa881a8..bf83d77fab 100644
--- a/src/test/regress/sql/regproc.sql
+++ b/src/test/regress/sql/regproc.sql
@@ -145,3 +145,17 @@ SELECT * FROM pg_input_error_info('incorrect type name syntax', 'regtype');
 SELECT * FROM pg_input_error_info('numeric(1,2,3)', 'regtype');  -- bogus typmod
 SELECT * FROM pg_input_error_info('way.too.many.names', 'regtype');
 SELECT * FROM pg_input_error_info('no_such_catalog.schema.name', 'regtype');
+
+-- Test to_regtypemod
+SELECT to_regtypemod('text');
+SELECT to_regtypemod('timestamp(4)');
+SELECT to_regtypemod('interval(0)');
+SELECT to_regtypemod('interval second(0)');
+SELECT to_regtypemod('timestamptz');
+SELECT to_regtypemod('timestamptz(6)');
+SELECT to_regtypemod('varchar');
+SELECT to_regtypemod('varchar(128)');
+
+SELECT to_regtypemod(NULL); -- null expected
+SELECT to_regtypemod('interval nonesuch'); -- grammar error expected
+SELECT to_regtypemod('year(4)'); -- grammar error expected
-- 
2.43.2

