]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
refactor: dispatch type modifier parsing to type-specific objects
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 8 May 2024 21:49:19 +0000 (23:49 +0200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 15 May 2024 15:56:39 +0000 (17:56 +0200)
This changeset is enough to pass the current test suite, but probably it
might require some cleanup.

Close #450

psycopg/psycopg/_column.py
psycopg/psycopg/_oids.py
psycopg/psycopg/_typeinfo.py
psycopg/psycopg/_typemod.py [new file with mode: 0644]
psycopg/psycopg/crdb/_types.py
psycopg/psycopg/postgres.py
psycopg_c/psycopg_c/_psycopg/oids.pxd
tools/update_oids.py

index 331df6266bc757942796b1a1a9111cc50abbc4fc..171f3470daddcd14c3c81bf72ae48636018004ba 100644 (file)
@@ -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]:
index fafb6201ff5c552af2d0c2224827ba66bdebf4f8..5e3d6dc55c061ecd0fcdab5ff371600d2ac05fb1 100644 (file)
@@ -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
index a95376bec22dfef6299de736dd571b069f108c6e..f380cb03564c3c13e279526b23a5ae5c680146e8 100644 (file)
@@ -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 (file)
index 0000000..a8c43e8
--- /dev/null
@@ -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
index f19e7c6ce427a858b3a7253c5f076b700a5ddd53..dffb9620e70a200616b43ade1c54c0cce3412c6b 100644 (file)
@@ -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)
index 7c50dfc7936d2b93288c595bc30ace718857517e..4e3eecbb51303cc01787159f93c7fc0db4c844b5 100644 (file)
@@ -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),
index f89447be7648b6827ed452b8574983069374e882..d7bfcc21e520caf7db36d7f080318a30f33dd10f 100644 (file)
@@ -11,7 +11,7 @@ cdef enum:
 
     # autogenerated: start
 
-    # Generated from PostgreSQL 16.0
+    # Generated from PostgreSQL 16.2
 
     ACLITEM_OID = 1033
     BIT_OID = 1560
index 80980cc38fffe6e2067a92a37dbf2b286412c8dd..6697643872eff2d5d16538722a2c9ffd5336692e 100755 (executable)
@@ -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)}),")