From a90a20ed09caef0e347bb9b3a83cb6f2f9d89098 Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Wed, 8 May 2024 23:49:19 +0200 Subject: [PATCH] refactor: dispatch type modifier parsing to type-specific objects This changeset is enough to pass the current test suite, but probably it might require some cleanup. Close #450 --- psycopg/psycopg/_column.py | 54 +++++---------------- psycopg/psycopg/_oids.py | 2 +- psycopg/psycopg/_typeinfo.py | 15 ++++++ psycopg/psycopg/_typemod.py | 67 +++++++++++++++++++++++++++ psycopg/psycopg/crdb/_types.py | 45 ++++++++++++++---- psycopg/psycopg/postgres.py | 47 +++++++++++++++---- psycopg_c/psycopg_c/_psycopg/oids.pxd | 2 +- tools/update_oids.py | 18 ++++++- 8 files changed, 187 insertions(+), 63 deletions(-) create mode 100644 psycopg/psycopg/_typemod.py diff --git a/psycopg/psycopg/_column.py b/psycopg/psycopg/_column.py index 331df6266..171f3470d 100644 --- a/psycopg/psycopg/_column.py +++ b/psycopg/psycopg/_column.py @@ -55,19 +55,16 @@ class Column(Sequence[Any]): return 7 def _type_display(self) -> str: + if not self._type: + return str(self.type_code) + parts = [] - parts.append(self._type.name if self._type else str(self.type_code)) - - mod1 = self.precision - if mod1 is None: - mod1 = self.display_size - if mod1: - parts.append(f"({mod1}") - if self.scale: - parts.append(f", {self.scale}") - parts.append(")") - - if self._type and self.type_code == self._type.array_oid: + parts.append(self._type.name) + mod = self._type.get_modifier(self._data.fmod) + if mod: + parts.append(f"({', '.join(map(str, mod))})") + + if self.type_code == self._type.array_oid: parts.append("[]") return "".join(parts) @@ -91,15 +88,7 @@ class Column(Sequence[Any]): @property def display_size(self) -> Optional[int]: """The field size, for :sql:`varchar(n)`, None otherwise.""" - if not self._type: - return None - - if self._type.name in ("varchar", "char"): - fmod = self._data.fmod - if fmod >= 0: - return fmod - 4 - - return None + return self._type.get_display_size(self._data.fmod) if self._type else None @property def internal_size(self) -> Optional[int]: @@ -110,31 +99,12 @@ class Column(Sequence[Any]): @property def precision(self) -> Optional[int]: """The number of digits for fixed precision types.""" - if not self._type: - return None - - dttypes = ("time", "timetz", "timestamp", "timestamptz", "interval") - if self._type.name == "numeric": - fmod = self._data.fmod - if fmod >= 0: - return fmod >> 16 - - elif self._type.name in dttypes: - fmod = self._data.fmod - if fmod >= 0: - return fmod & 0xFFFF - - return None + return self._type.get_precision(self._data.fmod) if self._type else None @property def scale(self) -> Optional[int]: """The number of digits after the decimal point if available.""" - if self._type and self._type.name == "numeric": - fmod = self._data.fmod - 4 - if fmod >= 0: - return fmod & 0xFFFF - - return None + return self._type.get_scale(self._data.fmod) if self._type else None @property def null_ok(self) -> Optional[bool]: diff --git a/psycopg/psycopg/_oids.py b/psycopg/psycopg/_oids.py index fafb6201f..5e3d6dc55 100644 --- a/psycopg/psycopg/_oids.py +++ b/psycopg/psycopg/_oids.py @@ -15,7 +15,7 @@ TEXT_ARRAY_OID = 1009 # Use tools/update_oids.py to update this data. # autogenerated: start -# Generated from PostgreSQL 16.0 +# Generated from PostgreSQL 16.2 ACLITEM_OID = 1033 BIT_OID = 1560 diff --git a/psycopg/psycopg/_typeinfo.py b/psycopg/psycopg/_typeinfo.py index a95376bec..f380cb035 100644 --- a/psycopg/psycopg/_typeinfo.py +++ b/psycopg/psycopg/_typeinfo.py @@ -16,6 +16,7 @@ from . import errors as e from .abc import AdaptContext, Query from .rows import dict_row from ._compat import TypeAlias, TypeVar +from ._typemod import TypeModifier from ._encodings import conn_encoding if TYPE_CHECKING: @@ -42,12 +43,14 @@ class TypeInfo: *, regtype: str = "", delimiter: str = ",", + typemod: type[TypeModifier] = TypeModifier, ): self.name = name self.oid = oid self.array_oid = array_oid self.regtype = regtype or name self.delimiter = delimiter + self.typemod = typemod(oid) def __repr__(self) -> str: return ( @@ -191,6 +194,18 @@ ORDER BY t.oid """Method called by the `!registry` when the object is added there.""" pass + def get_modifier(self, fmod: int) -> tuple[int, ...] | None: + return self.typemod.get_modifier(fmod) + + def get_display_size(self, fmod: int) -> int | None: + return self.typemod.get_display_size(fmod) + + def get_precision(self, fmod: int) -> int | None: + return self.typemod.get_precision(fmod) + + def get_scale(self, fmod: int) -> int | None: + return self.typemod.get_scale(fmod) + class TypesRegistry: """ diff --git a/psycopg/psycopg/_typemod.py b/psycopg/psycopg/_typemod.py new file mode 100644 index 000000000..a8c43e887 --- /dev/null +++ b/psycopg/psycopg/_typemod.py @@ -0,0 +1,67 @@ +""" +PostgreSQL type modifiers. + +The type modifiers parse catalog information to obtain the type modifier +of a column - the numeric part of varchar(10) or decimal(6,2). +""" + +# Copyright (C) 2024 The Psycopg Team + +from __future__ import annotations + + +class TypeModifier: + """Type modifier that doesn't know any modifier. + + Useful to describe types with no type modifier. + """ + + def __init__(self, oid: int): + self.oid = oid + + def get_modifier(self, typemod: int) -> tuple[int, ...] | None: + return None + + def get_display_size(self, typemod: int) -> int | None: + return None + + def get_precision(self, typemod: int) -> int | None: + return None + + def get_scale(self, typemod: int) -> int | None: + return None + + +class NumericTypeModifier(TypeModifier): + """Handle numeric type modifier.""" + + def get_modifier(self, typemod: int) -> tuple[int, ...] | None: + rv = [] + precision = self.get_precision(typemod) + if precision is not None: + rv.append(precision) + scale = self.get_scale(typemod) + if scale is not None: + rv.append(scale) + return tuple(rv) if rv else None + + def get_precision(self, typemod: int) -> int | None: + return typemod >> 16 if typemod >= 0 else None + + def get_scale(self, typemod: int) -> int | None: + typemod -= 4 + return typemod & 0xFFFF if typemod >= 0 else None + + +class CharTypeModifier(TypeModifier): + """Handle char/varchar type modifier.""" + + def get_display_size(self, typemod: int) -> int | None: + return typemod - 4 if typemod >= 0 else None + + +class TimeTypeModifier(TypeModifier): + """Handle time-related types modifier.""" + + def get_precision(self, typemod: int) -> int | None: + return typemod & 0xFFFF if typemod >= 0 else None diff --git a/psycopg/psycopg/crdb/_types.py b/psycopg/psycopg/crdb/_types.py index f19e7c6ce..dffb9620e 100644 --- a/psycopg/psycopg/crdb/_types.py +++ b/psycopg/psycopg/crdb/_types.py @@ -9,6 +9,7 @@ from .._typeinfo import TypeInfo, TypesRegistry from ..abc import AdaptContext, NoneType from .._oids import TEXT_OID +from .._typemod import CharTypeModifier, NumericTypeModifier, TimeTypeModifier from .._adapters_map import AdaptersMap from ..types.enum import EnumDumper, EnumBinaryDumper from ..types.none import NoneDumper @@ -131,7 +132,7 @@ def register_crdb_types(types: TypesRegistry) -> None: TypeInfo("int8", 20, 1016, regtype="integer"), # Alias integer -> int8 TypeInfo('"char"', 18, 1002), # special case, not generated # autogenerated: start - # Generated from CockroachDB 22.2.1 + # Generated from CockroachDB 23.1.10 TypeInfo("bit", 1560, 1561), TypeInfo("bool", 16, 1000, regtype="boolean"), TypeInfo("bpchar", 1042, 1014, regtype="character"), @@ -144,10 +145,10 @@ def register_crdb_types(types: TypesRegistry) -> None: TypeInfo("int2vector", 22, 1006), TypeInfo("int4", 23, 1007), TypeInfo("int8", 20, 1016, regtype="bigint"), - TypeInfo("interval", 1186, 1187), + TypeInfo("interval", 1186, 1187, typemod=TimeTypeModifier), TypeInfo("jsonb", 3802, 3807), TypeInfo("name", 19, 1003), - TypeInfo("numeric", 1700, 1231), + TypeInfo("numeric", 1700, 1231, typemod=NumericTypeModifier), TypeInfo("oid", 26, 1028), TypeInfo("oidvector", 30, 1013), TypeInfo("record", 2249, 2287), @@ -158,14 +159,42 @@ def register_crdb_types(types: TypesRegistry) -> None: TypeInfo("regrole", 4096, 4097), TypeInfo("regtype", 2206, 2211), TypeInfo("text", 25, 1009), - TypeInfo("time", 1083, 1183, regtype="time without time zone"), - TypeInfo("timestamp", 1114, 1115, regtype="timestamp without time zone"), - TypeInfo("timestamptz", 1184, 1185, regtype="timestamp with time zone"), - TypeInfo("timetz", 1266, 1270, regtype="time with time zone"), + TypeInfo( + "time", + 1083, + 1183, + regtype="time without time zone", + typemod=TimeTypeModifier, + ), + TypeInfo( + "timestamp", + 1114, + 1115, + regtype="timestamp without time zone", + typemod=TimeTypeModifier, + ), + TypeInfo( + "timestamptz", + 1184, + 1185, + regtype="timestamp with time zone", + typemod=TimeTypeModifier, + ), + TypeInfo( + "timetz", + 1266, + 1270, + regtype="time with time zone", + typemod=TimeTypeModifier, + ), + TypeInfo("tsquery", 3615, 3645), + TypeInfo("tsvector", 3614, 3643), TypeInfo("unknown", 705, 0), TypeInfo("uuid", 2950, 2951), TypeInfo("varbit", 1562, 1563, regtype="bit varying"), - TypeInfo("varchar", 1043, 1015, regtype="character varying"), + TypeInfo( + "varchar", 1043, 1015, regtype="character varying", typemod=CharTypeModifier + ), # autogenerated: end ]: types.add(t) diff --git a/psycopg/psycopg/postgres.py b/psycopg/psycopg/postgres.py index 7c50dfc79..4e3eecbb5 100644 --- a/psycopg/psycopg/postgres.py +++ b/psycopg/psycopg/postgres.py @@ -4,8 +4,9 @@ Types configuration specific to PostgreSQL. # Copyright (C) 2020 The Psycopg Team -from ._typeinfo import TypeInfo, TypesRegistry from .abc import AdaptContext +from ._typemod import CharTypeModifier, NumericTypeModifier, TimeTypeModifier +from ._typeinfo import TypeInfo, TypesRegistry from ._adapters_map import AdaptersMap # Global objects with PostgreSQL builtins and globally registered user types. @@ -21,9 +22,9 @@ def register_default_types(types: TypesRegistry) -> None: # Use tools/update_oids.py to update this data. for t in [ - TypeInfo('"char"', 18, 1002), + TypeInfo('"char"', 18, 1002, typemod=CharTypeModifier), # autogenerated: start - # Generated from PostgreSQL 16.0 + # Generated from PostgreSQL 16.2 TypeInfo("aclitem", 1033, 1034), TypeInfo("bit", 1560, 1561), TypeInfo("bool", 16, 1000, regtype="boolean"), @@ -42,7 +43,7 @@ def register_default_types(types: TypesRegistry) -> None: TypeInfo("int2vector", 22, 1006), TypeInfo("int4", 23, 1007, regtype="integer"), TypeInfo("int8", 20, 1016, regtype="bigint"), - TypeInfo("interval", 1186, 1187), + TypeInfo("interval", 1186, 1187, typemod=TimeTypeModifier), TypeInfo("json", 114, 199), TypeInfo("jsonb", 3802, 3807), TypeInfo("jsonpath", 4072, 4073), @@ -52,7 +53,7 @@ def register_default_types(types: TypesRegistry) -> None: TypeInfo("macaddr8", 774, 775), TypeInfo("money", 790, 791), TypeInfo("name", 19, 1003), - TypeInfo("numeric", 1700, 1231), + TypeInfo("numeric", 1700, 1231, typemod=NumericTypeModifier), TypeInfo("oid", 26, 1028), TypeInfo("oidvector", 30, 1013), TypeInfo("path", 602, 1019), @@ -74,16 +75,42 @@ def register_default_types(types: TypesRegistry) -> None: TypeInfo("regtype", 2206, 2211), TypeInfo("text", 25, 1009), TypeInfo("tid", 27, 1010), - TypeInfo("time", 1083, 1183, regtype="time without time zone"), - TypeInfo("timestamp", 1114, 1115, regtype="timestamp without time zone"), - TypeInfo("timestamptz", 1184, 1185, regtype="timestamp with time zone"), - TypeInfo("timetz", 1266, 1270, regtype="time with time zone"), + TypeInfo( + "time", + 1083, + 1183, + regtype="time without time zone", + typemod=TimeTypeModifier, + ), + TypeInfo( + "timestamp", + 1114, + 1115, + regtype="timestamp without time zone", + typemod=TimeTypeModifier, + ), + TypeInfo( + "timestamptz", + 1184, + 1185, + regtype="timestamp with time zone", + typemod=TimeTypeModifier, + ), + TypeInfo( + "timetz", + 1266, + 1270, + regtype="time with time zone", + typemod=TimeTypeModifier, + ), TypeInfo("tsquery", 3615, 3645), TypeInfo("tsvector", 3614, 3643), TypeInfo("txid_snapshot", 2970, 2949), TypeInfo("uuid", 2950, 2951), TypeInfo("varbit", 1562, 1563, regtype="bit varying"), - TypeInfo("varchar", 1043, 1015, regtype="character varying"), + TypeInfo( + "varchar", 1043, 1015, regtype="character varying", typemod=CharTypeModifier + ), TypeInfo("xid", 28, 1011), TypeInfo("xid8", 5069, 271), TypeInfo("xml", 142, 143), diff --git a/psycopg_c/psycopg_c/_psycopg/oids.pxd b/psycopg_c/psycopg_c/_psycopg/oids.pxd index f89447be7..d7bfcc21e 100644 --- a/psycopg_c/psycopg_c/_psycopg/oids.pxd +++ b/psycopg_c/psycopg_c/_psycopg/oids.pxd @@ -11,7 +11,7 @@ cdef enum: # autogenerated: start - # Generated from PostgreSQL 16.0 + # Generated from PostgreSQL 16.2 ACLITEM_OID = 1033 BIT_OID = 1560 diff --git a/tools/update_oids.py b/tools/update_oids.py index 80980cc38..669764387 100755 --- a/tools/update_oids.py +++ b/tools/update_oids.py @@ -119,6 +119,18 @@ order by typname return lines +typemods = { + "char": "CharTypeModifier", + "varchar": "CharTypeModifier", + "numeric": "NumericTypeModifier", + "time": "TimeTypeModifier", + "timetz": "TimeTypeModifier", + "timestamp": "TimeTypeModifier", + "timestamptz": "TimeTypeModifier", + "interval": "TimeTypeModifier", +} + + def get_py_types(conn: Connection) -> List[str]: # Note: "record" is a pseudotype but still a useful one to have. # "pg_lsn" is a documented public type and useful in streaming replication @@ -138,6 +150,8 @@ where order by typname """ ): + typemod = typemods.get(typname) + # Weird legacy type in postgres catalog if typname == "char": typname = regtype = '"char"' @@ -146,9 +160,11 @@ order by typname if typname == "int4" and conn.info.vendor == "CockroachDB": regtype = typname - params = [f"{typname!r}, {oid}, {typarray}"] + params = [repr(typname), str(oid), str(typarray)] if regtype != typname: params.append(f"regtype={regtype!r}") + if typemod: + params.append(f"typemod={typemod}") if typdelim != ",": params.append(f"delimiter={typdelim!r}") lines.append(f"TypeInfo({','.join(params)}),") -- 2.47.2