Advertise type modifiers in DataTypes from pg_type.typmodin by alaricd · Pull Request #6683 · npgsql/npgsql · GitHub
Skip to content

Advertise type modifiers in DataTypes from pg_type.typmodin - #6683

Open
alaricd wants to merge 1 commit into
npgsql:mainfrom
pengdows:fix/datatypes-typmodin
Open

alaricd wants to merge 1 commit into
npgsql:mainfrom
pengdows:fix/datatypes-typmodin

Conversation

@alaricd

@alaricd alaricd commented Oct 20, 2026

Copy link
Copy Markdown

GetSchema("DataTypes") gets CreateFormat and CreateParameters wrong for the character types. A CreateFormat is the "format string that represents how to add this column to a data definition statement, such as CREATE TABLE", but:

TypeName CreateFormat on main Problem
text TEXT({0}), size PostgreSQL rejects it: type modifier is not allowed for type "text"
character varying CHARACTER VARYING no size
character CHARACTER no size
bit, bit varying BIT, BIT VARYING no length
time, timetz, timestamp, timestamptz, interval no precision TIMESTAMP WITH TIME ZONE(3) would not parse anyway

PopulateHardcodedDataTypeInfo switches on PostgresType.Name, the display name, so case "varchar" never matches (character varying) and case "char" matches the internal one-byte "char" type (OID 18), not bpchar. Both fall through to case "text", which is where the size was added, so only text and "char" got one.

This takes the modifier from the server instead. pg_type.typmodin is how PostgreSQL itself decides whether a type takes a modifier: text has none, which is why text(10) fails. GetDataTypes reads it once (SELECT oid, typmodin::text FROM pg_catalog.pg_type) and adds the modifier after the type's internal name (VARCHAR({0}), BPCHAR({0}), TIMESTAMPTZ({0}), NUMERIC({0},{1})). Parameter names come from the typmodin function, so only the built-in ones are recognised; an extension's modifier is left out, as before. The query is skipped below PostgreSQL 8.3 (where typmodin arrived), on Redshift, and when type loading is off, since a failed query would abort the caller's transaction. Because it now queries, GetDataTypes follows the async path like the other collections.

Tests, which fail on main:

  • DataTypes_CreateFormat_is_accepted_with_its_parameters creates a column from every row that has CreateParameters.
  • DataTypes_CreateFormat_has_a_modifier_when_the_type_takes_one pins the expected format per type, including arrays.
  • DataTypes_DataType_matches_the_type_read_for_types_with_a_modifier checks that DataType is what the reader returns for such a column.

All SchemaTests pass against PostgreSQL 17 (234). The query also runs on CockroachDB (which reports 0 rather than - for no modifier) and on Spanner through PGAdapter.

On Spanner

Selecting from pg_type.typmodin is the right rule, and Spanner is the problem:

  • Real PostgreSQL: typmodin is how PostgreSQL itself decides whether a type takes a modifier. text(10) fails precisely because text has no typmodin, so the rule is accurate by construction.
  • Spanner: it copies PostgreSQL's pg_type rows and keeps numerictypmodin, timestamptztypmodin and intervaltypmodin, then refuses those modifiers ("Type modifier is not supported for type numeric"). A catalog that advertises abilities the server rejects is a catalog bug, and every tool that reads pg_type is misled by it, not just Npgsql.
  • The current switch isn't more correct on Spanner, only differently wrong: it gets TEXT and NUMERIC wrong there today, and text, char, varchar and bpchar wrong on real PostgreSQL.

The internal "char" type is handled in a separate PR, since its CreateFormat needs quoting rather than a modifier.

Found with the pengdows ADO.NET conformance suite, whose type-fidelity tests create a column from each DataTypes row and round-trip values through it.

🤖 Generated with Claude Code

CreateFormat added a size to text, which takes none, so TEXT({0}) could
not be used in CREATE TABLE, and gave none to varchar, bpchar, bit,
varbit, the time types and interval, which take one. The hardcoded
"varchar" and "char" cases never matched varchar or bpchar, since they
compared the display name.

Read pg_type.typmodin, which PostgreSQL uses to decide whether a type
takes a modifier, and add the modifier after the type's internal name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alaricd

alaricd commented Oct 20, 2026 •

Copy link
Copy Markdown
Author

I checked the rule against the PostgreSQL-compatible servers I could run. For each, whether pg_type.typmodin claims a modifier, and whether the server accepts one (CREATE TABLE … (c text(10)), varchar(10), bpchar(10), numeric(10,2), time(3), timetz(3), timestamp(3), timestamptz(3), interval(3), bit(8), varbit(8), "char"):

Server typmodin matches what the server accepts? Result with this PR
PostgreSQL 17 yes correct
TimescaleDB (PostgreSQL 17.11) yes, identical to PostgreSQL correct
Citus (PostgreSQL 18.4) yes, identical correct
YugabyteDB 2026.1 (PostgreSQL 15.12) yes, identical correct
Fujitsu Enterprise Postgres 17 (PostgreSQL 17.11) yes, identical for these types; it adds nvarchartypmodin and nbpchartypmodin for its national-character types, which aren't recognised, so those get no size correct
CockroachDB v26.3 no: typmodin is 0 for every type, but varchar(10), numeric(10,2) and the time precisions are accepted no modifiers advertised: never an invalid definition, but NUMERIC({0},{1}) becomes NUMERIC there
Spanner (PGAdapter) no: claims numerictypmodin, timestamptztypmodin, intervaltypmodin, then rejects them right for text and varchar, wrong where its catalog is

CockroachDB under-reports where Spanner over-reports. If losing NUMERIC({0},{1}) on CockroachDB matters, the old numeric format could be kept when the server reports no modifier for any type; I left that out to keep the rule to what the catalog says, but I'm happy to add it.

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
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.

1 participant