Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions postgres/parser/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2506,6 +2506,24 @@ func init() {
typNameLiterals[name] = t
}
}
for name, t := range sqlStandardTypeAliases {
if _, ok := typNameLiterals[name]; !ok {
typNameLiterals[name] = t
}
}
}

// typeNameWhitespace matches any run of whitespace within a type name.
var typeNameWhitespace = regexp.MustCompile(`\s+`)

// normalizeTypeName collapses each run of whitespace within the given type name to a single space, so
// that the multi-word SQL standard type names (`double precision`, `timestamp\n\twith time zone`, etc.)
// match their entries in typNameLiterals regardless of how they were spelled in the original statement.
func normalizeTypeName(name string) string {
if !strings.ContainsAny(name, " \t\n\r\f\v") {
return name
}
return typeNameWhitespace.ReplaceAllString(strings.TrimSpace(name), " ")
}

// TypeForNonKeywordTypeName returns the column type for the string name of a
Expand All @@ -2515,6 +2533,7 @@ func init() {
// -1 if the type is known in postgres.
// >0 for a github issue number.
func TypeForNonKeywordTypeName(name string) (*T, bool, int) {
name = normalizeTypeName(name)
t, ok := typNameLiterals[name]
if ok {
return t, ok, 0
Expand Down Expand Up @@ -2581,6 +2600,46 @@ var unreservedTypeTokens = map[string]*T{
"uuid": Uuid,
}

// sqlStandardTypeAliases contains the SQL standard type names that Postgres accepts as aliases for its own
// type names (https://www.postgresql.org/docs/15/datatype.html#DATATYPE-TABLE). The grammar has tokens for
// all of them, so this map serves only the callers that resolve a name arriving as a bare string rather than
// as parser input: a schema-qualified name such as `pg_catalog.boolean`, or a name from a PL/pgSQL datum
// list. Multi-word names are keyed with single spaces, which is what TypeForNonKeywordTypeName normalizes to.
//
// `char` is absent because `pg_catalog.char` names the internal one-byte "char" type that OidToType already
// registers, rather than the bpchar that a bare CHAR keyword names.
var sqlStandardTypeAliases = map[string]*T{
"bigint": Int,
"boolean": Bool,
"dec": Decimal,
"decimal": Decimal,
"int": Int4,
"real": Float4,
"smallint": Int2,

// FLOAT with no precision is FLOAT8; only FLOAT(1) through FLOAT(24) are FLOAT4, and the precision is applied
// by the caller rather than by this lookup.
"float": Float,
"double precision": Float,

"character": typeBpChar,
"national char": typeBpChar,
"national character": typeBpChar,
"nchar": typeBpChar,
"char varying": VarChar,
"character varying": VarChar,
"national char varying": VarChar,
"national character varying": VarChar,
"nchar varying": VarChar,

"bit varying": VarBit,

"time without time zone": Time,
"time with time zone": TimeTZ,
"timestamp without time zone": Timestamp,
"timestamp with time zone": TimestampTZ,
}

// The following map must include all types predefined in PostgreSQL
// that are also not yet defined in CockroachDB and link them to
// github issues. It is also possible, but not necessary, to include
Expand Down
20 changes: 12 additions & 8 deletions server/plpgsql/interpreter_logic.go

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View All Evidence

Medium severity Qualified type limits are lost

What failed: Calling the function with the qualified character type failed instead of preserving its length limit. The decimal case returned a value, but the qualified character varying(5) case reported that the full text was not a type.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Medium Medium severity
  • Impact: Functions that declare a schema-qualified character type with a length limit fail instead of running correctly. Users may need to avoid the qualified form, and the affected declaration cannot enforce its intended limit.
  • Steps to Reproduce:
    1. Create a PL/pgSQL function with a local variable declared as pg_catalog.decimal(8,2), pg_catalog.character varying(5), or pg_catalog.bit varying(4), and assign a value that fits the modifier.
    2. Call each function and inspect its returned value and type metadata.
    3. Compare the results with the declaration's precision, character length, or bit length; the qualified character varying(5) case fails with a type-does-not-exist error.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: server/plpgsql/statements.go:115-124 copies variable.Type into InterpreterOperation.PrimaryData without separating the base type from its modifier, so a declaration such as pg_catalog.character varying(5) reaches execution as one string. In server/plpgsql/interpreter_logic.go:129-160, the OpCode_Declare handler removes quotes, splits the schema from the remaining text, and calls types.TypeForNonKeywordTypeName(elementName) only to map an alias. That lookup receives the modifier-bearing text and cannot turn character varying(5) into the base alias character varying. The handler then calls typeCollection.GetType(ctx, id.NewType(schemaName, typeName)) at line 160 using the unchanged modifier-bearing name when alias lookup misses, producing the observed type-does-not-exist error. Even where a base alias is found, the code replaces typeName with arrayPrefix + typ.PGName() at lines 154-157, which has no typmod component, so the declaration's precision, character length, or bit length is not represented in the catalog lookup. The smallest practical fix is to parse the type name into its base name and modifier before alias canonicalization, resolve the base name, and pass the parsed modifier through the declaration/type construction path rather than treating the entire declaration type as a catalog name.
  • Why this is likely a bug: The expected behavior is that a qualified alias and its modifier behave like the corresponding PostgreSQL type declaration. The local SQL evidence shows a concrete failure for pg_catalog.character varying(5), while source inspection explains it without relying on the unavailable browser endpoint: the resolver performs a string lookup on a modifier-bearing name and has no representation for the modifier after canonicalization. This is directly within the PR's changed PL/pgSQL mapping branch, not a test-only or browser problem. A targeted fix that separates the base alias from its typmod and preserves the typmod during type resolution should address the failure without changing unrelated schema lookup behavior.
Relevant code

server/plpgsql/statements.go:115-124

for _, variable := range stmt.Variables {
	op := InterpreterOperation{
		OpCode: OpCode_Declare,
		PrimaryData: variable.Type,
		Target: variable.Name,
	}
	var val any
	if variable.Default != "" {
		op.SecondaryData = []string{variable.Default}
		val = variable.Default
	}

server/plpgsql/interpreter_logic.go:137-160

typeName := operation.PrimaryData
typeName = strings.ReplaceAll(typeName, `"`, "")
schemaName := "pg_catalog"
if strings.Contains(typeName, ".") {
	parts := strings.Split(typeName, ".")
	schemaName = parts[0]
	typeName = parts[1]
	if schemaName == "pg_catalog" {
		arrayPrefix := ""
		elementName := typeName
		if strings.HasPrefix(typeName, "_") {
			arrayPrefix, elementName = "_", typeName[1:]
		}
		typ, ok, _ := types.TypeForNonKeywordTypeName(elementName)
		if ok && typ != nil {
			typeName = arrayPrefix + typ.PGName()
		}
	}

postgres/parser/types/types.go:2522-2539

func normalizeTypeName(name string) string {
	if !strings.ContainsAny(name, " \t\n\r\f\v") {
		return name
	}
	return typeNameWhitespace.ReplaceAllString(strings.TrimSpace(name), " ")
}

func TypeForNonKeywordTypeName(name string) (*T, bool, int) {
	name = normalizeTypeName(name)
	t, ok := typNameLiterals[name]
	if ok {
		return t, ok, 0
	}
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Medium severity — Qualified type limits are lost**

**What failed:** Calling the function with the qualified character type failed instead of preserving its length limit. The decimal case returned a value, but the qualified character varying(5) case reported that the full text was not a type.

- **Impact:** Functions that declare a schema-qualified character type with a length limit fail instead of running correctly. Users may need to avoid the qualified form, and the affected declaration cannot enforce its intended limit.
- **Steps to reproduce:**
  1. Create a PL/pgSQL function with a local variable declared as pg_catalog.decimal(8,2), pg_catalog.character varying(5), or pg_catalog.bit varying(4), and assign a value that fits the modifier.
  2. Call each function and inspect its returned value and type metadata.
  3. Compare the results with the declaration's precision, character length, or bit length; the qualified character varying(5) case fails with a type-does-not-exist error.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** server/plpgsql/statements.go:115-124 copies variable.Type into InterpreterOperation.PrimaryData without separating the base type from its modifier, so a declaration such as pg_catalog.character varying(5) reaches execution as one string. In server/plpgsql/interpreter_logic.go:129-160, the OpCode_Declare handler removes quotes, splits the schema from the remaining text, and calls types.TypeForNonKeywordTypeName(elementName) only to map an alias. That lookup receives the modifier-bearing text and cannot turn character varying(5) into the base alias character varying. The handler then calls typeCollection.GetType(ctx, id.NewType(schemaName, typeName)) at line 160 using the unchanged modifier-bearing name when alias lookup misses, producing the observed type-does-not-exist error. Even where a base alias is found, the code replaces typeName with arrayPrefix + typ.PGName() at lines 154-157, which has no typmod component, so the declaration's precision, character length, or bit length is not represented in the catalog lookup. The smallest practical fix is to parse the type name into its base name and modifier before alias canonicalization, resolve the base name, and pass the parsed modifier through the declaration/type construction path rather than treating the entire declaration type as a catalog name.
- **Why this is likely a bug:** The expected behavior is that a qualified alias and its modifier behave like the corresponding PostgreSQL type declaration. The local SQL evidence shows a concrete failure for pg_catalog.character varying(5), while source inspection explains it without relying on the unavailable browser endpoint: the resolver performs a string lookup on a modifier-bearing name and has no representation for the modifier after canonicalization. This is directly within the PR's changed PL/pgSQL mapping branch, not a test-only or browser problem. A targeted fix that separates the base alias from its typmod and preserves the typmod during type resolution should address the failure without changing unrelated schema lookup behavior.

**Relevant code:**

`server/plpgsql/statements.go:115-124`

~~~go
for _, variable := range stmt.Variables {
	op := InterpreterOperation{
		OpCode: OpCode_Declare,
		PrimaryData: variable.Type,
		Target: variable.Name,
	}
	var val any
	if variable.Default != "" {
		op.SecondaryData = []string{variable.Default}
		val = variable.Default
	}
~~~

`server/plpgsql/interpreter_logic.go:137-160`

~~~go
typeName := operation.PrimaryData
typeName = strings.ReplaceAll(typeName, `"`, "")
schemaName := "pg_catalog"
if strings.Contains(typeName, ".") {
	parts := strings.Split(typeName, ".")
	schemaName = parts[0]
	typeName = parts[1]
	if schemaName == "pg_catalog" {
		arrayPrefix := ""
		elementName := typeName
		if strings.HasPrefix(typeName, "_") {
			arrayPrefix, elementName = "_", typeName[1:]
		}
		typ, ok, _ := types.TypeForNonKeywordTypeName(elementName)
		if ok && typ != nil {
			typeName = arrayPrefix + typ.PGName()
		}
	}
~~~

`postgres/parser/types/types.go:2522-2539`

~~~go
func normalizeTypeName(name string) string {
	if !strings.ContainsAny(name, " \t\n\r\f\v") {
		return name
	}
	return typeNameWhitespace.ReplaceAllString(strings.TrimSpace(name), " ")
}

func TypeForNonKeywordTypeName(name string) (*T, bool, int) {
	name = normalizeTypeName(name)
	t, ok := typNameLiterals[name]
	if ok {
		return t, ok, 0
	}
~~~

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View All Evidence

High severity Qualified array declarations fail when called

What failed: The functions were accepted, but invoking the qualified array functions produced a malformed array literal error. The expected array values, element types, and array metadata were not returned.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: High High severity
  • Impact: Users who define functions with schema-qualified array types cannot call those functions successfully. Their array values and type metadata do not return as expected.
  • Steps to Reproduce:
    1. Connect to the local Doltgres server as postgres.
    2. Create a PL/pgSQL function with a local variable declared as pg_catalog.boolean[] and initialize it with a representative array value.
    3. Repeat with pg_catalog.character varying[] and pg_catalog.double precision[], then create a comparable function using pg_catalog._int4.
    4. Call each function and check whether the array value and array type are returned.
    5. Observe the malformed array literal error instead of a successful array round-trip.
  • Stub / mock content: Local SCRAM authentication was disabled so the test could connect to the local Doltgres server. No application mocks, route interceptions, or test-data bypasses were used.
  • Code Analysis: In server/plpgsql/statements.go:115-129, each PL/pgSQL declaration is emitted as an OpCode_Declare whose PrimaryData contains the type text. In server/plpgsql/interpreter_logic.go:129-160, the declaration text is split into schemaName and typeName before TypeCollection lookup. The PR-modified branch at lines 140-158 handles an array only when typeName starts with '_' (lines 149-153), strips that prefix for alias lookup, and restores it with typ.PGName() (line 156). It never parses a SQL [] suffix. Therefore a declaration such as pg_catalog.boolean[] keeps the bracket form while alias lookup expects a base element name and the TypeCollection lookup cannot establish the correct registered array type. The subsequent default path at lines 167-189 calls resolvedType.IoInput with the declaration's default text; with the wrong or incomplete array resolution, the ARRAY[...] expression is treated as an invalid array literal, matching the recorded error. The smallest practical fix is to detect and remove the [] suffix before alias lookup, resolve the normalized element alias, then construct the underscore-prefixed registered array name before calling GetType; the internal pg_catalog._int4 spelling must continue to work as it does today.
  • Why this is likely a bug: The test exercises ordinary PostgreSQL array declarations, not an artificial fault or an unavailable service. The local server accepted all declarations and then failed consistently when the resulting functions were called, while the source path independently shows that SQL [] syntax is not converted to the underscore-prefixed array lookup form used by the type collection. PostgreSQL users expect boolean[], character varying[], and double precision[] to preserve array shape and values; a malformed literal at invocation prevents that core function workflow. The PR's changed mapping branch is the smallest practical repair surface because it already owns schema-qualified alias and array-name normalization.
Relevant code

server/plpgsql/interpreter_logic.go:129-160

case OpCode_Declare: ... typeName := operation.PrimaryData ... if strings.Contains(typeName, ".") { ... if schemaName == "pg_catalog" { arrayPrefix := ""; elementName := typeName; if strings.HasPrefix(typeName, "_") { arrayPrefix, elementName = "_", typeName[1:] }; typ, ok, _ := types.TypeForNonKeywordTypeName(elementName); if ok && typ != nil { typeName = arrayPrefix + typ.PGName() } } } ... resolvedType, err := typeCollection.GetType(ctx, id.NewType(schemaName, typeName))

server/plpgsql/statements.go:115-129

for _, variable := range stmt.Variables { op := InterpreterOperation{OpCode: OpCode_Declare, PrimaryData: variable.Type, Target: variable.Name}; ... *ops = append(*ops, op); stack.NewVariableWithValue(variable.Name, nil, val) }

server/types/type.go:143-154

func NewUnresolvedArrayDoltgresType(sch, elemName string) *DoltgresType { return &DoltgresType{ID: id.NewType(sch, "_"+elemName), IsUnresolved: true, TypCategory: TypeCategory_ArrayTypes, Elem: &DoltgresType{ID: id.NewType(sch, elemName), IsUnresolved: true}, ...} }
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**High severity — Qualified array declarations fail when called**

**What failed:** The functions were accepted, but invoking the qualified array functions produced a malformed array literal error. The expected array values, element types, and array metadata were not returned.

- **Impact:** Users who define functions with schema-qualified array types cannot call those functions successfully. Their array values and type metadata do not return as expected.
- **Steps to reproduce:**
  1. Connect to the local Doltgres server as postgres.
  2. Create a PL/pgSQL function with a local variable declared as pg_catalog.boolean[] and initialize it with a representative array value.
  3. Repeat with pg_catalog.character varying[] and pg_catalog.double precision[], then create a comparable function using pg_catalog._int4.
  4. Call each function and check whether the array value and array type are returned.
  5. Observe the malformed array literal error instead of a successful array round-trip.
- **Stub / mock content:** Local SCRAM authentication was disabled so the test could connect to the local Doltgres server. No application mocks, route interceptions, or test-data bypasses were used.
- **Code analysis:** In server/plpgsql/statements.go:115-129, each PL/pgSQL declaration is emitted as an OpCode_Declare whose PrimaryData contains the type text. In server/plpgsql/interpreter_logic.go:129-160, the declaration text is split into schemaName and typeName before TypeCollection lookup. The PR-modified branch at lines 140-158 handles an array only when typeName starts with '_' (lines 149-153), strips that prefix for alias lookup, and restores it with typ.PGName() (line 156). It never parses a SQL [] suffix. Therefore a declaration such as pg_catalog.boolean[] keeps the bracket form while alias lookup expects a base element name and the TypeCollection lookup cannot establish the correct registered array type. The subsequent default path at lines 167-189 calls resolvedType.IoInput with the declaration's default text; with the wrong or incomplete array resolution, the ARRAY[...] expression is treated as an invalid array literal, matching the recorded error. The smallest practical fix is to detect and remove the [] suffix before alias lookup, resolve the normalized element alias, then construct the underscore-prefixed registered array name before calling GetType; the internal pg_catalog._int4 spelling must continue to work as it does today.
- **Why this is likely a bug:** The test exercises ordinary PostgreSQL array declarations, not an artificial fault or an unavailable service. The local server accepted all declarations and then failed consistently when the resulting functions were called, while the source path independently shows that SQL [] syntax is not converted to the underscore-prefixed array lookup form used by the type collection. PostgreSQL users expect boolean[], character varying[], and double precision[] to preserve array shape and values; a malformed literal at invocation prevents that core function workflow. The PR's changed mapping branch is the smallest practical repair surface because it already owns schema-qualified alias and array-name normalization.

**Relevant code:**

`server/plpgsql/interpreter_logic.go:129-160`

~~~go
case OpCode_Declare: ... typeName := operation.PrimaryData ... if strings.Contains(typeName, ".") { ... if schemaName == "pg_catalog" { arrayPrefix := ""; elementName := typeName; if strings.HasPrefix(typeName, "_") { arrayPrefix, elementName = "_", typeName[1:] }; typ, ok, _ := types.TypeForNonKeywordTypeName(elementName); if ok && typ != nil { typeName = arrayPrefix + typ.PGName() } } } ... resolvedType, err := typeCollection.GetType(ctx, id.NewType(schemaName, typeName))
~~~

`server/plpgsql/statements.go:115-129`

~~~go
for _, variable := range stmt.Variables { op := InterpreterOperation{OpCode: OpCode_Declare, PrimaryData: variable.Type, Target: variable.Name}; ... *ops = append(*ops, op); stack.NewVariableWithValue(variable.Name, nil, val) }
~~~

`server/types/type.go:143-154`

~~~go
func NewUnresolvedArrayDoltgresType(sch, elemName string) *DoltgresType { return &DoltgresType{ID: id.NewType(sch, "_"+elemName), IsUnresolved: true, TypCategory: TypeCategory_ArrayTypes, Elem: &DoltgresType{ID: id.NewType(sch, elemName), IsUnresolved: true}, ...} }
~~~

Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,19 @@ func call(ctx *sql.Context, iFunc InterpretedFunction, stack InterpreterStack) (
parts := strings.Split(typeName, ".")
schemaName = parts[0]
typeName = parts[1]
// Check the NonKeyword type names to see if we're looking at
// an alias of a type if we're in the pg_catalog schema.
// Skip array types (names starting with "_") since their internal
// lookup key uses the "_typename" form, not the "typename[]" form
// that TypeForNonKeywordTypeName returns.
if schemaName == "pg_catalog" && !strings.HasPrefix(typeName, "_") {
typ, ok, _ := types.TypeForNonKeywordTypeName(typeName)
// Within pg_catalog the name may be an alias, so map it to the name the type is
// registered under (`pg_catalog.boolean` to `pg_catalog.bool`). An array type arrives
// as "_typename", which is also the form its lookup key takes, so it is the element
// name that gets mapped.
if schemaName == "pg_catalog" {
arrayPrefix := ""
elementName := typeName
if strings.HasPrefix(typeName, "_") {
arrayPrefix, elementName = "_", typeName[1:]
}
typ, ok, _ := types.TypeForNonKeywordTypeName(elementName)
if ok && typ != nil {
typeName = typ.Name()
typeName = arrayPrefix + typ.PGName()
}
}
}
Expand Down
202 changes: 202 additions & 0 deletions testing/go/plpgsql_type_alias_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// Copyright 2026 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package _go

import (
"fmt"
"testing"

"github.com/dolthub/go-mysql-server/sql"
)

// typeAliasCase declares a single PL/pgSQL variable of the type named by TypeName and checks what the variable
// actually holds, so that a type name resolving to the wrong type is caught rather than just a type name that
// fails to resolve at all.
type typeAliasCase struct {
// FuncName names the generated function, and so also names the assertion.
FuncName string
// TypeName is written verbatim into the DECLARE, e.g. "pg_catalog.boolean".
TypeName string
// Literal is the DECLARE's default value, written verbatim. When empty the variable is declared without one.
Literal string
// ValueExpr is the expression the function returns, and defaults to "b::text".
ValueExpr string
// ExpectedType is the expected pg_typeof of the declared variable. When empty, pg_typeof is not checked,
// which is necessary for the types that pg_typeof does not report yet.
ExpectedType string
// ExpectedValue is the expected result of ValueExpr.
ExpectedValue string
}

func (c typeAliasCase) statement() string {
declaration := fmt.Sprintf("b %s", c.TypeName)
if c.Literal != "" {
declaration = fmt.Sprintf("%s := %s", declaration, c.Literal)
}
value := c.ValueExpr
if value == "" {
value = "b::text"
}
if c.ExpectedType != "" {
value = fmt.Sprintf("pg_typeof(b)::text || '|' || %s", value)
}
return fmt.Sprintf(`CREATE FUNCTION %s() RETURNS text AS $$ DECLARE %s; BEGIN RETURN %s; END; $$ LANGUAGE plpgsql;`,
c.FuncName, declaration, value)
}

func (c typeAliasCase) expected() string {
if c.ExpectedType != "" {
return c.ExpectedType + "|" + c.ExpectedValue
}
return c.ExpectedValue
}

// typeAliasScript builds a ScriptTest that creates one function per case and asserts on what each one returns.
func typeAliasScript(name string, cases []typeAliasCase) ScriptTest {
script := ScriptTest{Name: name}
for _, c := range cases {
script.SetUpScript = append(script.SetUpScript, c.statement())
script.Assertions = append(script.Assertions, ScriptTestAssertion{
Query: fmt.Sprintf("SELECT %s();", c.FuncName),
Expected: []sql.Row{{c.expected()}},
})
}
return script
}

// TestPlpgsqlDeclareTypeAliases tests that a PL/pgSQL variable may be declared with any of the SQL standard type
// names that Postgres accepts as aliases for its own type names, qualified with the pg_catalog schema.
//
// The grammar resolves these names when they are written bare, but a schema-qualified name is handed to the
// interpreter as text (pg_query_go reports a declared type as the text that was written), so it can only be
// resolved by looking the name up in the parser's type name table.
func TestPlpgsqlDeclareTypeAliases(t *testing.T) {
RunScripts(t, []ScriptTest{
typeAliasScript("schema-qualified numeric type aliases", []typeAliasCase{
{FuncName: "alias_boolean", TypeName: "pg_catalog.boolean", Literal: "true",
ExpectedType: "boolean", ExpectedValue: "true"},
{FuncName: "alias_int", TypeName: "pg_catalog.int", Literal: "11",
ExpectedType: "integer", ExpectedValue: "11"},
{FuncName: "alias_integer", TypeName: "pg_catalog.integer", Literal: "12",
ExpectedType: "integer", ExpectedValue: "12"},
{FuncName: "alias_bigint", TypeName: "pg_catalog.bigint", Literal: "9223372036854775807",
ExpectedType: "bigint", ExpectedValue: "9223372036854775807"},
{FuncName: "alias_smallint", TypeName: "pg_catalog.smallint", Literal: "32767",
ExpectedType: "smallint", ExpectedValue: "32767"},
{FuncName: "alias_decimal", TypeName: "pg_catalog.decimal", Literal: "'1.25'",
ExpectedType: "numeric", ExpectedValue: "1.25"},
{FuncName: "alias_dec", TypeName: "pg_catalog.dec", Literal: "'2.50'",
ExpectedType: "numeric", ExpectedValue: "2.50"},
{FuncName: "alias_real", TypeName: "pg_catalog.real", Literal: "'1.5'",
ExpectedType: "real", ExpectedValue: "1.5"},
{FuncName: "alias_double_precision", TypeName: "pg_catalog.double precision", Literal: "'2.5'",
ExpectedType: "double precision", ExpectedValue: "2.5"},
// FLOAT with no precision is FLOAT8, the same as DOUBLE PRECISION.
{FuncName: "alias_float", TypeName: "pg_catalog.float", Literal: "'3.5'",
ExpectedType: "double precision", ExpectedValue: "3.5"},
}),
typeAliasScript("schema-qualified character type aliases", []typeAliasCase{
{FuncName: "alias_character_varying", TypeName: "pg_catalog.character varying", Literal: "'abc'",
ExpectedType: "character varying", ExpectedValue: "abc"},
{FuncName: "alias_char_varying", TypeName: "pg_catalog.char varying", Literal: "'def'",
ExpectedType: "character varying", ExpectedValue: "def"},
{FuncName: "alias_national_character_varying", TypeName: "pg_catalog.national character varying", Literal: "'ghi'",
ExpectedType: "character varying", ExpectedValue: "ghi"},
{FuncName: "alias_national_char_varying", TypeName: "pg_catalog.national char varying", Literal: "'jkl'",
ExpectedType: "character varying", ExpectedValue: "jkl"},
{FuncName: "alias_nchar_varying", TypeName: "pg_catalog.nchar varying", Literal: "'mno'",
ExpectedType: "character varying", ExpectedValue: "mno"},
{FuncName: "alias_character", TypeName: "pg_catalog.character", Literal: "'pqr'",
ExpectedType: "character", ExpectedValue: "pqr"},
{FuncName: "alias_national_character", TypeName: "pg_catalog.national character", Literal: "'stu'",
ExpectedType: "character", ExpectedValue: "stu"},
{FuncName: "alias_nchar", TypeName: "pg_catalog.nchar", Literal: "'vwx'",
ExpectedType: "character", ExpectedValue: "vwx"},
}),
typeAliasScript("schema-qualified date/time type aliases", []typeAliasCase{
{FuncName: "alias_timestamp_without_tz", TypeName: "pg_catalog.timestamp without time zone",
Literal: "'2024-01-02 03:04:05'", ExpectedType: "timestamp without time zone",
ExpectedValue: "2024-01-02 03:04:05"},
// The value is rendered in UTC rather than in the session's time zone so that the expected result does
// not depend on where the test runs.
{FuncName: "alias_timestamp_with_tz", TypeName: "pg_catalog.timestamp with time zone",
Literal: "'2024-01-02 03:04:05+00'", ValueExpr: "(b AT TIME ZONE 'UTC')::text",
ExpectedType: "timestamp with time zone", ExpectedValue: "2024-01-02 03:04:05"},
// pg_typeof does not report the time types yet, so these only check the round-tripped value.
{FuncName: "alias_time_without_tz", TypeName: "pg_catalog.time without time zone",
Literal: "'03:04:05'", ExpectedValue: "03:04:05"},
{FuncName: "alias_time_with_tz", TypeName: "pg_catalog.time with time zone",
Literal: "'03:04:05+00'", ExpectedValue: "03:04:05+00"},
}),
typeAliasScript("schema-qualified bit string type alias", []typeAliasCase{
// A PL/pgSQL variable of a bit string type cannot hold a value yet (assigning one fails the same way
// for the canonical `varbit` spelling), so this only checks that the name resolves to a usable type.
{FuncName: "alias_bit_varying", TypeName: "pg_catalog.bit varying",
ValueExpr: "(b IS NULL)::text", ExpectedValue: "true"},
}),
// pg_query_go reports a declared type as the text that was written, without normalizing the whitespace
// within a multi-word name, so the lookup has to normalize it.
typeAliasScript("multi-word type aliases with irregular whitespace", []typeAliasCase{
{FuncName: "alias_double_precision_spaces", TypeName: "pg_catalog.double precision", Literal: "'4.5'",
ExpectedType: "double precision", ExpectedValue: "4.5"},
{FuncName: "alias_timestamp_tz_newline", TypeName: "pg_catalog.timestamp\nwith\ttime zone",
Literal: "'2024-01-02 03:04:05+00'", ValueExpr: "(b AT TIME ZONE 'UTC')::text",
ExpectedType: "timestamp with time zone", ExpectedValue: "2024-01-02 03:04:05"},
}),
// Not aliases, but they resolve through the same lookup, and each of them is a name whose
// registered spelling differs from the one being declared.
typeAliasScript("names whose registered spelling differs", []typeAliasCase{
{FuncName: "canonical_bytea", TypeName: "pg_catalog.bytea", Literal: `'\x0102'`,
ExpectedValue: `\x0102`},
{FuncName: "unqualified_bytea", TypeName: "bytea", Literal: `'\x0304'`,
ExpectedValue: `\x0304`},
{FuncName: "canonical_bpchar", TypeName: "pg_catalog.bpchar", Literal: "'yz'",
ExpectedType: "character", ExpectedValue: "yz"},
{FuncName: "unqualified_character", TypeName: "character(3)", Literal: "'abc'",
ExpectedType: "character", ExpectedValue: "abc"},
// Unlike the bare CHARACTER keyword, which names bpchar, `pg_catalog.char` names the internal
// one-byte "char" type. A PL/pgSQL variable of that type cannot hold a value yet (assigning one fails
// the same way for the canonical `"char"` spelling), so this only checks that the name resolves.
{FuncName: "canonical_qchar", TypeName: "pg_catalog.char",
ValueExpr: "(b IS NULL)::text", ExpectedValue: "true"},
{FuncName: "canonical_integer_array", TypeName: "pg_catalog.integer[]", Literal: "'{1,2,3}'",
ExpectedType: "integer[]", ExpectedValue: "{1,2,3}"},
}),
})
}

// TestPlpgsqlDeclareUnknownType tests that a schema-qualified type name that names no type is still rejected,
// rather than being resolved by a too-eager alias lookup.
func TestPlpgsqlDeclareUnknownType(t *testing.T) {
RunScripts(t, []ScriptTest{
{
Name: "unknown schema-qualified type names are still rejected",
SetUpScript: []string{
`CREATE FUNCTION unknown_type() RETURNS text AS $$ DECLARE b pg_catalog.not_a_type; BEGIN RETURN 'x'; END; $$ LANGUAGE plpgsql;`,
`CREATE FUNCTION unknown_multiword_type() RETURNS text AS $$ DECLARE b pg_catalog.double imprecision; BEGIN RETURN 'x'; END; $$ LANGUAGE plpgsql;`,
},
Assertions: []ScriptTestAssertion{
{
Query: "SELECT unknown_type();",
ExpectedErr: `type "pg_catalog.not_a_type" does not exist`,
},
{
Query: "SELECT unknown_multiword_type();",
ExpectedErr: `type "pg_catalog.double imprecision" does not exist`,
},
},
},
})
}
Loading