From 2a58ef7d7c8d3d7558e0f21941c4707bedb4eda3 Mon Sep 17 00:00:00 2001
From: "David E. Wheeler" <david@justatheory.com>
Date: Fri, 8 Mar 2024 20:40:43 -0500
Subject: [PATCH v11] 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 and cite examples.

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                | 54 +++++++++++++++++++---
 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, 160 insertions(+), 8 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 0bb7aeb40e..18908c8a48 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,53 @@ 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. For example:
+<programlisting>
+SELECT to_regtype('interval nonesuch');
+ERROR:  syntax error at or near "nonesuch"
+LINE 1: select to_regtype('interval nonesuch');
+                 ^
+CONTEXT:  invalid type name "interval nonesuch"
+</programlisting>
+        However, if the extracted name is not known to the system, this function
+        will return <literal>NULL</literal>. For example:
+<programlisting>
+SELECT to_regtype('party');
+ to_regtype
+------------
+
+</programlisting>
+       </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> ( <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 291ed876fc..48feafaad4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7155,6 +7155,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.44.0

