Skip to content

Resolve SQL standard type aliases by name in the parser type table - #3233

Open
reltuk wants to merge 1 commit into
mainfrom
aaron/plpgsql-type-alias-resolution
Open

Resolve SQL standard type aliases by name in the parser type table#3233
reltuk wants to merge 1 commit into
mainfrom
aaron/plpgsql-type-alias-resolution

Conversation

@reltuk

@reltuk reltuk commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

A schema-qualified type name such as pg_catalog.boolean reaches the PL/pgSQL interpreter as text rather than as parser input, so it can only be resolved by looking the name up in the parser's type name table, which listed almost none of the SQL standard aliases. This adds them, and normalizes whitespace within a name so that the multi-word spellings match however they happen to be written. The interpreter now maps a name it finds to the spelling the type is registered under, which also fixes bytea, bpchar and json resolving to a different type or to none.

A schema-qualified type name such as `pg_catalog.boolean` reaches the
PL/pgSQL interpreter as text rather than as parser input, so it can only
be resolved by looking the name up in the parser's type name table, which
listed almost none of the SQL standard aliases. This adds them, and
normalizes whitespace within a name so that the multi-word spellings match
however they happen to be written. The interpreter now maps a name it
finds to the spelling the type is registered under, which also fixes
`bytea`, `bpchar` and `json` resolving to a different type or to none.
@reltuk
reltuk requested a review from Hydrocharged August 31, 2026 12:17
@reltuk
reltuk enabled auto-merge August 31, 2026 12:17
@github-actions

Copy link
Copy Markdown
Contributor
Main PR
Total 42090 42090
Successful 19270 19271
Failures 22820 22819
Partial Successes1 5459 5459
Main PR
Successful 45.7828% 45.7852%
Failures 54.2172% 54.2148%

${\color{lightgreen}Progressions (1)}$

subselect

QUERY: select count(*) from tenk1 t
where (exists(select 1 from tenk1 k where k.unique1 = t.unique2) or ten < 0);

Footnotes

  1. These are tests that we're marking as Successful, however they do not match the expected output in some way. This is due to small differences, such as different wording on the error messages, or the column names being incorrect while the data itself is correct.

@coffeegoddd

Copy link
Copy Markdown
Contributor

@reltuk DOLT

read_tests from_latency to_latency percent_change
covering_index_scan_postgres 2.52 2.52 0.0
groupby_scan_postgres 77.19 77.19 0.0
index_join_postgres 2.22 2.22 0.0
index_join_scan_postgres 1.58 1.58 0.0
index_scan_postgres 484.44 484.44 0.0
oltp_point_select 0.36 0.36 0.0
oltp_read_only 6.43 6.32 -1.71
select_random_points 0.7 0.7 0.0
select_random_ranges 1.01 1.03 1.98
table_scan_postgres 484.44 484.44 0.0
types_table_scan_postgres 1213.57 1235.62 1.82
write_tests from_latency to_latency percent_change
oltp_delete_insert_postgres 6.67 6.67 0.0
oltp_insert 3.36 3.36 0.0
oltp_read_write 13.46 13.46 0.0
oltp_update_index 3.55 3.55 0.0
oltp_update_non_index 3.25 3.25 0.0
oltp_write_only 7.04 7.04 0.0
types_delete_insert_postgres 7.17 7.17 0.0

@itoqa

itoqa Bot commented Aug 31, 2026

Copy link
Copy Markdown

Ito QA test results
Commit: ba5eaab: 14 test cases ran, 2 failed ❌, 12 passed ✅.

Summary

The run covers database function behavior for qualified type names across normal values, aliases, whitespace normalization, arrays, length and precision limits, schema isolation, invalid inputs, error recovery, and existing cast behavior. Broad happy-path and edge-case coverage shows most type handling remains healthy, but array handling and constrained type declarations have important correctness gaps.

Not safe to merge yet — this PR introduces a high-severity failure in qualified array declarations and a medium-severity failure in qualified type modifiers, affecting whether functions can be called successfully and whether declared constraints are honored. These are attributable behavior regressions in core type resolution, not unrelated caveats.

Tests run by Ito

View full run

Result Severity Type Description
High severity Rev 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.
Medium severity Rev 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.
Alias A function using the qualified boolean type ran successfully and returned boolean|true, so the declared type and value were preserved.
Alias A variable declared as pg_catalog.bigint without a default was created successfully and returned bigint|0.
General The valid double precision type worked and returned 1.5. Names with the wrong word or extra spaces were rejected as unknown types.
General A scalar integer declaration returned integer and 42. The matching underscore-prefixed declaration returned integer[] and {7,8,9} twice, including after the scalar call.
General An invalid boolean default showed the expected error, and later valid declarations still returned boolean|true in the same session.
General A decimal value declared through the qualified alias was converted correctly every time and matched the canonical numeric type.
Canonical A database function accepted the qualified bytea type, returned the canonical bytea identity, and preserved the CAFE value.
Regression Casting 42 to int4 returned the value 42 and the canonical type integer.
Regression Declaring a variable with an unknown qualified type fails with the expected type-does-not-exist error, and the function is not created.
Rev A missing type in the app_types schema was rejected, while a real app_types.boolean type worked normally. Built-in boolean alias handling did not leak into other schemas.
Whitespace A PL/pgSQL variable with extra spaces around and inside its type name was created successfully. The function returned the canonical type, character varying, and kept the value with its surrounding spaces.
Whitespace A function using a tab and newline between the type words compiled successfully and returned the expected text value as character varying.

Tip

Reply with @itoqa to send us feedback on this test run.

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}, ...} }
~~~

@reltuk

reltuk commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

I believe these two itoqa call outs are actual issues with the plpgsql implementation, but they are not directly caused by this PR. This PR is improving support for some types of existing qualified type names, which currently fail to parse and/or resolve at compile time. It still doesn't handle all cases of parameterized types or array types, but it does improve things somewhat from the status quo.

@Hydrocharged Hydrocharged left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants