I noticed this with the old style implicit shell type creation:
postgres=# create function blah(text, text, text) returns bogus_type[]
immutable strict language internal as 'int8out';
NOTICE: type "bogus_type[]" is not yet defined
DETAIL: Creating a shell type definition.
CREATE FUNCTION
Surely we shouldn't create a shell type when the array syntax was used?
This path is for old scripts that created the I/O functions before the
type definition, and you wouldn't specify I/O functions for an array
like this.
If you then repeat the command, after the shell type's been created, you
unsurprisingly get this:
postgres=# create function blah(text, text, text) returns bogus_type[]
strict language internal as 'int8out';
NOTICE: type "bogus_type[]" is not yet defined
DETAIL: Creating a shell type definition.
ERROR: duplicate key value violates unique constraint
"pg_type_typname_nsp_index"
DETAIL: Key (typname, typnamespace)=(bogus_type, 2200) already exists.
I propose the attached to fix that. It also changes the error message
you get if you specify a typmod and the type doesn't exist. Currently on
master:
postgres=# create function blah(text, text, text) returns
bogus_type(100) strict language internal as 'int8out';
ERROR: type modifier cannot be specified for shell type "bogus_type"
And with the patch:
postgres=# create function blah(text, text, text) returns
bogus_type(100) strict language internal as 'int8out';
ERROR: type "bogus_type" does not exist
I think "type does not exist" is better, it's unlikely that the user
really intended to create a shell type.
We could narrow down further the criteria for shell type creation. It's
really only needed for creating the input function, so we could check
that the signature looks like an input function. For example, the above
'blah' function takes text args, so it's surely not an input function
and could be rejected on those grounds. But I didn't include that in
this patch yet.
- Heikki
P.S. I know the 'int8out' internal function is wouldn't work for the
definitions in the above examples anyway, please ignore thatFrom 72e5c00df12ae637fe68f54de402f45476dbdaab Mon Sep 17 00:00:00 2001
From: Heikki Linnakangas <[email protected]>
Date: Tue, 18 Aug 2026 22:32:39 +0300
Subject: [PATCH 1/1] Don't create a shell type for function returning an array
Refactor the checks in the function to move all the conditions for
when to attempt creating a shell type into one place. Add a check for
the array syntax.
In addition to rejecting array syntax, another user-visible effect is
that the error message is now different if the type specified a
typmod. You now get "type does not exist" instead of the more
specific "type modifier cannot be specified for shell type". That
seems better; the implicit shell type creation exists only for
backwards compatibility, and it never worked with type modifiers, so
if there's a type modifier it's most likely not because the user tried
to create a shell type,
Add test for the array syntax, the type modifier, and some other cases
for which we don't create shell types.
---
src/backend/commands/functioncmds.c | 58 ++++++++++++++---------
src/test/regress/expected/create_type.out | 50 ++++++++++++++++++-
src/test/regress/sql/create_type.sql | 42 ++++++++++++++++
3 files changed, 126 insertions(+), 24 deletions(-)
diff --git a/src/backend/commands/functioncmds.c b/src/backend/commands/functioncmds.c
index ba24a708982..4d46013d7de 100644
--- a/src/backend/commands/functioncmds.c
+++ b/src/backend/commands/functioncmds.c
@@ -92,11 +92,32 @@ compute_return_type(TypeName *returnType, Oid languageOid,
Oid rettype;
Type typtup;
AclResult aclresult;
+ bool attempt_shell_creation;
- typtup = LookupTypeName(NULL, returnType, NULL, false);
+ /*
+ * If this looks like it could be an input function, and the type doesn't
+ * exist, we'll create it as a shell type.
+ *
+ * If the type name contains any modifiers like %TYPE, type[] array
+ * syntax, or typmod decoration, it's not an input function, or at least
+ * not one for which we'd want to automatically create a shell type.
+ *
+ * Only C-coded functions can be I/O functions. We enforce this
+ * restriction here mainly to prevent littering the catalogs with shell
+ * types due to simple typos in user-defined function definitions.
+ */
+ attempt_shell_creation =
+ !returnType->pct_type && returnType->arrayBounds == NULL &&
+ returnType->typmods == NIL &&
+ (languageOid == INTERNALlanguageId || languageOid == ClanguageId);
+ typtup = LookupTypeName(NULL, returnType, NULL, false);
if (typtup)
{
+ /*
+ * Found an existing type with the given name. Check if it's a shell
+ * type.
+ */
if (!((Form_pg_type) GETSTRUCT(typtup))->typisdefined)
{
if (languageOid == SQLlanguageId)
@@ -113,37 +134,27 @@ compute_return_type(TypeName *returnType, Oid languageOid,
rettype = typeTypeId(typtup);
ReleaseSysCache(typtup);
}
+ else if (!attempt_shell_creation)
+ {
+ /* Type not found and we don't want to create a shell type */
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("type \"%s\" does not exist",
+ TypeNameToString(returnType))));
+ }
else
{
- char *typnam = TypeNameToString(returnType);
+ /* Make a shell type */
Oid namespaceId;
char *typname;
ObjectAddress address;
- /*
- * Only C-coded functions can be I/O functions. We enforce this
- * restriction here mainly to prevent littering the catalogs with
- * shell types due to simple typos in user-defined function
- * definitions.
- */
- if (languageOid != INTERNALlanguageId &&
- languageOid != ClanguageId)
- ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("type \"%s\" does not exist", typnam)));
-
- /* Reject if there's typmod decoration, too */
- if (returnType->typmods != NIL)
- ereport(ERROR,
- (errcode(ERRCODE_SYNTAX_ERROR),
- errmsg("type modifier cannot be specified for shell type \"%s\"",
- typnam)));
-
- /* Otherwise, go ahead and make a shell type */
ereport(NOTICE,
(errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("type \"%s\" is not yet defined", typnam),
+ errmsg("type \"%s\" is not yet defined",
+ TypeNameToString(returnType)),
errdetail("Creating a shell type definition.")));
+
namespaceId = QualifiedNameGetCreationNamespace(returnType->names,
&typname);
aclresult = object_aclcheck(NamespaceRelationId, namespaceId, GetUserId(),
@@ -151,6 +162,7 @@ compute_return_type(TypeName *returnType, Oid languageOid,
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, OBJECT_SCHEMA,
get_namespace_name(namespaceId));
+
address = TypeShellMake(typname, namespaceId, GetUserId());
rettype = address.objectId;
Assert(OidIsValid(rettype));
diff --git a/src/test/regress/expected/create_type.out b/src/test/regress/expected/create_type.out
index 5181c4290b4..09cfa8da22c 100644
--- a/src/test/regress/expected/create_type.out
+++ b/src/test/regress/expected/create_type.out
@@ -51,6 +51,30 @@ CREATE TYPE city_budget (
category = 'x', -- just to verify the system will take it
preferred = true -- ditto
);
+-- If the specified type includes typmods or array syntax, don't create a shell type
+CREATE FUNCTION bogus_in(cstring)
+ RETURNS bogus_shell(123)
+ AS :'regresslib', 'widget_in'
+ LANGUAGE C STRICT IMMUTABLE;
+ERROR: type "bogus_shell" does not exist
+CREATE FUNCTION bogus_in(cstring)
+ RETURNS bogus_shell[]
+ AS :'regresslib', 'widget_in'
+ LANGUAGE C STRICT IMMUTABLE;
+ERROR: type "bogus_shell[]" does not exist
+-- If the column specified with %TYPE does not exist, don't try to create a shell type
+CREATE TEMP TABLE bogus_tbl (col int);
+CREATE FUNCTION bogus_in(cstring)
+ RETURNS bogus_tbl.nonexistent_col%TYPE
+ AS :'regresslib', 'widget_in'
+ LANGUAGE C STRICT IMMUTABLE;
+ERROR: column "nonexistent_col" of relation "bogus_tbl" does not exist
+-- If the schema does not exist, don't try to create a shell type
+CREATE FUNCTION bogus_in(cstring)
+ RETURNS nonexistent_schema.bogus_shell
+ AS :'regresslib', 'widget_in'
+ LANGUAGE C STRICT IMMUTABLE;
+ERROR: schema "nonexistent_schema" does not exist
-- Test creation and destruction of shell types
CREATE TYPE shell;
CREATE TYPE shell; -- fail, type already present
@@ -343,12 +367,30 @@ NOTICE: return type myvarchar is only a shell
-- fail, it's still a shell:
ALTER TYPE myvarchar SET (storage = extended);
ERROR: type "myvarchar" is only a shell
+-- fail: typmods not allowed for a shell type
+CREATE FUNCTION myvarchar_lower(text) RETURNS myvarchar(100)
+LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT AS 'lower';
+ERROR: type modifier cannot be specified for shell type "myvarchar"
+CREATE FUNCTION myvarchar_lower(myvarchar(100)) RETURNS text
+LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT AS 'lower';
+ERROR: type modifier cannot be specified for shell type "myvarchar"
+LINE 1: CREATE FUNCTION myvarchar_lower(myvarchar(100)) RETURNS text
+ ^
CREATE TYPE myvarchar (
input = myvarcharin,
output = myvarcharout,
alignment = integer,
storage = main
);
+-- fail: typmods not allowed because 'typmod_in' / 'typmod_out' were not specified.
+CREATE FUNCTION myvarchar_lower(text) RETURNS myvarchar(100)
+LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT AS 'lower';
+ERROR: type modifier is not allowed for type "myvarchar"
+CREATE FUNCTION myvarchar_lower(myvarchar(100)) RETURNS text
+LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT AS 'lower';
+ERROR: type modifier is not allowed for type "myvarchar"
+LINE 1: CREATE FUNCTION myvarchar_lower(myvarchar(100)) RETURNS text
+ ^
-- want to check updating of a domain over the target type, too
CREATE DOMAIN myvarchardom AS myvarchar;
ALTER TYPE myvarchar SET (storage = plain); -- not allowed
@@ -395,6 +437,9 @@ FROM pg_type WHERE typname = '_myvarchardom';
array_in | array_out | array_recv | array_send | - | - | array_typanalyze | array_subscript_handler | x
(1 row)
+-- typmods are now accepted in CREATE FUNCTION, although they are not stored
+CREATE FUNCTION myvarchar_lower(myvarchar(100)) RETURNS myvarchar(100)
+LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT AS 'lower';
-- ensure dependencies are straight
DROP FUNCTION myvarcharsend(myvarchar); -- fail
ERROR: cannot drop function myvarcharsend(myvarchar) because other objects depend on it
@@ -402,6 +447,7 @@ DETAIL: type myvarchar depends on function myvarcharsend(myvarchar)
function myvarcharin(cstring,oid,integer) depends on type myvarchar
function myvarcharout(myvarchar) depends on type myvarchar
function myvarcharrecv(internal,oid,integer) depends on type myvarchar
+function myvarchar_lower(myvarchar) depends on type myvarchar
type myvarchardom depends on function myvarcharsend(myvarchar)
HINT: Use DROP ... CASCADE to drop the dependent objects too.
DROP TYPE myvarchar; -- fail
@@ -411,11 +457,13 @@ function myvarcharout(myvarchar) depends on type myvarchar
function myvarcharsend(myvarchar) depends on type myvarchar
function myvarcharrecv(internal,oid,integer) depends on type myvarchar
type myvarchardom depends on type myvarchar
+function myvarchar_lower(myvarchar) depends on type myvarchar
HINT: Use DROP ... CASCADE to drop the dependent objects too.
DROP TYPE myvarchar CASCADE;
-NOTICE: drop cascades to 5 other objects
+NOTICE: drop cascades to 6 other objects
DETAIL: drop cascades to function myvarcharin(cstring,oid,integer)
drop cascades to function myvarcharout(myvarchar)
drop cascades to function myvarcharsend(myvarchar)
drop cascades to function myvarcharrecv(internal,oid,integer)
drop cascades to type myvarchardom
+drop cascades to function myvarchar_lower(myvarchar)
diff --git a/src/test/regress/sql/create_type.sql b/src/test/regress/sql/create_type.sql
index c25018029c2..4fd1facdffe 100644
--- a/src/test/regress/sql/create_type.sql
+++ b/src/test/regress/sql/create_type.sql
@@ -50,6 +50,32 @@ CREATE TYPE city_budget (
preferred = true -- ditto
);
+
+-- If the specified type includes typmods or array syntax, don't create a shell type
+CREATE FUNCTION bogus_in(cstring)
+ RETURNS bogus_shell(123)
+ AS :'regresslib', 'widget_in'
+ LANGUAGE C STRICT IMMUTABLE;
+
+CREATE FUNCTION bogus_in(cstring)
+ RETURNS bogus_shell[]
+ AS :'regresslib', 'widget_in'
+ LANGUAGE C STRICT IMMUTABLE;
+
+-- If the column specified with %TYPE does not exist, don't try to create a shell type
+CREATE TEMP TABLE bogus_tbl (col int);
+CREATE FUNCTION bogus_in(cstring)
+ RETURNS bogus_tbl.nonexistent_col%TYPE
+ AS :'regresslib', 'widget_in'
+ LANGUAGE C STRICT IMMUTABLE;
+
+-- If the schema does not exist, don't try to create a shell type
+CREATE FUNCTION bogus_in(cstring)
+ RETURNS nonexistent_schema.bogus_shell
+ AS :'regresslib', 'widget_in'
+ LANGUAGE C STRICT IMMUTABLE;
+
+
-- Test creation and destruction of shell types
CREATE TYPE shell;
CREATE TYPE shell; -- fail, type already present
@@ -252,6 +278,12 @@ LANGUAGE internal STABLE PARALLEL SAFE STRICT AS 'varcharrecv';
-- fail, it's still a shell:
ALTER TYPE myvarchar SET (storage = extended);
+-- fail: typmods not allowed for a shell type
+CREATE FUNCTION myvarchar_lower(text) RETURNS myvarchar(100)
+LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT AS 'lower';
+CREATE FUNCTION myvarchar_lower(myvarchar(100)) RETURNS text
+LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT AS 'lower';
+
CREATE TYPE myvarchar (
input = myvarcharin,
output = myvarcharout,
@@ -259,6 +291,12 @@ CREATE TYPE myvarchar (
storage = main
);
+-- fail: typmods not allowed because 'typmod_in' / 'typmod_out' were not specified.
+CREATE FUNCTION myvarchar_lower(text) RETURNS myvarchar(100)
+LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT AS 'lower';
+CREATE FUNCTION myvarchar_lower(myvarchar(100)) RETURNS text
+LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT AS 'lower';
+
-- want to check updating of a domain over the target type, too
CREATE DOMAIN myvarchardom AS myvarchar;
@@ -292,6 +330,10 @@ SELECT typinput, typoutput, typreceive, typsend, typmodin, typmodout,
typanalyze, typsubscript, typstorage
FROM pg_type WHERE typname = '_myvarchardom';
+-- typmods are now accepted in CREATE FUNCTION, although they are not stored
+CREATE FUNCTION myvarchar_lower(myvarchar(100)) RETURNS myvarchar(100)
+LANGUAGE internal IMMUTABLE PARALLEL SAFE STRICT AS 'lower';
+
-- ensure dependencies are straight
DROP FUNCTION myvarcharsend(myvarchar); -- fail
DROP TYPE myvarchar; -- fail
--
2.47.3