From 8b918759fd6f2328ac06a17c69a0990e2748e834 Mon Sep 17 00:00:00 2001
From: "David E. Wheeler" <david@justatheory.com>
Date: Wed, 21 Feb 2024 10:12:08 -0500
Subject: [PATCH v8] 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.

Like `to_regtype()`, `to_regtypemod()` raises an error when the
`parseTypeString()` function drops into the SQL grammar parser raises
an error. So invalid type names like `inteval` or `interval nonesuch`
raise an error, rather than return `NULL`. Note this behavior in the
docs, and also update the documentation for `to_regtype()` to note
this behavior.

While at it, fix an "use of undefined value in sprintf" error in
`genbki.pl` that arose when I had specified `prorettype => 'integer'`
instead of `prorettype => 'int4'` in `pg_proc.dat`. I only realized the
invalid type name when I fixed the Perl bug.
---
 doc/src/sgml/func.sgml                | 40 +++++++++++++---
 src/backend/catalog/genbki.pl         |  2 +-
 src/backend/utils/adt/regproc.c       | 29 ++++++++++++
 src/include/catalog/pg_proc.dat       |  3 ++
 src/test/regress/expected/regproc.out | 66 +++++++++++++++++++++++++++
 src/test/regress/sql/regproc.sql      | 14 ++++++
 6 files changed, 146 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index e5fa82c161..4c3ffeb5e4 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -24763,7 +24763,7 @@ SELECT pg_type_is_visible('myschema.widget'::regtype);
 
      <tbody>
       <row>
-       <entry role="func_table_entry"><para role="func_signature">
+       <entry id="format_type" xreflabel="format_type" role="func_table_entry"><para role="func_signature">
         <indexterm>
          <primary>format_type</primary>
         </indexterm>
@@ -25461,7 +25461,7 @@ SELECT collation for ('foo' COLLATE "de_DE");
       </row>
 
       <row>
-       <entry role="func_table_entry"><para role="func_signature">
+       <entry id="to_regtype" xreflabel="to_regtype" role="func_table_entry"><para role="func_signature">
         <indexterm>
          <primary>to_regtype</primary>
         </indexterm>
@@ -25469,13 +25469,39 @@ SELECT collation for ('foo' COLLATE "de_DE");
         <returnvalue>regtype</returnvalue>
        </para>
        <para>
-        Translates a textual type name to its OID.  A similar result is
-        obtained by casting the string to type <type>regtype</type> (see
-        <xref linkend="datatype-oid"/>); however, this function will return
-        <literal>NULL</literal> rather than throwing an error if the name is
-        not found.
+        Parses a string of text, extracts a potential type name from it, and
+        translates that name into an OID. A similar result is obtained by
+        casting the string to type <type>regtype</type> (see
+        <xref linkend="datatype-oid"/>). Failure to extract a valid potential
+        type name results in an error; however, if the extracted name is not
+        known to the system, this function will return <literal>NULL</literal>.
+       </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 of text, extracts a potential type name from it, and
+        translates its type modifier, if any. Failure to extract a valid
+        potential type name results in an error; however, if the extracted name
+        is not known to the system, this function will return
+        <literal>NULL</literal>. Complements <xref linkend="to_regtype"/>, and
+        can be passed to <xref linkend="format_type" />. 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..75b8429e0d 100644
--- a/src/backend/utils/adt/regproc.c
+++ b/src/backend/utils/adt/regproc.c
@@ -1220,6 +1220,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..ff1ee5b52c 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);                -- returns null on null input
+ to_regtypemod 
+---------------
+              
+(1 row)
+
+SELECT to_regtypemod('year(4)');           -- error trapped, returns null
+ to_regtypemod 
+---------------
+              
+(1 row)
+
+SELECT to_regtypemod('interval nonesuch'); -- grammar error raised, not trapped
+ERROR:  syntax error at or near "nonesuch"
+LINE 1: SELECT to_regtypemod('interval nonesuch');
+                 ^
+CONTEXT:  invalid type name "interval nonesuch"
diff --git a/src/test/regress/sql/regproc.sql b/src/test/regress/sql/regproc.sql
index de2aa881a8..c1717e1f8f 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);                -- returns null on null input
+SELECT to_regtypemod('year(4)');           -- error trapped, returns null
+SELECT to_regtypemod('interval nonesuch'); -- grammar error raised, not trapped
-- 
2.43.2

