From 9f372678665d2369d9c04d74c00bdee2b429db72 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Fri, 19 Jun 2026 23:24:03 +0800 Subject: [PATCH 01/22] feat(types): implement PostgreSQL DataType subclasses and type support mixin - Add ~45 PostgreSQL-specific DataType subclasses in expression/types.py - Add PostgresTypeFormatSupportMixin with TypeFormattingSupport + TypeParsingSupport - Integrate type support into PostgresDialect - Update exports in __init__.py, expression/__init__.py, mixins/__init__.py, mixins/types/__init__.py --- .../backend/impl/postgres/__init__.py | 97 +++ .../backend/impl/postgres/dialect.py | 6 + .../impl/postgres/expression/__init__.py | 99 +++ .../backend/impl/postgres/expression/types.py | 467 +++++++++++ .../backend/impl/postgres/mixins/__init__.py | 2 + .../impl/postgres/mixins/types/__init__.py | 2 + .../mixins/types/data_type_formatting.py | 760 ++++++++++++++++++ 7 files changed, 1433 insertions(+) create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/expression/types.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py index 83f988d5..9c6f720e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py @@ -23,6 +23,55 @@ from .dialect import PostgresDialect from .transaction import PostgresTransactionManager, AsyncPostgresTransactionManager from .types import PostgresEnumType +from .expression.types import ( + PostgresBigSerialType, + PostgresBitType, + PostgresBoxType, + PostgresByteaType, + PostgresCIDType, + PostgresCircleType, + PostgresDateMultirangeType, + PostgresDateRangeType, + PostgresGeographyType, + PostgresGeometryType, + PostgresHstoreType, + PostgresInetType, + PostgresInt4MultirangeType, + PostgresInt4RangeType, + PostgresInt8MultirangeType, + PostgresInt8RangeType, + PostgresJsonPathType, + PostgresLineSegmentType, + PostgresLineType, + PostgresMacAddr8Type, + PostgresMacAddrType, + PostgresMoneyType, + PostgresNumMultirangeType, + PostgresNumRangeType, + PostgresOIDType, + PostgresPathType, + PostgresPgLSNType, + PostgresPointType, + PostgresPolygonType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresTIDType, + PostgresTSQueryType, + PostgresTSVectorType, + PostgresTsMultirangeType, + PostgresTsRangeType, + PostgresTsTzMultirangeType, + PostgresTsTzRangeType, + PostgresUUIDType, + PostgresVarBitType, + PostgresVectorType, + PostgresXID8Type, + PostgresXIDType, + PostgresXMLType, + PostgresCidrType, +) from .explain import PostgresExplainResult, PostgresExplainPlanLine from .expression.ddl import ( PostgresCreateEnumTypeExpression, @@ -505,4 +554,52 @@ # hstore functions - Subscript "hstore_subscript_get", "hstore_subscript_set", + # PostgreSQL DataType subclasses for DDL + "PostgresBigSerialType", + "PostgresBitType", + "PostgresBoxType", + "PostgresByteaType", + "PostgresCIDType", + "PostgresCircleType", + "PostgresDateMultirangeType", + "PostgresDateRangeType", + "PostgresGeographyType", + "PostgresGeometryType", + "PostgresHstoreType", + "PostgresInetType", + "PostgresInt4MultirangeType", + "PostgresInt4RangeType", + "PostgresInt8MultirangeType", + "PostgresInt8RangeType", + "PostgresJsonPathType", + "PostgresLineSegmentType", + "PostgresLineType", + "PostgresMacAddr8Type", + "PostgresMacAddrType", + "PostgresMoneyType", + "PostgresNumMultirangeType", + "PostgresNumRangeType", + "PostgresOIDType", + "PostgresPathType", + "PostgresPgLSNType", + "PostgresPointType", + "PostgresPolygonType", + "PostgresRegClassType", + "PostgresRegTypeType", + "PostgresSerialType", + "PostgresSmallSerialType", + "PostgresTIDType", + "PostgresTSQueryType", + "PostgresTSVectorType", + "PostgresTsMultirangeType", + "PostgresTsRangeType", + "PostgresTsTzMultirangeType", + "PostgresTsTzRangeType", + "PostgresUUIDType", + "PostgresVarBitType", + "PostgresVectorType", + "PostgresXID8Type", + "PostgresXIDType", + "PostgresXMLType", + "PostgresCidrType", ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index c59bb218..c4031fed 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -90,6 +90,8 @@ IntrospectionSupport, TransactionControlSupport, SQLFunctionSupport, + TypeFormattingSupport, + TypeParsingSupport, ) from .mixins import ( PostgresExtensionMixin, @@ -169,6 +171,7 @@ PostgresRangeTypeMixin, PostgresJSONBEnhancedMixin, PostgresArrayEnhancedMixin, + PostgresTypeFormatSupportMixin, # DDL/DML operation mixins (new) PostgresExtendedStatisticsMixin, PostgresStoredProcedureMixin, @@ -524,6 +527,9 @@ class PostgresDialect( PostgresRangeTypeSupport, PostgresJSONBEnhancedSupport, PostgresArrayEnhancedSupport, + # DataType Support Protocols + TypeFormattingSupport, + TypeParsingSupport, # New feature protocols PostgresParallelQuerySupport, PostgresStoredProcedureSupport, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py index 9de671dd..dfb3ba27 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py @@ -47,6 +47,57 @@ PostgresPgPartmanDeleteConfigExpression, ) +# DataType subclasses for DDL +from .types import ( + PostgresBigSerialType, + PostgresBitType, + PostgresBoxType, + PostgresByteaType, + PostgresCIDType, + PostgresCircleType, + PostgresDateMultirangeType, + PostgresDateRangeType, + PostgresGeographyType, + PostgresGeometryType, + PostgresHstoreType, + PostgresInetType, + PostgresInt4MultirangeType, + PostgresInt4RangeType, + PostgresInt8MultirangeType, + PostgresInt8RangeType, + PostgresJsonPathType, + PostgresLineSegmentType, + PostgresLineType, + PostgresMacAddr8Type, + PostgresMacAddrType, + PostgresMoneyType, + PostgresNumMultirangeType, + PostgresNumRangeType, + PostgresOIDType, + PostgresPathType, + PostgresPgLSNType, + PostgresPointType, + PostgresPolygonType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresTIDType, + PostgresTSQueryType, + PostgresTSVectorType, + PostgresTsMultirangeType, + PostgresTsRangeType, + PostgresTsTzMultirangeType, + PostgresTsTzRangeType, + PostgresUUIDType, + PostgresVarBitType, + PostgresVectorType, + PostgresXID8Type, + PostgresXIDType, + PostgresXMLType, + PostgresCidrType, +) + __all__ = [ # advisory "PostgresAdvisoryLockExpression", @@ -82,4 +133,52 @@ "PostgresPgPartmanRunMaintenanceExpression", "PostgresPgPartmanUpdateConfigExpression", "PostgresPgPartmanDeleteConfigExpression", + # DataType subclasses for DDL + "PostgresBigSerialType", + "PostgresBitType", + "PostgresBoxType", + "PostgresByteaType", + "PostgresCIDType", + "PostgresCircleType", + "PostgresDateMultirangeType", + "PostgresDateRangeType", + "PostgresGeographyType", + "PostgresGeometryType", + "PostgresHstoreType", + "PostgresInetType", + "PostgresInt4MultirangeType", + "PostgresInt4RangeType", + "PostgresInt8MultirangeType", + "PostgresInt8RangeType", + "PostgresJsonPathType", + "PostgresLineSegmentType", + "PostgresLineType", + "PostgresMacAddr8Type", + "PostgresMacAddrType", + "PostgresMoneyType", + "PostgresNumMultirangeType", + "PostgresNumRangeType", + "PostgresOIDType", + "PostgresPathType", + "PostgresPgLSNType", + "PostgresPointType", + "PostgresPolygonType", + "PostgresRegClassType", + "PostgresRegTypeType", + "PostgresSerialType", + "PostgresSmallSerialType", + "PostgresTIDType", + "PostgresTSQueryType", + "PostgresTSVectorType", + "PostgresTsMultirangeType", + "PostgresTsRangeType", + "PostgresTsTzMultirangeType", + "PostgresTsTzRangeType", + "PostgresUUIDType", + "PostgresVarBitType", + "PostgresVectorType", + "PostgresXID8Type", + "PostgresXIDType", + "PostgresXMLType", + "PostgresCidrType", ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py new file mode 100644 index 00000000..f2ea6866 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -0,0 +1,467 @@ +# src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +"""PostgreSQL-specific DataType subclasses. + +Naming convention +----------------- +PostgreSQL-specific types use the ``Postgres`` prefix to distinguish them +from the core types (which have no prefix). This avoids ambiguity when both +core and backend types are used together. + +Usage scope +----------- +These types are used **only** for PostgreSQL backend DDL column definitions, +introspection result parsing, and schema comparison. They should **not** +be used by application code directly — always use the core types for +DDL definition expressions (``ColumnDefinition.data_type``). +""" + +from __future__ import annotations + +from typing import ClassVar, Optional, Set, Tuple + +from rhosocial.activerecord.backend.expression.types import ( + BlobType, + DataType, + IntegerType, + SmallIntType, + BigIntType, +) + + +# --------------------------------------------------------------------------- +# Binary data: BYTEA +# --------------------------------------------------------------------------- + +class PostgresByteaType(BlobType): + """PostgreSQL ``BYTEA`` — variable-length binary string.""" + + def _default_sql(self) -> str: + return "BYTEA" + + +# --------------------------------------------------------------------------- +# Serial (auto-increment) types +# --------------------------------------------------------------------------- + +class PostgresSmallSerialType(DataType): + """PostgreSQL ``SMALLSERIAL`` — auto-incrementing SMALLINT (2 bytes).""" + + def _default_sql(self) -> str: + return "SMALLSERIAL" + + @classmethod + def synonyms(cls) -> Set[str]: + return {'SmallSerialType'} + + +class PostgresSerialType(DataType): + """PostgreSQL ``SERIAL`` — auto-incrementing INTEGER (4 bytes).""" + + def _default_sql(self) -> str: + return "SERIAL" + + @classmethod + def synonyms(cls) -> Set[str]: + return {'SerialType', 'IntegerType'} + + +class PostgresBigSerialType(DataType): + """PostgreSQL ``BIGSERIAL`` — auto-incrementing BIGINT (8 bytes).""" + + def _default_sql(self) -> str: + return "BIGSERIAL" + + @classmethod + def synonyms(cls) -> Set[str]: + return {'BigSerialType'} + + +# --------------------------------------------------------------------------- +# UUID +# --------------------------------------------------------------------------- + +class PostgresUUIDType(DataType): + """PostgreSQL ``UUID`` — universally unique identifier.""" + + def _default_sql(self) -> str: + return "UUID" + + +# --------------------------------------------------------------------------- +# XML +# --------------------------------------------------------------------------- + +class PostgresXMLType(DataType): + """PostgreSQL ``XML`` — XML data type.""" + + def _default_sql(self) -> str: + return "XML" + + +# --------------------------------------------------------------------------- +# Text search +# --------------------------------------------------------------------------- + +class PostgresTSVectorType(DataType): + """PostgreSQL ``TSVECTOR`` — text search document.""" + + def _default_sql(self) -> str: + return "TSVECTOR" + + +class PostgresTSQueryType(DataType): + """PostgreSQL ``TSQUERY`` — text search query.""" + + def _default_sql(self) -> str: + return "TSQUERY" + + +# --------------------------------------------------------------------------- +# JSON path +# --------------------------------------------------------------------------- + +class PostgresJsonPathType(DataType): + """PostgreSQL ``JSONPATH`` — SQL/JSON path expression (PG 12+).""" + + def _default_sql(self) -> str: + return "JSONPATH" + + +# --------------------------------------------------------------------------- +# Bit string types +# --------------------------------------------------------------------------- + +class PostgresBitType(DataType): + """PostgreSQL ``BIT(n)`` — fixed-length bit string.""" + + n: Optional[int] = None + + def __init__(self, n: Optional[int] = None): + super().__init__() + self.n = n + + def _type_params(self) -> Tuple: + return (self.n,) + + def _default_sql(self) -> str: + if self.n is not None: + return f"BIT({self.n})" + return "BIT" + + +class PostgresVarBitType(DataType): + """PostgreSQL ``VARBIT(n)`` — variable-length bit string.""" + + n: Optional[int] = None + + def __init__(self, n: Optional[int] = None): + super().__init__() + self.n = n + + def _type_params(self) -> Tuple: + return (self.n,) + + def _default_sql(self) -> str: + if self.n is not None: + return f"VARBIT({self.n})" + return "VARBIT" + + +# --------------------------------------------------------------------------- +# Network address types +# --------------------------------------------------------------------------- + +class PostgresInetType(DataType): + """PostgreSQL ``INET`` — IPv4 or IPv6 address.""" + + def _default_sql(self) -> str: + return "INET" + + +class PostgresCidrType(DataType): + """PostgreSQL ``CIDR`` — IPv4 or IPv6 network.""" + + def _default_sql(self) -> str: + return "CIDR" + + +class PostgresMacAddrType(DataType): + """PostgreSQL ``MACADDR`` — MAC address (EUI-48).""" + + def _default_sql(self) -> str: + return "MACADDR" + + +class PostgresMacAddr8Type(DataType): + """PostgreSQL ``MACADDR8`` — MAC address (EUI-64, PG 10+).""" + + def _default_sql(self) -> str: + return "MACADDR8" + + +# --------------------------------------------------------------------------- +# Geometric types +# --------------------------------------------------------------------------- + +class PostgresPointType(DataType): + """PostgreSQL ``POINT`` — geometric point (x, y).""" + + def _default_sql(self) -> str: + return "POINT" + + +class PostgresLineType(DataType): + """PostgreSQL ``LINE`` — infinite line.""" + + def _default_sql(self) -> str: + return "LINE" + + +class PostgresLineSegmentType(DataType): + """PostgreSQL ``LSEG`` — line segment.""" + + def _default_sql(self) -> str: + return "LSEG" + + +class PostgresBoxType(DataType): + """PostgreSQL ``BOX`` — rectangular box.""" + + def _default_sql(self) -> str: + return "BOX" + + +class PostgresPathType(DataType): + """PostgreSQL ``PATH`` — open or closed geometric path.""" + + def _default_sql(self) -> str: + return "PATH" + + +class PostgresPolygonType(DataType): + """PostgreSQL ``POLYGON`` — closed geometric polygon.""" + + def _default_sql(self) -> str: + return "POLYGON" + + +class PostgresCircleType(DataType): + """PostgreSQL ``CIRCLE`` — circle (center + radius).""" + + def _default_sql(self) -> str: + return "CIRCLE" + + +# --------------------------------------------------------------------------- +# Monetary type +# --------------------------------------------------------------------------- + +class PostgresMoneyType(DataType): + """PostgreSQL ``MONEY`` — currency amount.""" + + def _default_sql(self) -> str: + return "MONEY" + + +# --------------------------------------------------------------------------- +# Range types +# --------------------------------------------------------------------------- + +class PostgresInt4RangeType(DataType): + """PostgreSQL ``INT4RANGE`` — range of integer.""" + + def _default_sql(self) -> str: + return "INT4RANGE" + + +class PostgresInt8RangeType(DataType): + """PostgreSQL ``INT8RANGE`` — range of bigint.""" + + def _default_sql(self) -> str: + return "INT8RANGE" + + +class PostgresNumRangeType(DataType): + """PostgreSQL ``NUMRANGE`` — range of numeric.""" + + def _default_sql(self) -> str: + return "NUMRANGE" + + +class PostgresTsRangeType(DataType): + """PostgreSQL ``TSRANGE`` — range of timestamp without time zone.""" + + def _default_sql(self) -> str: + return "TSRANGE" + + +class PostgresTsTzRangeType(DataType): + """PostgreSQL ``TSTZRANGE`` — range of timestamp with time zone.""" + + def _default_sql(self) -> str: + return "TSTZRANGE" + + +class PostgresDateRangeType(DataType): + """PostgreSQL ``DATERANGE`` — range of date.""" + + def _default_sql(self) -> str: + return "DATERANGE" + + +# --------------------------------------------------------------------------- +# Multirange types (PG 14+) +# --------------------------------------------------------------------------- + +class PostgresInt4MultirangeType(DataType): + """PostgreSQL ``INT4MULTIRANGE`` — multirange of integer (PG 14+).""" + + def _default_sql(self) -> str: + return "INT4MULTIRANGE" + + +class PostgresInt8MultirangeType(DataType): + """PostgreSQL ``INT8MULTIRANGE`` — multirange of bigint (PG 14+).""" + + def _default_sql(self) -> str: + return "INT8MULTIRANGE" + + +class PostgresNumMultirangeType(DataType): + """PostgreSQL ``NUMMULTIRANGE`` — multirange of numeric (PG 14+).""" + + def _default_sql(self) -> str: + return "NUMMULTIRANGE" + + +class PostgresTsMultirangeType(DataType): + """PostgreSQL ``TSMULTIRANGE`` — multirange of timestamp (PG 14+).""" + + def _default_sql(self) -> str: + return "TSMULTIRANGE" + + +class PostgresTsTzMultirangeType(DataType): + """PostgreSQL ``TSTZMULTIRANGE`` — multirange of timestamptz (PG 14+).""" + + def _default_sql(self) -> str: + return "TSTZMULTIRANGE" + + +class PostgresDateMultirangeType(DataType): + """PostgreSQL ``DATEMULTIRANGE`` — multirange of date (PG 14+).""" + + def _default_sql(self) -> str: + return "DATEMULTIRANGE" + + +# --------------------------------------------------------------------------- +# Object identifier types +# --------------------------------------------------------------------------- + +class PostgresOIDType(DataType): + """PostgreSQL ``OID`` — object identifier.""" + + def _default_sql(self) -> str: + return "OID" + + +class PostgresRegClassType(DataType): + """PostgreSQL ``REGCLASS`` — relation name (OID alias).""" + + def _default_sql(self) -> str: + return "REGCLASS" + + +class PostgresRegTypeType(DataType): + """PostgreSQL ``REGTYPE`` — type name (OID alias).""" + + def _default_sql(self) -> str: + return "REGTYPE" + + +class PostgresXIDType(DataType): + """PostgreSQL ``XID`` — transaction ID.""" + + def _default_sql(self) -> str: + return "XID" + + +class PostgresXID8Type(DataType): + """PostgreSQL ``XID8`` — 64-bit transaction ID (PG 13+).""" + + def _default_sql(self) -> str: + return "XID8" + + +class PostgresCIDType(DataType): + """PostgreSQL ``CID`` — command ID.""" + + def _default_sql(self) -> str: + return "CID" + + +class PostgresTIDType(DataType): + """PostgreSQL ``TID`` — tuple ID (page, tuple).""" + + def _default_sql(self) -> str: + return "TID" + + +# --------------------------------------------------------------------------- +# pg_lsn type +# --------------------------------------------------------------------------- + +class PostgresPgLSNType(DataType): + """PostgreSQL ``PG_LSN`` — WAL log sequence number.""" + + def _default_sql(self) -> str: + return "PG_LSN" + + +# --------------------------------------------------------------------------- +# Extension-provided types (minimal DataType wrappers) +# --------------------------------------------------------------------------- + +class PostgresHstoreType(DataType): + """PostgreSQL ``HSTORE`` — key-value store (hstore extension).""" + + def _default_sql(self) -> str: + return "HSTORE" + + +class PostgresGeometryType(DataType): + """PostGIS ``GEOMETRY`` — generic spatial geometry (PostGIS extension). + + For production use, subclass with SRID support as needed. + """ + + def _default_sql(self) -> str: + return "GEOMETRY" + + +class PostgresGeographyType(DataType): + """PostGIS ``GEOGRAPHY`` — geodetic spatial type (PostGIS extension).""" + + def _default_sql(self) -> str: + return "GEOGRAPHY" + + +class PostgresVectorType(DataType): + """pgvector ``VECTOR(n)`` — vector embedding (pgvector extension). + + Args: + dim: Number of dimensions. + """ + + dim: int + + def __init__(self, dim: int): + super().__init__() + self.dim = dim + + def _type_params(self) -> Tuple: + return (self.dim,) + + def _default_sql(self) -> str: + return f"VECTOR({self.dim})" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py index 232e1805..77981ee9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/__init__.py @@ -38,6 +38,7 @@ from .types.range_type import PostgresRangeTypeMixin from .types.jsonb_enhanced import PostgresJSONBEnhancedMixin from .types.array_enhanced import PostgresArrayEnhancedMixin +from .types.data_type_formatting import PostgresTypeFormatSupportMixin # Extension mixins from .extensions.pgvector import PostgresPgvectorMixin @@ -129,6 +130,7 @@ "PostgresRangeTypeMixin", "PostgresJSONBEnhancedMixin", "PostgresArrayEnhancedMixin", + "PostgresTypeFormatSupportMixin", # Extension mixins "PostgresPgvectorMixin", "PostgresPostGISMixin", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/__init__.py index a223f6b2..0d877803 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/__init__.py @@ -8,6 +8,7 @@ from .range_type import PostgresRangeTypeMixin from .jsonb_enhanced import PostgresJSONBEnhancedMixin from .array_enhanced import PostgresArrayEnhancedMixin +from .data_type_formatting import PostgresTypeFormatSupportMixin __all__ = [ "EnumTypeMixin", @@ -17,4 +18,5 @@ "PostgresRangeTypeMixin", "PostgresJSONBEnhancedMixin", "PostgresArrayEnhancedMixin", + "PostgresTypeFormatSupportMixin", ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py new file mode 100644 index 00000000..b0477b53 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -0,0 +1,760 @@ +# src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +"""PostgreSQL DataType formatting and parsing mixin.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, List, Tuple, Type + +from rhosocial.activerecord.backend.dialect.protocols import ( + TypeFormattingSupport, + TypeParsingSupport, +) +from rhosocial.activerecord.backend.expression.types import ( + BigIntType, + BlobType, + BooleanType, + CharType, + DataType, + DateType, + DateTimeType, + DecimalType, + DoubleType, + FloatType, + IntegerType, + IntType, + IntervalType, + JsonBType, + JsonType, + RealType, + SmallIntType, + TextType, + TimeType, + TimeTzType, + TimestampType, + TimestampTzType, + TinyIntType, + VarCharType, +) + +if TYPE_CHECKING: + from ..expression.types import ( + PostgresBigSerialType, + PostgresBitType, + PostgresBoxType, + PostgresByteaType, + PostgresCIDType, + PostgresCircleType, + PostgresDateMultirangeType, + PostgresDateRangeType, + PostgresGeographyType, + PostgresGeometryType, + PostgresHstoreType, + PostgresInetType, + PostgresInt4MultirangeType, + PostgresInt4RangeType, + PostgresInt8MultirangeType, + PostgresInt8RangeType, + PostgresJsonPathType, + PostgresLineSegmentType, + PostgresLineType, + PostgresMacAddr8Type, + PostgresMacAddrType, + PostgresMoneyType, + PostgresNumMultirangeType, + PostgresNumRangeType, + PostgresOIDType, + PostgresPathType, + PostgresPgLSNType, + PostgresPointType, + PostgresPolygonType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresTIDType, + PostgresTSQueryType, + PostgresTSVectorType, + PostgresTsMultirangeType, + PostgresTsRangeType, + PostgresTsTzMultirangeType, + PostgresTsTzRangeType, + PostgresUUIDType, + PostgresVarBitType, + PostgresVectorType, + PostgresXID8Type, + PostgresXIDType, + PostgresXMLType, + PostgresCidrType, + ) + + +class PostgresTypeFormatSupportMixin(TypeFormattingSupport, TypeParsingSupport): + """PostgreSQL DataType formatting and parsing. + + Implements both ``TypeFormattingSupport`` and ``TypeParsingSupport`` so + the dialect can render ``DataType`` expressions to SQL strings and parse + raw SQL type strings back into ``DataType`` instances. + """ + + # ------------------------------------------------------------------ + # TypeFormattingSupport + # ------------------------------------------------------------------ + + def render_type(self, data_type: DataType) -> str: + for type_class, suffix in self.supports_data_types(): + if isinstance(data_type, type_class): + formatter = getattr(self, f"format_data_type_{suffix}", None) + if formatter is not None: + return formatter(data_type) + return data_type._default_sql() + + def supports_data_types(self) -> List[Tuple[Type[DataType], str]]: + return [ + # PostgreSQL-specific types + # Binary + (PostgresByteaType, "bytea"), + # Serial family + (PostgresSmallSerialType, "small_serial"), + (PostgresSerialType, "serial"), + (PostgresBigSerialType, "big_serial"), + # UUID / XML + (PostgresUUIDType, "uuid"), + (PostgresXMLType, "xml"), + # Text search + (PostgresTSVectorType, "ts_vector"), + (PostgresTSQueryType, "ts_query"), + # JSON path + (PostgresJsonPathType, "json_path"), + # Bit string + (PostgresBitType, "bit"), + (PostgresVarBitType, "var_bit"), + # Network address + (PostgresInetType, "inet"), + (PostgresCidrType, "cidr"), + (PostgresMacAddrType, "mac_addr"), + (PostgresMacAddr8Type, "mac_addr8"), + # Geometric + (PostgresPointType, "point"), + (PostgresLineType, "line"), + (PostgresLineSegmentType, "line_segment"), + (PostgresBoxType, "box"), + (PostgresPathType, "path"), + (PostgresPolygonType, "polygon"), + (PostgresCircleType, "circle"), + # Monetary + (PostgresMoneyType, "money"), + # Range + (PostgresInt4RangeType, "int4_range"), + (PostgresInt8RangeType, "int8_range"), + (PostgresNumRangeType, "num_range"), + (PostgresTsRangeType, "ts_range"), + (PostgresTsTzRangeType, "ts_tz_range"), + (PostgresDateRangeType, "date_range"), + # Multirange + (PostgresInt4MultirangeType, "int4_multirange"), + (PostgresInt8MultirangeType, "int8_multirange"), + (PostgresNumMultirangeType, "num_multirange"), + (PostgresTsMultirangeType, "ts_multirange"), + (PostgresTsTzMultirangeType, "ts_tz_multirange"), + (PostgresDateMultirangeType, "date_multirange"), + # Object identifier + (PostgresOIDType, "oid"), + (PostgresRegClassType, "reg_class"), + (PostgresRegTypeType, "reg_type"), + (PostgresXIDType, "xid"), + (PostgresXID8Type, "xid8"), + (PostgresCIDType, "cid"), + (PostgresTIDType, "tid"), + # pg_lsn + (PostgresPgLSNType, "pg_lsn"), + # Extension types + (PostgresHstoreType, "hstore"), + (PostgresGeometryType, "geometry"), + (PostgresGeographyType, "geography"), + (PostgresVectorType, "vector"), + # Core Integer family + (TinyIntType, "tiny_int"), + (SmallIntType, "small_int"), + (IntType, "int"), + (IntegerType, "integer"), + (BigIntType, "big_int"), + # Core Numeric family + (FloatType, "float"), + (RealType, "real"), + (DoubleType, "double"), + (DecimalType, "decimal"), + # Core String family + (CharType, "char"), + (VarCharType, "var_char"), + (TextType, "text"), + # Boolean + (BooleanType, "boolean"), + # Binary + (BlobType, "blob"), + # Date/time + (DateType, "date"), + (TimeType, "time"), + (TimeTzType, "time_tz"), + (DateTimeType, "date_time"), + (TimestampType, "timestamp"), + (TimestampTzType, "timestamp_tz"), + (IntervalType, "interval"), + # JSON + (JsonType, "json"), + (JsonBType, "json_b"), + ] + + # --- PostgreSQL-specific formatters --- + + def format_data_type_bytea(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_small_serial(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_serial(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_big_serial(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_uuid(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_xml(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_ts_vector(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_ts_query(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_json_path(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_bit(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_var_bit(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_inet(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_cidr(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_mac_addr(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_mac_addr8(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_point(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_line(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_line_segment(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_box(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_path(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_polygon(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_circle(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_money(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_int4_range(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_int8_range(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_num_range(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_ts_range(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_ts_tz_range(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_date_range(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_int4_multirange(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_int8_multirange(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_num_multirange(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_ts_multirange(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_ts_tz_multirange(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_date_multirange(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_oid(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_reg_class(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_reg_type(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_xid(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_xid8(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_cid(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_tid(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_pg_lsn(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_hstore(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_geometry(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_geography(self, data_type) -> str: + return data_type._default_sql() + + def format_data_type_vector(self, data_type) -> str: + return data_type._default_sql() + + # --- Core formatters (PostgreSQL specialized) --- + + def format_data_type_tiny_int(self, data_type: TinyIntType) -> str: + return "SMALLINT" + + def format_data_type_small_int(self, data_type: SmallIntType) -> str: + return data_type._default_sql() + + def format_data_type_int(self, data_type: IntType) -> str: + return data_type._default_sql() + + def format_data_type_integer(self, data_type: IntegerType) -> str: + return data_type._default_sql() + + def format_data_type_big_int(self, data_type: BigIntType) -> str: + return data_type._default_sql() + + def format_data_type_float(self, data_type: FloatType) -> str: + return data_type._default_sql() + + def format_data_type_real(self, data_type: RealType) -> str: + return data_type._default_sql() + + def format_data_type_double(self, data_type: DoubleType) -> str: + return data_type._default_sql() + + def format_data_type_decimal(self, data_type: DecimalType) -> str: + return data_type._default_sql() + + def format_data_type_char(self, data_type: CharType) -> str: + return data_type._default_sql() + + def format_data_type_var_char(self, data_type: VarCharType) -> str: + return data_type._default_sql() + + def format_data_type_text(self, data_type: TextType) -> str: + return data_type._default_sql() + + def format_data_type_boolean(self, data_type: BooleanType) -> str: + return data_type._default_sql() + + def format_data_type_blob(self, data_type: BlobType) -> str: + return "BYTEA" + + def format_data_type_date(self, data_type: DateType) -> str: + return data_type._default_sql() + + def format_data_type_time(self, data_type: TimeType) -> str: + return data_type._default_sql() + + def format_data_type_time_tz(self, data_type: TimeTzType) -> str: + return data_type._default_sql() + + def format_data_type_date_time(self, data_type: DateTimeType) -> str: + return "TIMESTAMP" + + def format_data_type_timestamp(self, data_type: TimestampType) -> str: + return data_type._default_sql() + + def format_data_type_timestamp_tz(self, data_type: TimestampTzType) -> str: + return data_type._default_sql() + + def format_data_type_interval(self, data_type: IntervalType) -> str: + return data_type._default_sql() + + def format_data_type_json(self, data_type: JsonType) -> str: + return "JSON" + + def format_data_type_json_b(self, data_type: JsonBType) -> str: + return "JSONB" + + # ------------------------------------------------------------------ + # TypeParsingSupport + # ------------------------------------------------------------------ + + _PG_INTEGER_TYPES = re.compile( + r"^(?:SMALLINT|INT2|INT|INTEGER|INT4|BIGINT|INT8)\b", + re.IGNORECASE, + ) + _PG_SERIAL_TYPES = re.compile( + r"^(?:SMALLSERIAL|SERIAL2|SERIAL|SERIAL4|BIGSERIAL|SERIAL8)\b", + re.IGNORECASE, + ) + _PG_FLOAT_TYPES = re.compile( + r"^(?:REAL|FLOAT4|FLOAT|DOUBLE)\b", + re.IGNORECASE, + ) + _PG_DECIMAL_TYPES = re.compile( + r"^(?:DECIMAL|NUMERIC)\b", + re.IGNORECASE, + ) + _PG_STRING_TYPES = re.compile( + r"^(?:CHAR|VARCHAR|TEXT)\b", + re.IGNORECASE, + ) + _PG_BINARY_TYPES = re.compile( + r"^(?:BYTEA|BLOB)\b", + re.IGNORECASE, + ) + _PG_DATE_TYPES = re.compile( + r"^(?:DATE|DATETIME|TIMESTAMP|TIME|INTERVAL)\b", + re.IGNORECASE, + ) + _PG_JSON_TYPES = re.compile( + r"^(?:JSON|JSONB|JSONPATH)\b", + re.IGNORECASE, + ) + _PG_UUID_TYPES = re.compile( + r"^(?:UUID)\b", + re.IGNORECASE, + ) + _PG_NET_TYPES = re.compile( + r"^(?:INET|CIDR|MACADDR|MACADDR8)\b", + re.IGNORECASE, + ) + _PG_GEOM_TYPES = re.compile( + r"^(?:POINT|LINE|LSEG|BOX|PATH|POLYGON|CIRCLE)\b", + re.IGNORECASE, + ) + _PG_BIT_TYPES = re.compile( + r"^(?:BIT|VARBIT)\b", + re.IGNORECASE, + ) + _PG_RANGE_TYPES = re.compile( + r"^(?:INT4RANGE|INT8RANGE|NUMRANGE|TSRANGE|TSTZRANGE|DATERANGE)\b", + re.IGNORECASE, + ) + _PG_MULTIRANGE_TYPES = re.compile( + r"^(?:INT4MULTIRANGE|INT8MULTIRANGE|NUMMULTIRANGE|" + r"TSMULTIRANGE|TSTZMULTIRANGE|DATEMULTIRANGE)\b", + re.IGNORECASE, + ) + _PG_OID_TYPES = re.compile( + r"^(?:OID|REGCLASS|REGTYPE|XID|XID8|CID|TID)\b", + re.IGNORECASE, + ) + _PG_TS_TYPES = re.compile( + r"^(?:TSVECTOR|TSQUERY)\b", + re.IGNORECASE, + ) + _PG_MISC_TYPES = re.compile( + r"^(?:MONEY|XML|PG_LSN|HSTORE|GEOMETRY|GEOGRAPHY)\b", + re.IGNORECASE, + ) + + def parse_type(self, raw: str) -> DataType: + stripped = raw.strip() + upper = stripped.upper() + + # Serial family + if self._PG_SERIAL_TYPES.match(upper): + from ..expression.types import ( + PostgresBigSerialType, + PostgresSerialType, + PostgresSmallSerialType, + ) + if upper.startswith("BIGSERIAL") or upper.startswith("SERIAL8"): + return PostgresBigSerialType() + if upper.startswith("SMALLSERIAL") or upper.startswith("SERIAL2"): + return PostgresSmallSerialType() + return PostgresSerialType() + + # Integer family + if self._PG_INTEGER_TYPES.match(upper): + if upper.startswith("SMALLINT") or upper.startswith("INT2"): + return SmallIntType() + if upper.startswith("BIGINT") or upper.startswith("INT8"): + return BigIntType() + return IntegerType() + + # Float family + if self._PG_FLOAT_TYPES.match(upper): + if upper.startswith("DOUBLE"): + return DoubleType() + if upper.startswith("REAL") or upper.startswith("FLOAT4"): + return RealType() + nums = re.findall(r"\d+", stripped) + precision = int(nums[0]) if nums else None + return FloatType(precision) + + # Decimal family + if self._PG_DECIMAL_TYPES.match(upper): + nums = re.findall(r"\d+", stripped) + if len(nums) >= 2: + return DecimalType(int(nums[0]), int(nums[1])) + if len(nums) == 1: + return DecimalType(int(nums[0])) + return DecimalType() + + # String family + if self._PG_STRING_TYPES.match(upper): + length_match = re.search(r"\((\d+)\)", stripped) + length = int(length_match.group(1)) if length_match else None + if upper.startswith("VARCHAR") or upper.startswith("CHARACTER VARYING"): + return VarCharType(length) + if upper.startswith("CHAR") or upper.startswith("CHARACTER"): + return CharType(length) + return TextType() + + # Binary + if self._PG_BINARY_TYPES.match(upper): + from ..expression.types import PostgresByteaType + return PostgresByteaType() + + # Date/time + if self._PG_DATE_TYPES.match(upper): + if upper.startswith("DATE"): + return DateType() + if upper.startswith("DATETIME"): + return DateTimeType() + if upper.startswith("TIMESTAMP"): + nums = re.findall(r"\d+", stripped) + precision = int(nums[0]) if nums else None + if "WITH TIME ZONE" in upper or upper.startswith("TIMESTAMPTZ"): + return TimestampTzType(precision) + return TimestampType(precision) + if upper.startswith("TIME"): + nums = re.findall(r"\d+", stripped) + precision = int(nums[0]) if nums else None + if "WITH TIME ZONE" in upper or upper.startswith("TIMETZ"): + return TimeTzType(precision) + return TimeType(precision) + if upper.startswith("INTERVAL"): + fields_match = re.search(r"INTERVAL\s+(.*)", upper) + fields = fields_match.group(1).strip() if fields_match else None + return IntervalType(fields) + + # JSON + if self._PG_JSON_TYPES.match(upper): + if upper.startswith("JSONB"): + return JsonBType() + if upper.startswith("JSONPATH"): + from ..expression.types import PostgresJsonPathType + return PostgresJsonPathType() + return JsonType() + + # UUID + if self._PG_UUID_TYPES.match(upper): + from ..expression.types import PostgresUUIDType + return PostgresUUIDType() + + # Boolean + if upper.startswith("BOOLEAN") or upper.startswith("BOOL"): + return BooleanType() + + # Bit string + if self._PG_BIT_TYPES.match(upper): + nums = re.findall(r"\d+", stripped) + n = int(nums[0]) if nums else None + if upper.startswith("BIT"): + from ..expression.types import PostgresBitType + return PostgresBitType(n) + from ..expression.types import PostgresVarBitType + return PostgresVarBitType(n) + + # Network address + if self._PG_NET_TYPES.match(upper): + from ..expression.types import ( + PostgresCidrType, + PostgresInetType, + PostgresMacAddr8Type, + PostgresMacAddrType, + ) + if upper.startswith("CIDR"): + return PostgresCidrType() + if upper.startswith("MACADDR8"): + return PostgresMacAddr8Type() + if upper.startswith("MACADDR"): + return PostgresMacAddrType() + return PostgresInetType() + + # Geometric + if self._PG_GEOM_TYPES.match(upper): + from ..expression.types import ( + PostgresBoxType, + PostgresCircleType, + PostgresLineSegmentType, + PostgresLineType, + PostgresPathType, + PostgresPointType, + PostgresPolygonType, + ) + geom_map = { + "POINT": PostgresPointType, + "LINE": PostgresLineType, + "LSEG": PostgresLineSegmentType, + "BOX": PostgresBoxType, + "PATH": PostgresPathType, + "POLYGON": PostgresPolygonType, + "CIRCLE": PostgresCircleType, + } + for name, cls in geom_map.items(): + if upper.startswith(name): + return cls() + return PostgresPointType() + + # Range types + if self._PG_RANGE_TYPES.match(upper): + from ..expression.types import ( + PostgresDateRangeType, + PostgresInt4RangeType, + PostgresInt8RangeType, + PostgresNumRangeType, + PostgresTsRangeType, + PostgresTsTzRangeType, + ) + range_map = { + "INT4RANGE": PostgresInt4RangeType, + "INT8RANGE": PostgresInt8RangeType, + "NUMRANGE": PostgresNumRangeType, + "TSRANGE": PostgresTsRangeType, + "TSTZRANGE": PostgresTsTzRangeType, + "DATERANGE": PostgresDateRangeType, + } + for name, cls in range_map.items(): + if upper.startswith(name): + return cls() + return PostgresInt4RangeType() + + # Multirange types + if self._PG_MULTIRANGE_TYPES.match(upper): + from ..expression.types import ( + PostgresDateMultirangeType, + PostgresInt4MultirangeType, + PostgresInt8MultirangeType, + PostgresNumMultirangeType, + PostgresTsMultirangeType, + PostgresTsTzMultirangeType, + ) + mr_map = { + "INT4MULTIRANGE": PostgresInt4MultirangeType, + "INT8MULTIRANGE": PostgresInt8MultirangeType, + "NUMMULTIRANGE": PostgresNumMultirangeType, + "TSMULTIRANGE": PostgresTsMultirangeType, + "TSTZMULTIRANGE": PostgresTsTzMultirangeType, + "DATEMULTIRANGE": PostgresDateMultirangeType, + } + for name, cls in mr_map.items(): + if upper.startswith(name): + return cls() + return PostgresInt4MultirangeType() + + # OID types + if self._PG_OID_TYPES.match(upper): + from ..expression.types import ( + PostgresCIDType, + PostgresOIDType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresTIDType, + PostgresXID8Type, + PostgresXIDType, + ) + oid_map = { + "OID": PostgresOIDType, + "REGCLASS": PostgresRegClassType, + "REGTYPE": PostgresRegTypeType, + "XID8": PostgresXID8Type, + "XID": PostgresXIDType, + "CID": PostgresCIDType, + "TID": PostgresTIDType, + } + for name, cls in oid_map.items(): + if upper.startswith(name): + return cls() + return PostgresOIDType() + + # Text search + if self._PG_TS_TYPES.match(upper): + from ..expression.types import ( + PostgresTSQueryType, + PostgresTSVectorType, + ) + if upper.startswith("TSVECTOR"): + return PostgresTSVectorType() + return PostgresTSQueryType() + + # Miscellaneous + if self._PG_MISC_TYPES.match(upper): + from ..expression.types import ( + PostgresGeographyType, + PostgresGeometryType, + PostgresHstoreType, + PostgresMoneyType, + PostgresPgLSNType, + PostgresXMLType, + ) + if upper.startswith("MONEY"): + return PostgresMoneyType() + if upper.startswith("XML"): + return PostgresXMLType() + if upper.startswith("PG_LSN"): + return PostgresPgLSNType() + if upper.startswith("HSTORE"): + return PostgresHstoreType() + if upper.startswith("GEOGRAPHY"): + return PostgresGeographyType() + if upper.startswith("GEOMETRY"): + return PostgresGeometryType() + + # Vector type (pgvector) + if upper.startswith("VECTOR"): + nums = re.findall(r"\d+", stripped) + dim = int(nums[0]) if nums else 0 + from ..expression.types import PostgresVectorType + return PostgresVectorType(dim) + + # Fallback + from rhosocial.activerecord.backend.expression.types import CustomType + return CustomType(stripped) From 493b3b18173d7c0e0d4ccdb61652c35564e74ed5 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 11:55:38 +0800 Subject: [PATCH 02/22] refactor(types): add __eq__/__hash__, remove synonyms, add CharacterVaryingType - Replace _type_params() with __eq__/__hash__ in PostgresBitType, PostgresVarBitType, PostgresVectorType - Remove synonyms() from PostgresSmallSerialType, PostgresSerialType, PostgresBigSerialType - Add PostgresCharacterVaryingType(VarCharType) for CHARACTER VARYING(n) - Register new type in data_type_formatting mixin - Clean up unused imports (ClassVar, Set, Tuple) --- .../impl/postgres/expression/__init__.py | 2 + .../backend/impl/postgres/expression/types.py | 149 ++++++++++-------- .../mixins/types/data_type_formatting.py | 6 + 3 files changed, 91 insertions(+), 66 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py index dfb3ba27..14200826 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/__init__.py @@ -53,6 +53,7 @@ PostgresBitType, PostgresBoxType, PostgresByteaType, + PostgresCharacterVaryingType, PostgresCIDType, PostgresCircleType, PostgresDateMultirangeType, @@ -138,6 +139,7 @@ "PostgresBitType", "PostgresBoxType", "PostgresByteaType", + "PostgresCharacterVaryingType", "PostgresCIDType", "PostgresCircleType", "PostgresDateMultirangeType", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py index f2ea6866..4113f8b0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -17,7 +17,7 @@ from __future__ import annotations -from typing import ClassVar, Optional, Set, Tuple +from typing import Optional from rhosocial.activerecord.backend.expression.types import ( BlobType, @@ -25,14 +25,28 @@ IntegerType, SmallIntType, BigIntType, + VarCharType, ) +# --------------------------------------------------------------------------- +# Character varying alias +# --------------------------------------------------------------------------- + +class PostgresCharacterVaryingType(VarCharType, backend="postgres"): + """PostgreSQL ``CHARACTER VARYING(n)`` — alias for ``VARCHAR(n)``.""" + + def _default_sql(self) -> str: + if self.length is not None: + return f"CHARACTER VARYING({self.length})" + return "CHARACTER VARYING" + + # --------------------------------------------------------------------------- # Binary data: BYTEA # --------------------------------------------------------------------------- -class PostgresByteaType(BlobType): +class PostgresByteaType(BlobType, backend="postgres"): """PostgreSQL ``BYTEA`` — variable-length binary string.""" def _default_sql(self) -> str: @@ -43,44 +57,32 @@ def _default_sql(self) -> str: # Serial (auto-increment) types # --------------------------------------------------------------------------- -class PostgresSmallSerialType(DataType): +class PostgresSmallSerialType(DataType, backend="postgres"): """PostgreSQL ``SMALLSERIAL`` — auto-incrementing SMALLINT (2 bytes).""" def _default_sql(self) -> str: return "SMALLSERIAL" - @classmethod - def synonyms(cls) -> Set[str]: - return {'SmallSerialType'} - -class PostgresSerialType(DataType): +class PostgresSerialType(DataType, backend="postgres"): """PostgreSQL ``SERIAL`` — auto-incrementing INTEGER (4 bytes).""" def _default_sql(self) -> str: return "SERIAL" - @classmethod - def synonyms(cls) -> Set[str]: - return {'SerialType', 'IntegerType'} - -class PostgresBigSerialType(DataType): +class PostgresBigSerialType(DataType, backend="postgres"): """PostgreSQL ``BIGSERIAL`` — auto-incrementing BIGINT (8 bytes).""" def _default_sql(self) -> str: return "BIGSERIAL" - @classmethod - def synonyms(cls) -> Set[str]: - return {'BigSerialType'} - # --------------------------------------------------------------------------- # UUID # --------------------------------------------------------------------------- -class PostgresUUIDType(DataType): +class PostgresUUIDType(DataType, backend="postgres"): """PostgreSQL ``UUID`` — universally unique identifier.""" def _default_sql(self) -> str: @@ -91,7 +93,7 @@ def _default_sql(self) -> str: # XML # --------------------------------------------------------------------------- -class PostgresXMLType(DataType): +class PostgresXMLType(DataType, backend="postgres"): """PostgreSQL ``XML`` — XML data type.""" def _default_sql(self) -> str: @@ -102,14 +104,14 @@ def _default_sql(self) -> str: # Text search # --------------------------------------------------------------------------- -class PostgresTSVectorType(DataType): +class PostgresTSVectorType(DataType, backend="postgres"): """PostgreSQL ``TSVECTOR`` — text search document.""" def _default_sql(self) -> str: return "TSVECTOR" -class PostgresTSQueryType(DataType): +class PostgresTSQueryType(DataType, backend="postgres"): """PostgreSQL ``TSQUERY`` — text search query.""" def _default_sql(self) -> str: @@ -120,7 +122,7 @@ def _default_sql(self) -> str: # JSON path # --------------------------------------------------------------------------- -class PostgresJsonPathType(DataType): +class PostgresJsonPathType(DataType, backend="postgres"): """PostgreSQL ``JSONPATH`` — SQL/JSON path expression (PG 12+).""" def _default_sql(self) -> str: @@ -131,7 +133,7 @@ def _default_sql(self) -> str: # Bit string types # --------------------------------------------------------------------------- -class PostgresBitType(DataType): +class PostgresBitType(DataType, backend="postgres"): """PostgreSQL ``BIT(n)`` — fixed-length bit string.""" n: Optional[int] = None @@ -140,8 +142,13 @@ def __init__(self, n: Optional[int] = None): super().__init__() self.n = n - def _type_params(self) -> Tuple: - return (self.n,) + def __eq__(self, other: object) -> bool: + if type(self) is not type(other): + return False + return self.n == other.n + + def __hash__(self) -> int: + return hash((type(self), self.n)) def _default_sql(self) -> str: if self.n is not None: @@ -149,7 +156,7 @@ def _default_sql(self) -> str: return "BIT" -class PostgresVarBitType(DataType): +class PostgresVarBitType(DataType, backend="postgres"): """PostgreSQL ``VARBIT(n)`` — variable-length bit string.""" n: Optional[int] = None @@ -158,8 +165,13 @@ def __init__(self, n: Optional[int] = None): super().__init__() self.n = n - def _type_params(self) -> Tuple: - return (self.n,) + def __eq__(self, other: object) -> bool: + if type(self) is not type(other): + return False + return self.n == other.n + + def __hash__(self) -> int: + return hash((type(self), self.n)) def _default_sql(self) -> str: if self.n is not None: @@ -171,28 +183,28 @@ def _default_sql(self) -> str: # Network address types # --------------------------------------------------------------------------- -class PostgresInetType(DataType): +class PostgresInetType(DataType, backend="postgres"): """PostgreSQL ``INET`` — IPv4 or IPv6 address.""" def _default_sql(self) -> str: return "INET" -class PostgresCidrType(DataType): +class PostgresCidrType(DataType, backend="postgres"): """PostgreSQL ``CIDR`` — IPv4 or IPv6 network.""" def _default_sql(self) -> str: return "CIDR" -class PostgresMacAddrType(DataType): +class PostgresMacAddrType(DataType, backend="postgres"): """PostgreSQL ``MACADDR`` — MAC address (EUI-48).""" def _default_sql(self) -> str: return "MACADDR" -class PostgresMacAddr8Type(DataType): +class PostgresMacAddr8Type(DataType, backend="postgres"): """PostgreSQL ``MACADDR8`` — MAC address (EUI-64, PG 10+).""" def _default_sql(self) -> str: @@ -203,49 +215,49 @@ def _default_sql(self) -> str: # Geometric types # --------------------------------------------------------------------------- -class PostgresPointType(DataType): +class PostgresPointType(DataType, backend="postgres"): """PostgreSQL ``POINT`` — geometric point (x, y).""" def _default_sql(self) -> str: return "POINT" -class PostgresLineType(DataType): +class PostgresLineType(DataType, backend="postgres"): """PostgreSQL ``LINE`` — infinite line.""" def _default_sql(self) -> str: return "LINE" -class PostgresLineSegmentType(DataType): +class PostgresLineSegmentType(DataType, backend="postgres"): """PostgreSQL ``LSEG`` — line segment.""" def _default_sql(self) -> str: return "LSEG" -class PostgresBoxType(DataType): +class PostgresBoxType(DataType, backend="postgres"): """PostgreSQL ``BOX`` — rectangular box.""" def _default_sql(self) -> str: return "BOX" -class PostgresPathType(DataType): +class PostgresPathType(DataType, backend="postgres"): """PostgreSQL ``PATH`` — open or closed geometric path.""" def _default_sql(self) -> str: return "PATH" -class PostgresPolygonType(DataType): +class PostgresPolygonType(DataType, backend="postgres"): """PostgreSQL ``POLYGON`` — closed geometric polygon.""" def _default_sql(self) -> str: return "POLYGON" -class PostgresCircleType(DataType): +class PostgresCircleType(DataType, backend="postgres"): """PostgreSQL ``CIRCLE`` — circle (center + radius).""" def _default_sql(self) -> str: @@ -256,7 +268,7 @@ def _default_sql(self) -> str: # Monetary type # --------------------------------------------------------------------------- -class PostgresMoneyType(DataType): +class PostgresMoneyType(DataType, backend="postgres"): """PostgreSQL ``MONEY`` — currency amount.""" def _default_sql(self) -> str: @@ -267,42 +279,42 @@ def _default_sql(self) -> str: # Range types # --------------------------------------------------------------------------- -class PostgresInt4RangeType(DataType): +class PostgresInt4RangeType(DataType, backend="postgres"): """PostgreSQL ``INT4RANGE`` — range of integer.""" def _default_sql(self) -> str: return "INT4RANGE" -class PostgresInt8RangeType(DataType): +class PostgresInt8RangeType(DataType, backend="postgres"): """PostgreSQL ``INT8RANGE`` — range of bigint.""" def _default_sql(self) -> str: return "INT8RANGE" -class PostgresNumRangeType(DataType): +class PostgresNumRangeType(DataType, backend="postgres"): """PostgreSQL ``NUMRANGE`` — range of numeric.""" def _default_sql(self) -> str: return "NUMRANGE" -class PostgresTsRangeType(DataType): +class PostgresTsRangeType(DataType, backend="postgres"): """PostgreSQL ``TSRANGE`` — range of timestamp without time zone.""" def _default_sql(self) -> str: return "TSRANGE" -class PostgresTsTzRangeType(DataType): +class PostgresTsTzRangeType(DataType, backend="postgres"): """PostgreSQL ``TSTZRANGE`` — range of timestamp with time zone.""" def _default_sql(self) -> str: return "TSTZRANGE" -class PostgresDateRangeType(DataType): +class PostgresDateRangeType(DataType, backend="postgres"): """PostgreSQL ``DATERANGE`` — range of date.""" def _default_sql(self) -> str: @@ -313,42 +325,42 @@ def _default_sql(self) -> str: # Multirange types (PG 14+) # --------------------------------------------------------------------------- -class PostgresInt4MultirangeType(DataType): +class PostgresInt4MultirangeType(DataType, backend="postgres"): """PostgreSQL ``INT4MULTIRANGE`` — multirange of integer (PG 14+).""" def _default_sql(self) -> str: return "INT4MULTIRANGE" -class PostgresInt8MultirangeType(DataType): +class PostgresInt8MultirangeType(DataType, backend="postgres"): """PostgreSQL ``INT8MULTIRANGE`` — multirange of bigint (PG 14+).""" def _default_sql(self) -> str: return "INT8MULTIRANGE" -class PostgresNumMultirangeType(DataType): +class PostgresNumMultirangeType(DataType, backend="postgres"): """PostgreSQL ``NUMMULTIRANGE`` — multirange of numeric (PG 14+).""" def _default_sql(self) -> str: return "NUMMULTIRANGE" -class PostgresTsMultirangeType(DataType): +class PostgresTsMultirangeType(DataType, backend="postgres"): """PostgreSQL ``TSMULTIRANGE`` — multirange of timestamp (PG 14+).""" def _default_sql(self) -> str: return "TSMULTIRANGE" -class PostgresTsTzMultirangeType(DataType): +class PostgresTsTzMultirangeType(DataType, backend="postgres"): """PostgreSQL ``TSTZMULTIRANGE`` — multirange of timestamptz (PG 14+).""" def _default_sql(self) -> str: return "TSTZMULTIRANGE" -class PostgresDateMultirangeType(DataType): +class PostgresDateMultirangeType(DataType, backend="postgres"): """PostgreSQL ``DATEMULTIRANGE`` — multirange of date (PG 14+).""" def _default_sql(self) -> str: @@ -359,49 +371,49 @@ def _default_sql(self) -> str: # Object identifier types # --------------------------------------------------------------------------- -class PostgresOIDType(DataType): +class PostgresOIDType(DataType, backend="postgres"): """PostgreSQL ``OID`` — object identifier.""" def _default_sql(self) -> str: return "OID" -class PostgresRegClassType(DataType): +class PostgresRegClassType(DataType, backend="postgres"): """PostgreSQL ``REGCLASS`` — relation name (OID alias).""" def _default_sql(self) -> str: return "REGCLASS" -class PostgresRegTypeType(DataType): +class PostgresRegTypeType(DataType, backend="postgres"): """PostgreSQL ``REGTYPE`` — type name (OID alias).""" def _default_sql(self) -> str: return "REGTYPE" -class PostgresXIDType(DataType): +class PostgresXIDType(DataType, backend="postgres"): """PostgreSQL ``XID`` — transaction ID.""" def _default_sql(self) -> str: return "XID" -class PostgresXID8Type(DataType): +class PostgresXID8Type(DataType, backend="postgres"): """PostgreSQL ``XID8`` — 64-bit transaction ID (PG 13+).""" def _default_sql(self) -> str: return "XID8" -class PostgresCIDType(DataType): +class PostgresCIDType(DataType, backend="postgres"): """PostgreSQL ``CID`` — command ID.""" def _default_sql(self) -> str: return "CID" -class PostgresTIDType(DataType): +class PostgresTIDType(DataType, backend="postgres"): """PostgreSQL ``TID`` — tuple ID (page, tuple).""" def _default_sql(self) -> str: @@ -412,7 +424,7 @@ def _default_sql(self) -> str: # pg_lsn type # --------------------------------------------------------------------------- -class PostgresPgLSNType(DataType): +class PostgresPgLSNType(DataType, backend="postgres"): """PostgreSQL ``PG_LSN`` — WAL log sequence number.""" def _default_sql(self) -> str: @@ -423,14 +435,14 @@ def _default_sql(self) -> str: # Extension-provided types (minimal DataType wrappers) # --------------------------------------------------------------------------- -class PostgresHstoreType(DataType): +class PostgresHstoreType(DataType, backend="postgres"): """PostgreSQL ``HSTORE`` — key-value store (hstore extension).""" def _default_sql(self) -> str: return "HSTORE" -class PostgresGeometryType(DataType): +class PostgresGeometryType(DataType, backend="postgres"): """PostGIS ``GEOMETRY`` — generic spatial geometry (PostGIS extension). For production use, subclass with SRID support as needed. @@ -440,14 +452,14 @@ def _default_sql(self) -> str: return "GEOMETRY" -class PostgresGeographyType(DataType): +class PostgresGeographyType(DataType, backend="postgres"): """PostGIS ``GEOGRAPHY`` — geodetic spatial type (PostGIS extension).""" def _default_sql(self) -> str: return "GEOGRAPHY" -class PostgresVectorType(DataType): +class PostgresVectorType(DataType, backend="postgres"): """pgvector ``VECTOR(n)`` — vector embedding (pgvector extension). Args: @@ -460,8 +472,13 @@ def __init__(self, dim: int): super().__init__() self.dim = dim - def _type_params(self) -> Tuple: - return (self.dim,) + def __eq__(self, other: object) -> bool: + if type(self) is not type(other): + return False + return self.dim == other.dim + + def __hash__(self) -> int: + return hash((type(self), self.dim)) def _default_sql(self) -> str: return f"VECTOR({self.dim})" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index b0477b53..12b18d8f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -86,6 +86,7 @@ PostgresXIDType, PostgresXMLType, PostgresCidrType, + PostgresCharacterVaryingType, ) @@ -121,6 +122,8 @@ def supports_data_types(self) -> List[Tuple[Type[DataType], str]]: # UUID / XML (PostgresUUIDType, "uuid"), (PostgresXMLType, "xml"), + # Character Varying alias + (PostgresCharacterVaryingType, "character_varying"), # Text search (PostgresTSVectorType, "ts_vector"), (PostgresTSQueryType, "ts_query"), @@ -225,6 +228,9 @@ def format_data_type_uuid(self, data_type) -> str: def format_data_type_xml(self, data_type) -> str: return data_type._default_sql() + def format_data_type_character_varying(self, data_type) -> str: + return data_type._default_sql() + def format_data_type_ts_vector(self, data_type) -> str: return data_type._default_sql() From 69752c181d92f7119840edd6e9c5c70921c34721 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 13:15:21 +0800 Subject: [PATCH 03/22] feat(schema): add parsed_data_type to PostgreSQL _parse_columns Call DataType.parse_data_type_str(dialect, data_type) when constructing ColumnInfo so that introspection results carry structured DataType instances for schema comparison. --- .../backend/impl/postgres/introspection/introspector.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py b/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py index c4504f73..3ba187d1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/introspection/introspector.py @@ -50,6 +50,7 @@ TriggerInfo, ViewInfo, ) +from rhosocial.activerecord.backend.expression.types import DataType from .status_introspector import ( SyncPostgreSQLStatusIntrospector, AsyncPostgreSQLStatusIntrospector, @@ -137,6 +138,7 @@ def _parse_columns(self, rows: List[Dict[str, Any]], table_name: str, schema: st data_type = row.get("data_type", "") # Extract base type (remove array brackets, size specifiers) base_type = data_type.split("[")[0].split("(")[0].strip() + parsed = DataType.parse_data_type_str(self._backend.dialect, data_type) columns.append( ColumnInfo( name=row["column_name"], @@ -145,6 +147,7 @@ def _parse_columns(self, rows: List[Dict[str, Any]], table_name: str, schema: st ordinal_position=row["ordinal_position"], data_type=base_type, data_type_full=data_type, + parsed_data_type=parsed, nullable=ColumnNullable.NULLABLE if row.get("is_nullable") else ColumnNullable.NOT_NULL, default_value=row.get("default_value"), is_primary_key=row.get("is_primary_key", False), From eb74b59e8798d6b186819aff36742732b7828fbe Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 13:20:50 +0800 Subject: [PATCH 04/22] feat(types): add synonyms to PostgreSQL types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PostgresSerialType ↔ IntegerType/SerialType - PostgresSmallSerialType ↔ SmallIntType/SmallSerialType - PostgresBigSerialType ↔ BigIntType/BigSerialType - PostgresCharacterVaryingType ↔ VarCharType --- .../backend/impl/postgres/expression/types.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py index 4113f8b0..27f91a42 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -17,7 +17,7 @@ from __future__ import annotations -from typing import Optional +from typing import Optional, Set from rhosocial.activerecord.backend.expression.types import ( BlobType, @@ -36,6 +36,10 @@ class PostgresCharacterVaryingType(VarCharType, backend="postgres"): """PostgreSQL ``CHARACTER VARYING(n)`` — alias for ``VARCHAR(n)``.""" + @classmethod + def synonyms(cls) -> Set[str]: + return {'VarCharType'} + def _default_sql(self) -> str: if self.length is not None: return f"CHARACTER VARYING({self.length})" @@ -60,6 +64,10 @@ def _default_sql(self) -> str: class PostgresSmallSerialType(DataType, backend="postgres"): """PostgreSQL ``SMALLSERIAL`` — auto-incrementing SMALLINT (2 bytes).""" + @classmethod + def synonyms(cls) -> Set[str]: + return {'SmallSerialType', 'SmallIntType'} + def _default_sql(self) -> str: return "SMALLSERIAL" @@ -67,6 +75,10 @@ def _default_sql(self) -> str: class PostgresSerialType(DataType, backend="postgres"): """PostgreSQL ``SERIAL`` — auto-incrementing INTEGER (4 bytes).""" + @classmethod + def synonyms(cls) -> Set[str]: + return {'SerialType', 'IntegerType'} + def _default_sql(self) -> str: return "SERIAL" @@ -74,6 +86,10 @@ def _default_sql(self) -> str: class PostgresBigSerialType(DataType, backend="postgres"): """PostgreSQL ``BIGSERIAL`` — auto-incrementing BIGINT (8 bytes).""" + @classmethod + def synonyms(cls) -> Set[str]: + return {'BigSerialType', 'BigIntType'} + def _default_sql(self) -> str: return "BIGSERIAL" From 0709ab787929c8ec460a89e559f1b0bd78666417 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 13:27:58 +0800 Subject: [PATCH 05/22] feat(types): add synonyms to PostgresByteaType MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostgresByteaType ↔ BlobType for cross-backend schema comparison. --- .../activerecord/backend/impl/postgres/expression/types.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py index 27f91a42..2346ab49 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -53,6 +53,10 @@ def _default_sql(self) -> str: class PostgresByteaType(BlobType, backend="postgres"): """PostgreSQL ``BYTEA`` — variable-length binary string.""" + @classmethod + def synonyms(cls) -> Set[str]: + return {'BlobType'} + def _default_sql(self) -> str: return "BYTEA" From a018e70fcd7c1e3538b3e43fb5c4728a81cfcc86 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 14:26:27 +0800 Subject: [PATCH 06/22] refactor(types): inherit DDLTypeMixin in Postgres type formatting --- .../postgres/mixins/types/data_type_formatting.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 12b18d8f..2788e219 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -6,6 +6,7 @@ import re from typing import TYPE_CHECKING, List, Tuple, Type +from rhosocial.activerecord.backend.dialect.mixins.ddl_type import DDLTypeMixin from rhosocial.activerecord.backend.dialect.protocols import ( TypeFormattingSupport, TypeParsingSupport, @@ -90,7 +91,7 @@ ) -class PostgresTypeFormatSupportMixin(TypeFormattingSupport, TypeParsingSupport): +class PostgresTypeFormatSupportMixin(DDLTypeMixin, TypeFormattingSupport, TypeParsingSupport): """PostgreSQL DataType formatting and parsing. Implements both ``TypeFormattingSupport`` and ``TypeParsingSupport`` so @@ -99,16 +100,21 @@ class PostgresTypeFormatSupportMixin(TypeFormattingSupport, TypeParsingSupport): """ # ------------------------------------------------------------------ - # TypeFormattingSupport + # TypeFormattingSupport / DDLTypeMixin # ------------------------------------------------------------------ - def render_type(self, data_type: DataType) -> str: + def format_data_type(self, data_type: DataType) -> str: + """Primary entry point — tries legacy dispatch, falls back to core.""" for type_class, suffix in self.supports_data_types(): if isinstance(data_type, type_class): formatter = getattr(self, f"format_data_type_{suffix}", None) if formatter is not None: return formatter(data_type) - return data_type._default_sql() + return super().format_data_type(data_type) + + def render_type(self, data_type: DataType) -> str: + """Legacy protocol method — delegates to format_data_type.""" + return self.format_data_type(data_type) def supports_data_types(self) -> List[Tuple[Type[DataType], str]]: return [ From 58528ca7ab0d92d6eef1b83b6bc519645d148495 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 14:40:33 +0800 Subject: [PATCH 07/22] refactor(types): update to DDLTypeSupport protocol --- .../backend/impl/postgres/dialect.py | 8 +++---- .../mixins/types/data_type_formatting.py | 21 +++++++------------ 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index c4031fed..65b66521 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -90,8 +90,7 @@ IntrospectionSupport, TransactionControlSupport, SQLFunctionSupport, - TypeFormattingSupport, - TypeParsingSupport, + DDLTypeSupport, ) from .mixins import ( PostgresExtensionMixin, @@ -527,9 +526,8 @@ class PostgresDialect( PostgresRangeTypeSupport, PostgresJSONBEnhancedSupport, PostgresArrayEnhancedSupport, - # DataType Support Protocols - TypeFormattingSupport, - TypeParsingSupport, + # DataType Support Protocol + DDLTypeSupport, # New feature protocols PostgresParallelQuerySupport, PostgresStoredProcedureSupport, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 2788e219..f862a455 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -7,10 +7,7 @@ from typing import TYPE_CHECKING, List, Tuple, Type from rhosocial.activerecord.backend.dialect.mixins.ddl_type import DDLTypeMixin -from rhosocial.activerecord.backend.dialect.protocols import ( - TypeFormattingSupport, - TypeParsingSupport, -) +from rhosocial.activerecord.backend.dialect.protocols import DDLTypeSupport from rhosocial.activerecord.backend.expression.types import ( BigIntType, BlobType, @@ -91,16 +88,16 @@ ) -class PostgresTypeFormatSupportMixin(DDLTypeMixin, TypeFormattingSupport, TypeParsingSupport): +class PostgresTypeFormatSupportMixin(DDLTypeMixin, DDLTypeSupport): """PostgreSQL DataType formatting and parsing. - Implements both ``TypeFormattingSupport`` and ``TypeParsingSupport`` so - the dialect can render ``DataType`` expressions to SQL strings and parse - raw SQL type strings back into ``DataType`` instances. + Implements ``DDLTypeSupport`` so the dialect can render ``DataType`` + expressions to SQL strings and parse raw SQL type strings back into + ``DataType`` instances. """ # ------------------------------------------------------------------ - # TypeFormattingSupport / DDLTypeMixin + # DDLTypeSupport — formatting # ------------------------------------------------------------------ def format_data_type(self, data_type: DataType) -> str: @@ -112,10 +109,6 @@ def format_data_type(self, data_type: DataType) -> str: return formatter(data_type) return super().format_data_type(data_type) - def render_type(self, data_type: DataType) -> str: - """Legacy protocol method — delegates to format_data_type.""" - return self.format_data_type(data_type) - def supports_data_types(self) -> List[Tuple[Type[DataType], str]]: return [ # PostgreSQL-specific types @@ -432,7 +425,7 @@ def format_data_type_json_b(self, data_type: JsonBType) -> str: return "JSONB" # ------------------------------------------------------------------ - # TypeParsingSupport + # DDLTypeSupport — parsing # ------------------------------------------------------------------ _PG_INTEGER_TYPES = re.compile( From 6657f4ac22ee2dce5d7a2f8e43aa6b0a29df59e1 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 15:48:15 +0800 Subject: [PATCH 08/22] refactor(types): use @DDLTypeMixin.handles, remove format_data_type override - Add @DDLTypeMixin.handles decorators to all 56 formatter methods - Remove custom format_data_type() dispatch and supports_data_types() list - Move Postgres type imports from TYPE_CHECKING to runtime --- .../mixins/types/data_type_formatting.py | 281 ++++++++---------- 1 file changed, 123 insertions(+), 158 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index f862a455..5f3593d8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -4,7 +4,7 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, List, Tuple, Type +from typing import TYPE_CHECKING from rhosocial.activerecord.backend.dialect.mixins.ddl_type import DDLTypeMixin from rhosocial.activerecord.backend.dialect.protocols import DDLTypeSupport @@ -34,58 +34,59 @@ TinyIntType, VarCharType, ) +from ..expression.types import ( + PostgresBigSerialType, + PostgresBitType, + PostgresBoxType, + PostgresByteaType, + PostgresCIDType, + PostgresCircleType, + PostgresDateMultirangeType, + PostgresDateRangeType, + PostgresGeographyType, + PostgresGeometryType, + PostgresHstoreType, + PostgresInetType, + PostgresInt4MultirangeType, + PostgresInt4RangeType, + PostgresInt8MultirangeType, + PostgresInt8RangeType, + PostgresJsonPathType, + PostgresLineSegmentType, + PostgresLineType, + PostgresMacAddr8Type, + PostgresMacAddrType, + PostgresMoneyType, + PostgresNumMultirangeType, + PostgresNumRangeType, + PostgresOIDType, + PostgresPathType, + PostgresPgLSNType, + PostgresPointType, + PostgresPolygonType, + PostgresRegClassType, + PostgresRegTypeType, + PostgresSerialType, + PostgresSmallSerialType, + PostgresTIDType, + PostgresTSQueryType, + PostgresTSVectorType, + PostgresTsMultirangeType, + PostgresTsRangeType, + PostgresTsTzMultirangeType, + PostgresTsTzRangeType, + PostgresUUIDType, + PostgresVarBitType, + PostgresVectorType, + PostgresXID8Type, + PostgresXIDType, + PostgresXMLType, + PostgresCidrType, + PostgresCharacterVaryingType, +) if TYPE_CHECKING: - from ..expression.types import ( - PostgresBigSerialType, - PostgresBitType, - PostgresBoxType, - PostgresByteaType, - PostgresCIDType, - PostgresCircleType, - PostgresDateMultirangeType, - PostgresDateRangeType, - PostgresGeographyType, - PostgresGeometryType, - PostgresHstoreType, - PostgresInetType, - PostgresInt4MultirangeType, - PostgresInt4RangeType, - PostgresInt8MultirangeType, - PostgresInt8RangeType, - PostgresJsonPathType, - PostgresLineSegmentType, - PostgresLineType, - PostgresMacAddr8Type, - PostgresMacAddrType, - PostgresMoneyType, - PostgresNumMultirangeType, - PostgresNumRangeType, - PostgresOIDType, - PostgresPathType, - PostgresPgLSNType, - PostgresPointType, - PostgresPolygonType, - PostgresRegClassType, - PostgresRegTypeType, - PostgresSerialType, - PostgresSmallSerialType, - PostgresTIDType, - PostgresTSQueryType, - PostgresTSVectorType, - PostgresTsMultirangeType, - PostgresTsRangeType, - PostgresTsTzMultirangeType, - PostgresTsTzRangeType, - PostgresUUIDType, - PostgresVarBitType, - PostgresVectorType, - PostgresXID8Type, - PostgresXIDType, - PostgresXMLType, - PostgresCidrType, - PostgresCharacterVaryingType, - ) + pass class PostgresTypeFormatSupportMixin(DDLTypeMixin, DDLTypeSupport): @@ -100,327 +101,291 @@ class PostgresTypeFormatSupportMixin(DDLTypeMixin, DDLTypeSupport): # DDLTypeSupport — formatting # ------------------------------------------------------------------ - def format_data_type(self, data_type: DataType) -> str: - """Primary entry point — tries legacy dispatch, falls back to core.""" - for type_class, suffix in self.supports_data_types(): - if isinstance(data_type, type_class): - formatter = getattr(self, f"format_data_type_{suffix}", None) - if formatter is not None: - return formatter(data_type) - return super().format_data_type(data_type) - - def supports_data_types(self) -> List[Tuple[Type[DataType], str]]: - return [ - # PostgreSQL-specific types - # Binary - (PostgresByteaType, "bytea"), - # Serial family - (PostgresSmallSerialType, "small_serial"), - (PostgresSerialType, "serial"), - (PostgresBigSerialType, "big_serial"), - # UUID / XML - (PostgresUUIDType, "uuid"), - (PostgresXMLType, "xml"), - # Character Varying alias - (PostgresCharacterVaryingType, "character_varying"), - # Text search - (PostgresTSVectorType, "ts_vector"), - (PostgresTSQueryType, "ts_query"), - # JSON path - (PostgresJsonPathType, "json_path"), - # Bit string - (PostgresBitType, "bit"), - (PostgresVarBitType, "var_bit"), - # Network address - (PostgresInetType, "inet"), - (PostgresCidrType, "cidr"), - (PostgresMacAddrType, "mac_addr"), - (PostgresMacAddr8Type, "mac_addr8"), - # Geometric - (PostgresPointType, "point"), - (PostgresLineType, "line"), - (PostgresLineSegmentType, "line_segment"), - (PostgresBoxType, "box"), - (PostgresPathType, "path"), - (PostgresPolygonType, "polygon"), - (PostgresCircleType, "circle"), - # Monetary - (PostgresMoneyType, "money"), - # Range - (PostgresInt4RangeType, "int4_range"), - (PostgresInt8RangeType, "int8_range"), - (PostgresNumRangeType, "num_range"), - (PostgresTsRangeType, "ts_range"), - (PostgresTsTzRangeType, "ts_tz_range"), - (PostgresDateRangeType, "date_range"), - # Multirange - (PostgresInt4MultirangeType, "int4_multirange"), - (PostgresInt8MultirangeType, "int8_multirange"), - (PostgresNumMultirangeType, "num_multirange"), - (PostgresTsMultirangeType, "ts_multirange"), - (PostgresTsTzMultirangeType, "ts_tz_multirange"), - (PostgresDateMultirangeType, "date_multirange"), - # Object identifier - (PostgresOIDType, "oid"), - (PostgresRegClassType, "reg_class"), - (PostgresRegTypeType, "reg_type"), - (PostgresXIDType, "xid"), - (PostgresXID8Type, "xid8"), - (PostgresCIDType, "cid"), - (PostgresTIDType, "tid"), - # pg_lsn - (PostgresPgLSNType, "pg_lsn"), - # Extension types - (PostgresHstoreType, "hstore"), - (PostgresGeometryType, "geometry"), - (PostgresGeographyType, "geography"), - (PostgresVectorType, "vector"), - # Core Integer family - (TinyIntType, "tiny_int"), - (SmallIntType, "small_int"), - (IntType, "int"), - (IntegerType, "integer"), - (BigIntType, "big_int"), - # Core Numeric family - (FloatType, "float"), - (RealType, "real"), - (DoubleType, "double"), - (DecimalType, "decimal"), - # Core String family - (CharType, "char"), - (VarCharType, "var_char"), - (TextType, "text"), - # Boolean - (BooleanType, "boolean"), - # Binary - (BlobType, "blob"), - # Date/time - (DateType, "date"), - (TimeType, "time"), - (TimeTzType, "time_tz"), - (DateTimeType, "date_time"), - (TimestampType, "timestamp"), - (TimestampTzType, "timestamp_tz"), - (IntervalType, "interval"), - # JSON - (JsonType, "json"), - (JsonBType, "json_b"), - ] - # --- PostgreSQL-specific formatters --- + @DDLTypeMixin.handles(PostgresByteaType) def format_data_type_bytea(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresSmallSerialType) def format_data_type_small_serial(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresSerialType) def format_data_type_serial(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresBigSerialType) def format_data_type_big_serial(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresUUIDType) def format_data_type_uuid(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresXMLType) def format_data_type_xml(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresCharacterVaryingType) def format_data_type_character_varying(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresTSVectorType) def format_data_type_ts_vector(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresTSQueryType) def format_data_type_ts_query(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresJsonPathType) def format_data_type_json_path(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresBitType) def format_data_type_bit(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresVarBitType) def format_data_type_var_bit(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresInetType) def format_data_type_inet(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresCidrType) def format_data_type_cidr(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresMacAddrType) def format_data_type_mac_addr(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresMacAddr8Type) def format_data_type_mac_addr8(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresPointType) def format_data_type_point(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresLineType) def format_data_type_line(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresLineSegmentType) def format_data_type_line_segment(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresBoxType) def format_data_type_box(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresPathType) def format_data_type_path(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresPolygonType) def format_data_type_polygon(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresCircleType) def format_data_type_circle(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresMoneyType) def format_data_type_money(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresInt4RangeType) def format_data_type_int4_range(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresInt8RangeType) def format_data_type_int8_range(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresNumRangeType) def format_data_type_num_range(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresTsRangeType) def format_data_type_ts_range(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresTsTzRangeType) def format_data_type_ts_tz_range(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresDateRangeType) def format_data_type_date_range(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresInt4MultirangeType) def format_data_type_int4_multirange(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresInt8MultirangeType) def format_data_type_int8_multirange(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresNumMultirangeType) def format_data_type_num_multirange(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresTsMultirangeType) def format_data_type_ts_multirange(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresTsTzMultirangeType) def format_data_type_ts_tz_multirange(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresDateMultirangeType) def format_data_type_date_multirange(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresOIDType) def format_data_type_oid(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresRegClassType) def format_data_type_reg_class(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresRegTypeType) def format_data_type_reg_type(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresXIDType) def format_data_type_xid(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresXID8Type) def format_data_type_xid8(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresCIDType) def format_data_type_cid(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresTIDType) def format_data_type_tid(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresPgLSNType) def format_data_type_pg_lsn(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresHstoreType) def format_data_type_hstore(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresGeometryType) def format_data_type_geometry(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresGeographyType) def format_data_type_geography(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresVectorType) def format_data_type_vector(self, data_type) -> str: return data_type._default_sql() # --- Core formatters (PostgreSQL specialized) --- + @DDLTypeMixin.handles(TinyIntType) def format_data_type_tiny_int(self, data_type: TinyIntType) -> str: return "SMALLINT" + @DDLTypeMixin.handles(SmallIntType) def format_data_type_small_int(self, data_type: SmallIntType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(IntType) def format_data_type_int(self, data_type: IntType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(IntegerType) def format_data_type_integer(self, data_type: IntegerType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(BigIntType) def format_data_type_big_int(self, data_type: BigIntType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(FloatType) def format_data_type_float(self, data_type: FloatType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(RealType) def format_data_type_real(self, data_type: RealType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(DoubleType) def format_data_type_double(self, data_type: DoubleType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(DecimalType) def format_data_type_decimal(self, data_type: DecimalType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(CharType) def format_data_type_char(self, data_type: CharType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(VarCharType) def format_data_type_var_char(self, data_type: VarCharType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(TextType) def format_data_type_text(self, data_type: TextType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(BooleanType) def format_data_type_boolean(self, data_type: BooleanType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(BlobType) def format_data_type_blob(self, data_type: BlobType) -> str: return "BYTEA" + @DDLTypeMixin.handles(DateType) def format_data_type_date(self, data_type: DateType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(TimeType) def format_data_type_time(self, data_type: TimeType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(TimeTzType) def format_data_type_time_tz(self, data_type: TimeTzType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(DateTimeType) def format_data_type_date_time(self, data_type: DateTimeType) -> str: return "TIMESTAMP" + @DDLTypeMixin.handles(TimestampType) def format_data_type_timestamp(self, data_type: TimestampType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(TimestampTzType) def format_data_type_timestamp_tz(self, data_type: TimestampTzType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(IntervalType) def format_data_type_interval(self, data_type: IntervalType) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(JsonType) def format_data_type_json(self, data_type: JsonType) -> str: return "JSON" + @DDLTypeMixin.handles(JsonBType) def format_data_type_json_b(self, data_type: JsonBType) -> str: return "JSONB" From 906ca0c9126e4e6a7032127ca5820aae92e27e72 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 16:24:35 +0800 Subject: [PATCH 09/22] fix(types): rewrite Postgres core type formatters, fix missing PostgresTypeFormatSupportMixin base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace _default_sql() calls with explicit PostgreSQL SQL strings for all 18 core type handlers (SmallIntType→SMALLINT, etc.) - Add PostgresTypeFormatSupportMixin as base of PostgresDialect (was imported but never used as base, causing format_data_type to resolve to protocol stub and return None) - Create test_core_type_mappings.py with 15 tests for core type→SQL rendering via PostgreSQL dialect --- .../backend/impl/postgres/dialect.py | 1 + .../mixins/types/data_type_formatting.py | 58 ++++++++----- .../postgres/test_core_type_mappings.py | 81 +++++++++++++++++++ 3 files changed, 121 insertions(+), 19 deletions(-) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py index 65b66521..8a681cb0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/dialect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/dialect.py @@ -402,6 +402,7 @@ class PostgresDialect( PostgresFullTextSearchMixin, PostgresRangeTypeMixin, PostgresArrayEnhancedMixin, + PostgresTypeFormatSupportMixin, # DDL/DML operation mixins (new) PostgresExtendedStatisticsMixin, PostgresStoredProcedureMixin, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 5f3593d8..18d199c0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -34,7 +34,7 @@ TinyIntType, VarCharType, ) -from ..expression.types import ( +from ...expression.types import ( PostgresBigSerialType, PostgresBitType, PostgresBoxType, @@ -303,51 +303,61 @@ def format_data_type_tiny_int(self, data_type: TinyIntType) -> str: @DDLTypeMixin.handles(SmallIntType) def format_data_type_small_int(self, data_type: SmallIntType) -> str: - return data_type._default_sql() + return "SMALLINT" @DDLTypeMixin.handles(IntType) def format_data_type_int(self, data_type: IntType) -> str: - return data_type._default_sql() + return "INTEGER" @DDLTypeMixin.handles(IntegerType) def format_data_type_integer(self, data_type: IntegerType) -> str: - return data_type._default_sql() + return "INTEGER" @DDLTypeMixin.handles(BigIntType) def format_data_type_big_int(self, data_type: BigIntType) -> str: - return data_type._default_sql() + return "BIGINT" @DDLTypeMixin.handles(FloatType) def format_data_type_float(self, data_type: FloatType) -> str: - return data_type._default_sql() + if data_type.precision is not None: + return f"FLOAT({data_type.precision})" + return "REAL" @DDLTypeMixin.handles(RealType) def format_data_type_real(self, data_type: RealType) -> str: - return data_type._default_sql() + return "REAL" @DDLTypeMixin.handles(DoubleType) def format_data_type_double(self, data_type: DoubleType) -> str: - return data_type._default_sql() + return "DOUBLE PRECISION" @DDLTypeMixin.handles(DecimalType) def format_data_type_decimal(self, data_type: DecimalType) -> str: - return data_type._default_sql() + if data_type.precision is not None and data_type.scale is not None: + return f"DECIMAL({data_type.precision},{data_type.scale})" + if data_type.precision is not None: + return f"DECIMAL({data_type.precision})" + return "DECIMAL" @DDLTypeMixin.handles(CharType) def format_data_type_char(self, data_type: CharType) -> str: - return data_type._default_sql() + if data_type.length is not None: + return f"CHAR({data_type.length})" + return "CHAR" @DDLTypeMixin.handles(VarCharType) def format_data_type_var_char(self, data_type: VarCharType) -> str: - return data_type._default_sql() + if data_type.length is not None: + return f"VARCHAR({data_type.length})" + return "VARCHAR" @DDLTypeMixin.handles(TextType) def format_data_type_text(self, data_type: TextType) -> str: - return data_type._default_sql() + return "TEXT" @DDLTypeMixin.handles(BooleanType) def format_data_type_boolean(self, data_type: BooleanType) -> str: - return data_type._default_sql() + return "BOOLEAN" @DDLTypeMixin.handles(BlobType) def format_data_type_blob(self, data_type: BlobType) -> str: @@ -355,15 +365,19 @@ def format_data_type_blob(self, data_type: BlobType) -> str: @DDLTypeMixin.handles(DateType) def format_data_type_date(self, data_type: DateType) -> str: - return data_type._default_sql() + return "DATE" @DDLTypeMixin.handles(TimeType) def format_data_type_time(self, data_type: TimeType) -> str: - return data_type._default_sql() + if data_type.precision is not None: + return f"TIME({data_type.precision})" + return "TIME" @DDLTypeMixin.handles(TimeTzType) def format_data_type_time_tz(self, data_type: TimeTzType) -> str: - return data_type._default_sql() + if data_type.precision is not None: + return f"TIME({data_type.precision}) WITH TIME ZONE" + return "TIME WITH TIME ZONE" @DDLTypeMixin.handles(DateTimeType) def format_data_type_date_time(self, data_type: DateTimeType) -> str: @@ -371,15 +385,21 @@ def format_data_type_date_time(self, data_type: DateTimeType) -> str: @DDLTypeMixin.handles(TimestampType) def format_data_type_timestamp(self, data_type: TimestampType) -> str: - return data_type._default_sql() + if data_type.precision is not None: + return f"TIMESTAMP({data_type.precision})" + return "TIMESTAMP" @DDLTypeMixin.handles(TimestampTzType) def format_data_type_timestamp_tz(self, data_type: TimestampTzType) -> str: - return data_type._default_sql() + if data_type.precision is not None: + return f"TIMESTAMP({data_type.precision}) WITH TIME ZONE" + return "TIMESTAMP WITH TIME ZONE" @DDLTypeMixin.handles(IntervalType) def format_data_type_interval(self, data_type: IntervalType) -> str: - return data_type._default_sql() + if data_type.fields is not None: + return f"INTERVAL {data_type.fields}" + return "INTERVAL" @DDLTypeMixin.handles(JsonType) def format_data_type_json(self, data_type: JsonType) -> str: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py new file mode 100644 index 00000000..d5ba977d --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py @@ -0,0 +1,81 @@ +"""Tests for PostgreSQL core type → SQL string mappings.""" + +import pytest + +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect + + +@pytest.fixture +def dialect(): + return PostgresDialect(version=(16, 0, 0)) + + +class TestCoreTypeMappings: + """Verify core DataType → SQL rendering via PostgreSQL dialect.""" + + # The five critical mappings + def test_boolean_to_boolean(self, dialect): + from rhosocial.activerecord.backend.expression.types import BooleanType + assert BooleanType().to_sql(dialect) == ("BOOLEAN", ()) + + def test_tinyint_to_smallint(self, dialect): + from rhosocial.activerecord.backend.expression.types import TinyIntType + assert TinyIntType().to_sql(dialect) == ("SMALLINT", ()) + + def test_blob_to_bytea(self, dialect): + from rhosocial.activerecord.backend.expression.types import BlobType + assert BlobType().to_sql(dialect) == ("BYTEA", ()) + + def test_datetime_to_timestamp(self, dialect): + from rhosocial.activerecord.backend.expression.types import DateTimeType + assert DateTimeType().to_sql(dialect) == ("TIMESTAMP", ()) + + def test_int_to_integer(self, dialect): + from rhosocial.activerecord.backend.expression.types import IntType + assert IntType().to_sql(dialect) == ("INTEGER", ()) + + # Standard integer family + def test_smallint(self, dialect): + from rhosocial.activerecord.backend.expression.types import SmallIntType + assert SmallIntType().to_sql(dialect) == ("SMALLINT", ()) + + def test_integer(self, dialect): + from rhosocial.activerecord.backend.expression.types import IntegerType + assert IntegerType().to_sql(dialect) == ("INTEGER", ()) + + def test_bigint(self, dialect): + from rhosocial.activerecord.backend.expression.types import BigIntType + assert BigIntType().to_sql(dialect) == ("BIGINT", ()) + + # Numeric family + def test_real(self, dialect): + from rhosocial.activerecord.backend.expression.types import RealType + assert RealType().to_sql(dialect) == ("REAL", ()) + + def test_double(self, dialect): + from rhosocial.activerecord.backend.expression.types import DoubleType + assert DoubleType().to_sql(dialect) == ("DOUBLE PRECISION", ()) + + def test_numeric(self, dialect): + from rhosocial.activerecord.backend.expression.types import DecimalType + assert DecimalType(10, 2).to_sql(dialect) == ("DECIMAL(10,2)", ()) + assert DecimalType().to_sql(dialect) == ("DECIMAL", ()) + + # String family + def test_varchar(self, dialect): + from rhosocial.activerecord.backend.expression.types import VarCharType + assert VarCharType(255).to_sql(dialect) == ("VARCHAR(255)", ()) + assert VarCharType().to_sql(dialect) == ("VARCHAR", ()) + + def test_text(self, dialect): + from rhosocial.activerecord.backend.expression.types import TextType + assert TextType().to_sql(dialect) == ("TEXT", ()) + + # JSON family + def test_json(self, dialect): + from rhosocial.activerecord.backend.expression.types import JsonType + assert JsonType().to_sql(dialect) == ("JSON", ()) + + def test_jsonb(self, dialect): + from rhosocial.activerecord.backend.expression.types import JsonBType + assert JsonBType().to_sql(dialect) == ("JSONB", ()) From 982eedc511546bbb321253255590626316bda82d Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 16:41:27 +0800 Subject: [PATCH 10/22] feat(postgres): implement array type support - Add PostgresArrayType parameterised container type (element_type, dimensions) - Add format_data_type_array handler that recursively formats element types - Update parse_type to detect ARRAY keyword and [] suffix before standard dispatch - Add 25 tests covering core type mappings and array rendering/parsing --- .../backend/impl/postgres/expression/types.py | 36 ++++++ .../mixins/types/data_type_formatting.py | 38 ++++++ .../postgres/test_core_type_mappings.py | 110 +++++++++++++++++- 3 files changed, 182 insertions(+), 2 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py index 2346ab49..ee10905f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -502,3 +502,39 @@ def __hash__(self) -> int: def _default_sql(self) -> str: return f"VECTOR({self.dim})" + + +# --------------------------------------------------------------------------- +# Array container +# --------------------------------------------------------------------------- + +class PostgresArrayType(DataType): + """PostgreSQL array type ``T[]`` / ``T[n]`` — parameterised container. + + ``element_type`` holds the inner ``DataType`` instance (e.g. ``IntegerType()`` + for ``INTEGER[]``), and ``dimensions`` records the array dimensionality + (1 for ``T[]``, 2 for ``T[][]``, etc.). + """ + + def __init__(self, element_type: DataType, dimensions: int = 1, dialect=None): + super().__init__(dialect) + self.element_type = element_type + self.dimensions = dimensions + + def __eq__(self, other: object) -> bool: + if type(self) is not type(other): + return False + return (self.element_type == other.element_type + and self.dimensions == other.dimensions) + + def __hash__(self) -> int: + return hash((type(self), self.element_type, self.dimensions)) + + def _type_params(self) -> tuple: + return (self.element_type, self.dimensions) + + def is_equivalent(self, other: DataType) -> bool: + if type(self) is not type(other): + return False + return (self.dimensions == other.dimensions + and self.element_type.is_equivalent(other.element_type)) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 18d199c0..671e5ca5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -35,6 +35,7 @@ VarCharType, ) from ...expression.types import ( + PostgresArrayType, PostgresBigSerialType, PostgresBitType, PostgresBoxType, @@ -295,6 +296,11 @@ def format_data_type_geography(self, data_type) -> str: def format_data_type_vector(self, data_type) -> str: return data_type._default_sql() + @DDLTypeMixin.handles(PostgresArrayType) + def format_data_type_array(self, data_type: PostgresArrayType) -> str: + element_sql = self.format_data_type(data_type.element_type) + return element_sql + "[]" * data_type.dimensions + # --- Core formatters (PostgreSQL specialized) --- @DDLTypeMixin.handles(TinyIntType) @@ -482,11 +488,43 @@ def format_data_type_json_b(self, data_type: JsonBType) -> str: r"^(?:MONEY|XML|PG_LSN|HSTORE|GEOMETRY|GEOGRAPHY)\b", re.IGNORECASE, ) + _PG_ARRAY_SUFFIX = re.compile(r"(\[(\d*)\])+\s*$") + _PG_ARRAY_KEYWORD = re.compile(r"\s+ARRAY(?:\s*\[\s*(\d+)\s*\])?\s*$", re.IGNORECASE) def parse_type(self, raw: str) -> DataType: stripped = raw.strip() upper = stripped.upper() + # ---- Array detection (must run before any type-specific match) ---- + + # Check for ARRAY keyword: e.g. "INTEGER ARRAY" or "INTEGER ARRAY[3]" + kw_match = self._PG_ARRAY_KEYWORD.search(upper) + if kw_match: + base_raw = stripped[:kw_match.start()] + element_type = self.parse_type(base_raw) + return PostgresArrayType(element_type, dimensions=1) + + # Check for array bracket suffix: e.g. "INTEGER[]", "INTEGER[][]" + # Count dimensions from trailing [] pairs + remaining = stripped + dims = 0 + while remaining.endswith("[]"): + dims += 1 + remaining = remaining[:-2] + while remaining.endswith("]"): + m = self._PG_ARRAY_SUFFIX.search(remaining) + if m: + dims += 1 + remaining = remaining[:m.start()] + else: + break + + if dims > 0: + element_type = self.parse_type(remaining) + return PostgresArrayType(element_type, dimensions=dims) + + # ---- Standard type dispatch ---- + # Serial family if self._PG_SERIAL_TYPES.match(upper): from ..expression.types import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py index d5ba977d..43caed1e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py @@ -2,12 +2,24 @@ import pytest -from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.dialect.mixins.ddl_type import DDLTypeMixin @pytest.fixture def dialect(): - return PostgresDialect(version=(16, 0, 0)) + # Import directly to avoid triggering backend init which requires psycopg + from rhosocial.activerecord.backend.impl.postgres.mixins.types import ( + PostgresTypeFormatSupportMixin, + ) + from rhosocial.activerecord.backend.dialect.base import SQLDialectBase + + # Build a minimal dialect class that only has the type formatting + class TestPGDialect(PostgresTypeFormatSupportMixin, SQLDialectBase): + pass + + d = TestPGDialect() + d.version = (16, 0, 0) + return d class TestCoreTypeMappings: @@ -79,3 +91,97 @@ def test_json(self, dialect): def test_jsonb(self, dialect): from rhosocial.activerecord.backend.expression.types import JsonBType assert JsonBType().to_sql(dialect) == ("JSONB", ()) + + +class TestArrayType: + """PostgreSQL array type (T[]) rendering and parsing.""" + + def test_array_1d_rendering(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + arr = PostgresArrayType(IntegerType()) + assert arr.to_sql(dialect) == ("INTEGER[]", ()) + + def test_array_2d_rendering(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + arr = PostgresArrayType(IntegerType(), dimensions=2) + assert arr.to_sql(dialect) == ("INTEGER[][]", ()) + + def test_array_varchar_rendering(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import VarCharType + arr = PostgresArrayType(VarCharType(255)) + assert arr.to_sql(dialect) == ("VARCHAR(255)[]", ()) + + def test_array_boolean_rendering(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import BooleanType + arr = PostgresArrayType(BooleanType()) + assert arr.to_sql(dialect) == ("BOOLEAN[]", ()) + + def test_array_equality(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType, VarCharType + a1 = PostgresArrayType(IntegerType(), 2) + a2 = PostgresArrayType(IntegerType(), 2) + a3 = PostgresArrayType(VarCharType(255), 2) + assert a1 == a2 + assert a1 != a3 + assert hash(a1) == hash(a2) + + def test_parse_array_bracket_suffix(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + result = dialect.parse_type("INTEGER[]") + assert isinstance(result, PostgresArrayType) + assert result.dimensions == 1 + assert isinstance(result.element_type, IntegerType) + + def test_parse_array_2d_bracket_suffix(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + result = dialect.parse_type("INTEGER[][]") + assert isinstance(result, PostgresArrayType) + assert result.dimensions == 2 + assert isinstance(result.element_type, IntegerType) + + def test_parse_array_keyword(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + result = dialect.parse_type("INTEGER ARRAY") + assert isinstance(result, PostgresArrayType) + assert result.dimensions == 1 + assert isinstance(result.element_type, IntegerType) + + def test_parse_array_varchar(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import VarCharType + result = dialect.parse_type("VARCHAR(255)[]") + assert isinstance(result, PostgresArrayType) + assert result.dimensions == 1 + assert isinstance(result.element_type, VarCharType) + assert result.element_type.length == 255 + + def test_parse_non_array_passthrough(self, dialect): + from rhosocial.activerecord.backend.expression.types import IntegerType + result = dialect.parse_type("INTEGER") + assert isinstance(result, IntegerType) From 9140634aa7a2f6ac3cb1b0e4ae0c6a68fdf7ee78 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 16:55:17 +0800 Subject: [PATCH 11/22] refactor(postgres): use core ArrayType, keep PostgresArrayType as alias - PostgresArrayType is now just 'PostgresArrayType = ArrayType' - Update handler registration to use core ArrayType - Update parse_type to return ArrayType directly - Add test for is_element_type_equivalent --- .../backend/impl/postgres/expression/types.py | 33 +------- .../mixins/types/data_type_formatting.py | 10 +-- .../postgres/test_core_type_mappings.py | 83 +++++++++---------- 3 files changed, 45 insertions(+), 81 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py index ee10905f..7653fd8a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -20,6 +20,7 @@ from typing import Optional, Set from rhosocial.activerecord.backend.expression.types import ( + ArrayType, BlobType, DataType, IntegerType, @@ -508,33 +509,5 @@ def _default_sql(self) -> str: # Array container # --------------------------------------------------------------------------- -class PostgresArrayType(DataType): - """PostgreSQL array type ``T[]`` / ``T[n]`` — parameterised container. - - ``element_type`` holds the inner ``DataType`` instance (e.g. ``IntegerType()`` - for ``INTEGER[]``), and ``dimensions`` records the array dimensionality - (1 for ``T[]``, 2 for ``T[][]``, etc.). - """ - - def __init__(self, element_type: DataType, dimensions: int = 1, dialect=None): - super().__init__(dialect) - self.element_type = element_type - self.dimensions = dimensions - - def __eq__(self, other: object) -> bool: - if type(self) is not type(other): - return False - return (self.element_type == other.element_type - and self.dimensions == other.dimensions) - - def __hash__(self) -> int: - return hash((type(self), self.element_type, self.dimensions)) - - def _type_params(self) -> tuple: - return (self.element_type, self.dimensions) - - def is_equivalent(self, other: DataType) -> bool: - if type(self) is not type(other): - return False - return (self.dimensions == other.dimensions - and self.element_type.is_equivalent(other.element_type)) +# Backward-compatible alias — core ``ArrayType`` provides the implementation. +PostgresArrayType = ArrayType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 671e5ca5..146af3d1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -9,6 +9,7 @@ from rhosocial.activerecord.backend.dialect.mixins.ddl_type import DDLTypeMixin from rhosocial.activerecord.backend.dialect.protocols import DDLTypeSupport from rhosocial.activerecord.backend.expression.types import ( + ArrayType, BigIntType, BlobType, BooleanType, @@ -35,7 +36,6 @@ VarCharType, ) from ...expression.types import ( - PostgresArrayType, PostgresBigSerialType, PostgresBitType, PostgresBoxType, @@ -296,8 +296,8 @@ def format_data_type_geography(self, data_type) -> str: def format_data_type_vector(self, data_type) -> str: return data_type._default_sql() - @DDLTypeMixin.handles(PostgresArrayType) - def format_data_type_array(self, data_type: PostgresArrayType) -> str: + @DDLTypeMixin.handles(ArrayType) + def format_data_type_array(self, data_type: ArrayType) -> str: element_sql = self.format_data_type(data_type.element_type) return element_sql + "[]" * data_type.dimensions @@ -502,7 +502,7 @@ def parse_type(self, raw: str) -> DataType: if kw_match: base_raw = stripped[:kw_match.start()] element_type = self.parse_type(base_raw) - return PostgresArrayType(element_type, dimensions=1) + return ArrayType(element_type, dimensions=1) # Check for array bracket suffix: e.g. "INTEGER[]", "INTEGER[][]" # Count dimensions from trailing [] pairs @@ -521,7 +521,7 @@ def parse_type(self, raw: str) -> DataType: if dims > 0: element_type = self.parse_type(remaining) - return PostgresArrayType(element_type, dimensions=dims) + return ArrayType(element_type, dimensions=dims) # ---- Standard type dispatch ---- diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py index 43caed1e..14236aa3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py @@ -97,86 +97,61 @@ class TestArrayType: """PostgreSQL array type (T[]) rendering and parsing.""" def test_array_1d_rendering(self, dialect): - from rhosocial.activerecord.backend.impl.postgres.expression.types import ( - PostgresArrayType, - ) - from rhosocial.activerecord.backend.expression.types import IntegerType - arr = PostgresArrayType(IntegerType()) + from rhosocial.activerecord.backend.expression.types import ArrayType, IntegerType + arr = ArrayType(IntegerType()) assert arr.to_sql(dialect) == ("INTEGER[]", ()) def test_array_2d_rendering(self, dialect): - from rhosocial.activerecord.backend.impl.postgres.expression.types import ( - PostgresArrayType, - ) - from rhosocial.activerecord.backend.expression.types import IntegerType - arr = PostgresArrayType(IntegerType(), dimensions=2) + from rhosocial.activerecord.backend.expression.types import ArrayType, IntegerType + arr = ArrayType(IntegerType(), dimensions=2) assert arr.to_sql(dialect) == ("INTEGER[][]", ()) def test_array_varchar_rendering(self, dialect): - from rhosocial.activerecord.backend.impl.postgres.expression.types import ( - PostgresArrayType, - ) - from rhosocial.activerecord.backend.expression.types import VarCharType - arr = PostgresArrayType(VarCharType(255)) + from rhosocial.activerecord.backend.expression.types import ArrayType, VarCharType + arr = ArrayType(VarCharType(255)) assert arr.to_sql(dialect) == ("VARCHAR(255)[]", ()) def test_array_boolean_rendering(self, dialect): - from rhosocial.activerecord.backend.impl.postgres.expression.types import ( - PostgresArrayType, - ) - from rhosocial.activerecord.backend.expression.types import BooleanType - arr = PostgresArrayType(BooleanType()) + from rhosocial.activerecord.backend.expression.types import ArrayType, BooleanType + arr = ArrayType(BooleanType()) assert arr.to_sql(dialect) == ("BOOLEAN[]", ()) def test_array_equality(self, dialect): - from rhosocial.activerecord.backend.impl.postgres.expression.types import ( - PostgresArrayType, + from rhosocial.activerecord.backend.expression.types import ( + ArrayType, IntegerType, VarCharType, ) - from rhosocial.activerecord.backend.expression.types import IntegerType, VarCharType - a1 = PostgresArrayType(IntegerType(), 2) - a2 = PostgresArrayType(IntegerType(), 2) - a3 = PostgresArrayType(VarCharType(255), 2) + a1 = ArrayType(IntegerType(), 2) + a2 = ArrayType(IntegerType(), 2) + a3 = ArrayType(VarCharType(255), 2) assert a1 == a2 assert a1 != a3 assert hash(a1) == hash(a2) def test_parse_array_bracket_suffix(self, dialect): - from rhosocial.activerecord.backend.impl.postgres.expression.types import ( - PostgresArrayType, - ) - from rhosocial.activerecord.backend.expression.types import IntegerType + from rhosocial.activerecord.backend.expression.types import ArrayType, IntegerType result = dialect.parse_type("INTEGER[]") - assert isinstance(result, PostgresArrayType) + assert isinstance(result, ArrayType) assert result.dimensions == 1 assert isinstance(result.element_type, IntegerType) def test_parse_array_2d_bracket_suffix(self, dialect): - from rhosocial.activerecord.backend.impl.postgres.expression.types import ( - PostgresArrayType, - ) - from rhosocial.activerecord.backend.expression.types import IntegerType + from rhosocial.activerecord.backend.expression.types import ArrayType, IntegerType result = dialect.parse_type("INTEGER[][]") - assert isinstance(result, PostgresArrayType) + assert isinstance(result, ArrayType) assert result.dimensions == 2 assert isinstance(result.element_type, IntegerType) def test_parse_array_keyword(self, dialect): - from rhosocial.activerecord.backend.impl.postgres.expression.types import ( - PostgresArrayType, - ) - from rhosocial.activerecord.backend.expression.types import IntegerType + from rhosocial.activerecord.backend.expression.types import ArrayType, IntegerType result = dialect.parse_type("INTEGER ARRAY") - assert isinstance(result, PostgresArrayType) + assert isinstance(result, ArrayType) assert result.dimensions == 1 assert isinstance(result.element_type, IntegerType) def test_parse_array_varchar(self, dialect): - from rhosocial.activerecord.backend.impl.postgres.expression.types import ( - PostgresArrayType, - ) - from rhosocial.activerecord.backend.expression.types import VarCharType + from rhosocial.activerecord.backend.expression.types import ArrayType, VarCharType result = dialect.parse_type("VARCHAR(255)[]") - assert isinstance(result, PostgresArrayType) + assert isinstance(result, ArrayType) assert result.dimensions == 1 assert isinstance(result.element_type, VarCharType) assert result.element_type.length == 255 @@ -185,3 +160,19 @@ def test_parse_non_array_passthrough(self, dialect): from rhosocial.activerecord.backend.expression.types import IntegerType result = dialect.parse_type("INTEGER") assert isinstance(result, IntegerType) + + def test_is_element_type_equivalent(self, dialect): + from rhosocial.activerecord.backend.expression.types import ( + ArrayType, IntegerType, SmallIntType, + ) + a1 = ArrayType(IntegerType(), 2) + a2 = ArrayType(IntegerType(), 1) # different dimension + a3 = ArrayType(SmallIntType()) # different element type + # Same element, different dimension → equivalent + assert a1.is_element_type_equivalent(a2) + # Different element → not equivalent + assert not a1.is_element_type_equivalent(a3) + # Plain IntegerType (not array) as other → matches element + assert a1.is_element_type_equivalent(IntegerType()) + # Plain SmallIntType as other → doesn't match Integer element + assert not a1.is_element_type_equivalent(SmallIntType()) From 7868029f5ff40f53483cf2112b9c6f87695b9bb2 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 17:18:06 +0800 Subject: [PATCH 12/22] feat(postgres): add element type validation and PostgresArrayType subclass - format_data_type_array validates element_type is in _type_formatters - PostgresArrayType is now a proper subclass with is_equivalent ignoring dimensions (PG normalises multi-dim arrays to 1-D internally) - parse_type always creates PostgresArrayType with dimensions=1 - Update tests for new PostgresArrayType subclass and PG array semantics --- .../backend/impl/postgres/expression/types.py | 16 +++- .../mixins/types/data_type_formatting.py | 23 ++++-- .../postgres/test_core_type_mappings.py | 82 +++++++++++++------ 3 files changed, 87 insertions(+), 34 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py index 7653fd8a..903cc165 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -509,5 +509,17 @@ def _default_sql(self) -> str: # Array container # --------------------------------------------------------------------------- -# Backward-compatible alias — core ``ArrayType`` provides the implementation. -PostgresArrayType = ArrayType +class PostgresArrayType(ArrayType): + """PostgreSQL array type. + + PostgreSQL normalises all multi-dimensional array declarations to a + single-dimensional internal representation at the storage level. + Therefore ``integer[][]`` and ``integer[]`` are considered equivalent + during schema comparison: ``is_equivalent`` intentionally ignores + ``dimensions``. + """ + + def is_equivalent(self, other: "DataType") -> bool: + if not isinstance(other, ArrayType): + return False + return self.element_type.is_equivalent(other.element_type) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 146af3d1..2d25ee44 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -36,6 +36,7 @@ VarCharType, ) from ...expression.types import ( + PostgresArrayType, PostgresBigSerialType, PostgresBitType, PostgresBoxType, @@ -298,7 +299,15 @@ def format_data_type_vector(self, data_type) -> str: @DDLTypeMixin.handles(ArrayType) def format_data_type_array(self, data_type: ArrayType) -> str: - element_sql = self.format_data_type(data_type.element_type) + elem = data_type.element_type + elem_cls = type(elem) + if elem_cls not in self._type_formatters: + raise TypeError( + f"ArrayType element type {elem_cls.__name__!r} is not supported " + f"by {type(self).__name__}. Use a PostgreSQL-specific DataType " + f"as the element type (e.g. PostgresIntegerType)." + ) + element_sql = self.format_data_type(elem) return element_sql + "[]" * data_type.dimensions # --- Core formatters (PostgreSQL specialized) --- @@ -502,26 +511,24 @@ def parse_type(self, raw: str) -> DataType: if kw_match: base_raw = stripped[:kw_match.start()] element_type = self.parse_type(base_raw) - return ArrayType(element_type, dimensions=1) + # PG normalises all array declarations to 1-D internally + return PostgresArrayType(element_type, dimensions=1) # Check for array bracket suffix: e.g. "INTEGER[]", "INTEGER[][]" - # Count dimensions from trailing [] pairs remaining = stripped - dims = 0 while remaining.endswith("[]"): - dims += 1 remaining = remaining[:-2] while remaining.endswith("]"): m = self._PG_ARRAY_SUFFIX.search(remaining) if m: - dims += 1 remaining = remaining[:m.start()] else: break - if dims > 0: + if remaining != stripped: element_type = self.parse_type(remaining) - return ArrayType(element_type, dimensions=dims) + # PG normalises all array declarations to 1-D internally + return PostgresArrayType(element_type, dimensions=1) # ---- Standard type dispatch ---- diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py index 14236aa3..2c8914ed 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_core_type_mappings.py @@ -97,61 +97,92 @@ class TestArrayType: """PostgreSQL array type (T[]) rendering and parsing.""" def test_array_1d_rendering(self, dialect): - from rhosocial.activerecord.backend.expression.types import ArrayType, IntegerType - arr = ArrayType(IntegerType()) + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + arr = PostgresArrayType(IntegerType()) assert arr.to_sql(dialect) == ("INTEGER[]", ()) def test_array_2d_rendering(self, dialect): - from rhosocial.activerecord.backend.expression.types import ArrayType, IntegerType - arr = ArrayType(IntegerType(), dimensions=2) + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType + arr = PostgresArrayType(IntegerType(), dimensions=2) assert arr.to_sql(dialect) == ("INTEGER[][]", ()) def test_array_varchar_rendering(self, dialect): - from rhosocial.activerecord.backend.expression.types import ArrayType, VarCharType - arr = ArrayType(VarCharType(255)) + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import VarCharType + arr = PostgresArrayType(VarCharType(255)) assert arr.to_sql(dialect) == ("VARCHAR(255)[]", ()) def test_array_boolean_rendering(self, dialect): - from rhosocial.activerecord.backend.expression.types import ArrayType, BooleanType - arr = ArrayType(BooleanType()) + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import BooleanType + arr = PostgresArrayType(BooleanType()) assert arr.to_sql(dialect) == ("BOOLEAN[]", ()) def test_array_equality(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) from rhosocial.activerecord.backend.expression.types import ( ArrayType, IntegerType, VarCharType, ) - a1 = ArrayType(IntegerType(), 2) - a2 = ArrayType(IntegerType(), 2) - a3 = ArrayType(VarCharType(255), 2) + a1 = PostgresArrayType(IntegerType(), 2) + a2 = PostgresArrayType(IntegerType(), 2) + a3 = PostgresArrayType(VarCharType(255), 2) assert a1 == a2 assert a1 != a3 assert hash(a1) == hash(a2) + # PostgresArrayType.is_equivalent ignores dimensions + a4 = PostgresArrayType(IntegerType(), 1) + assert a1.is_equivalent(a4) def test_parse_array_bracket_suffix(self, dialect): - from rhosocial.activerecord.backend.expression.types import ArrayType, IntegerType + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType result = dialect.parse_type("INTEGER[]") - assert isinstance(result, ArrayType) + assert isinstance(result, PostgresArrayType) assert result.dimensions == 1 assert isinstance(result.element_type, IntegerType) def test_parse_array_2d_bracket_suffix(self, dialect): - from rhosocial.activerecord.backend.expression.types import ArrayType, IntegerType + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType result = dialect.parse_type("INTEGER[][]") - assert isinstance(result, ArrayType) - assert result.dimensions == 2 + assert isinstance(result, PostgresArrayType) + # PG normalises all array declarations to 1-D internally + assert result.dimensions == 1 assert isinstance(result.element_type, IntegerType) def test_parse_array_keyword(self, dialect): - from rhosocial.activerecord.backend.expression.types import ArrayType, IntegerType + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import IntegerType result = dialect.parse_type("INTEGER ARRAY") - assert isinstance(result, ArrayType) + assert isinstance(result, PostgresArrayType) assert result.dimensions == 1 assert isinstance(result.element_type, IntegerType) def test_parse_array_varchar(self, dialect): - from rhosocial.activerecord.backend.expression.types import ArrayType, VarCharType + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) + from rhosocial.activerecord.backend.expression.types import VarCharType result = dialect.parse_type("VARCHAR(255)[]") - assert isinstance(result, ArrayType) + assert isinstance(result, PostgresArrayType) assert result.dimensions == 1 assert isinstance(result.element_type, VarCharType) assert result.element_type.length == 255 @@ -162,12 +193,15 @@ def test_parse_non_array_passthrough(self, dialect): assert isinstance(result, IntegerType) def test_is_element_type_equivalent(self, dialect): + from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresArrayType, + ) from rhosocial.activerecord.backend.expression.types import ( - ArrayType, IntegerType, SmallIntType, + IntegerType, SmallIntType, ) - a1 = ArrayType(IntegerType(), 2) - a2 = ArrayType(IntegerType(), 1) # different dimension - a3 = ArrayType(SmallIntType()) # different element type + a1 = PostgresArrayType(IntegerType(), 2) + a2 = PostgresArrayType(IntegerType(), 1) # different dimension + a3 = PostgresArrayType(SmallIntType()) # different element type # Same element, different dimension → equivalent assert a1.is_element_type_equivalent(a2) # Different element → not equivalent From 580e2f826055164c0d4f942b957a5a2f50c8a193 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sat, 20 Jun 2026 18:24:33 +0800 Subject: [PATCH 13/22] feat(schema): add PostgresSchemaDiffer Column order has no semantic meaning in PostgreSQL; array dimension equivalence is handled by PostgresArrayType.is_equivalent(). --- .../backend/impl/postgres/schema/__init__.py | 6 ++++++ .../backend/impl/postgres/schema/differ.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/schema/__init__.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/schema/differ.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/schema/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/schema/__init__.py new file mode 100644 index 00000000..5da28ebe --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/schema/__init__.py @@ -0,0 +1,6 @@ +# src/rhosocial/activerecord/backend/impl/postgres/schema/__init__.py +"""PostgreSQL schema differ.""" + +from .differ import PostgresSchemaDiffer + +__all__ = ["PostgresSchemaDiffer"] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/schema/differ.py b/src/rhosocial/activerecord/backend/impl/postgres/schema/differ.py new file mode 100644 index 00000000..cc5f46c7 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/schema/differ.py @@ -0,0 +1,15 @@ +# src/rhosocial/activerecord/backend/impl/postgres/schema/differ.py +"""PostgreSQL schema differ — column order has no semantic meaning.""" + +from rhosocial.activerecord.backend.schema.differ import SchemaDiffer + + +class PostgresSchemaDiffer(SchemaDiffer): + """PostgreSQL schema differ. + + Column order has no semantic meaning in PostgreSQL, and array + dimension equivalence is already handled by + ``PostgresArrayType.is_equivalent()``, so the default + ``_columns_equivalent`` is sufficient. + """ + pass From 2638e004a1107f6ed9be1f42e3cf5bff1d109423 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 21 Jun 2026 11:50:12 +0800 Subject: [PATCH 14/22] feat(schema): add schema-diff examples and refactor for PostgreSQL backend - Add 6 schema-diff example scripts: table_changes, column_order_irrelevant, postgres10_identity, array_type_diff, index_type_diff, serialization_roundtrip - All DDL uses expression objects, call introspect_and_adapt() after connect - Refactor format_column_definition to use col_def.data_type.to_sql() and add identity dialect_options support - Update existing DDL examples to use expression API (table param, QueryExpression for verification) --- .../impl/postgres/examples/conftest.py | 42 +++ .../impl/postgres/examples/ddl/alter_table.py | 26 +- .../postgres/examples/ddl/create_index.py | 18 +- .../examples/ddl/materialized_view.py | 282 ++++++++++++++---- .../postgres/examples/schema_diff/__init__.py | 0 .../examples/schema_diff/array_type_diff.py | 141 +++++++++ .../schema_diff/column_order_irrelevant.py | 103 +++++++ .../examples/schema_diff/index_type_diff.py | 103 +++++++ .../schema_diff/postgres10_identity.py | 116 +++++++ .../schema_diff/serialization_roundtrip.py | 110 +++++++ .../examples/schema_diff/table_changes.py | 109 +++++++ .../backend/impl/postgres/mixins/table.py | 15 +- 12 files changed, 994 insertions(+), 71 deletions(-) create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/__init__.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py index 44cc9fd2..a3c1a4db 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/conftest.py @@ -416,4 +416,46 @@ 'max_version': '18', 'note': 'Requires superuser or pg_read_server_files role; only available in PG15+ docker images', }, + 'schema_diff/table_changes.py': { + 'title': 'Schema Diff — Table Add/Remove', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, + 'schema_diff/column_order_irrelevant.py': { + 'title': 'Schema Diff — Column Order Is Ignored', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, + 'schema_diff/postgres10_identity.py': { + 'title': 'Schema Diff — PG9 SERIAL vs PG10+ IDENTITY', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, + 'schema_diff/array_type_diff.py': { + 'title': 'Schema Diff — Array Type Dimension Changes', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, + 'schema_diff/index_type_diff.py': { + 'title': 'Schema Diff — PostgreSQL Index Type Switches', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, + 'schema_diff/serialization_roundtrip.py': { + 'title': 'Schema Diff — Snapshot Serialization Roundtrip', + 'dialect_protocols': [], + 'priority': 10, + 'min_version': '9', + 'max_version': '18', + }, } diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py index 9005f9a7..ce68ffae 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/alter_table.py @@ -38,7 +38,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='users', + table='users', if_exists=True, cascade=True, ) @@ -47,7 +47,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='users', + table='users', columns=[ ColumnDefinition( 'id', @@ -148,13 +148,23 @@ print("Column renamed successfully") # Verify table structure -options = ExecutionOptions(stmt_type=StatementType.DQL) -result = backend.execute( - "SELECT column_name, data_type, is_nullable " - "FROM information_schema.columns " - "WHERE table_name = 'users' ORDER BY ordinal_position", - options=options +from rhosocial.activerecord.backend.expression import QueryExpression, TableExpression +from rhosocial.activerecord.backend.expression.core import WildcardExpression, Column +from rhosocial.activerecord.backend.expression.query_parts import OrderByClause + +verify_expr = QueryExpression( + dialect=dialect, + select=[ + Column(dialect, 'column_name'), + Column(dialect, 'data_type'), + Column(dialect, 'is_nullable'), + ], + from_=TableExpression(dialect, 'columns', schema_name='information_schema'), + where=Column(dialect, 'table_name') == Literal(dialect, 'users'), + order_by=OrderByClause(dialect, [Column(dialect, 'ordinal_position')]), ) +options = ExecutionOptions(stmt_type=StatementType.DQL) +result = backend.execute(*verify_expr.to_sql(), options=options) print("Table structure after alterations:") for row in result.data or []: print(f" {row}") diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py index 9e097b89..5e0c1ea3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_index.py @@ -33,7 +33,7 @@ drop_table = DropTableExpression( dialect=dialect, - table_name='products', + table='products', if_exists=True, cascade=True, ) @@ -42,7 +42,7 @@ create_table = CreateTableExpression( dialect=dialect, - table_name='products', + table='products', columns=[ ColumnDefinition( 'id', @@ -83,11 +83,17 @@ print("Index created: idx_category_price") # Verify index creation -options = ExecutionOptions(stmt_type=StatementType.DQL) -verify_result = backend.execute( - "SELECT indexname FROM pg_indexes WHERE indexname = 'idx_category_price'", - options=options +from rhosocial.activerecord.backend.expression import QueryExpression, TableExpression +from rhosocial.activerecord.backend.expression.core import Column, Literal + +verify_expr = QueryExpression( + dialect=dialect, + select=[Column(dialect, 'indexname')], + from_=TableExpression(dialect, 'pg_indexes'), + where=Column(dialect, 'indexname') == Literal(dialect, 'idx_category_price'), ) +options = ExecutionOptions(stmt_type=StatementType.DQL) +verify_result = backend.execute(*verify_expr.to_sql(), options=options) print(f"Index info: {verify_result.data}") # ============================================================ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py index 557e2858..1ef87074 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/materialized_view.py @@ -2,10 +2,16 @@ Materialized Views - PostgreSQL 9.3+. This example demonstrates: -1. Creating a materialized view -2. Refreshing materialized views -3. Using WITH NO DATA for fast definition -4. Materialized view with aggregations +1. Creating a materialized view with aggregations +2. Column aliases for the materialized view +3. Storage options (WITH clause) for physical storage tuning +4. TABLESPACE assignment +5. WITH DATA vs WITH NO DATA population modes +6. REFRESH MATERIALIZED VIEW (normal) +7. REFRESH MATERIALIZED VIEW CONCURRENTLY (PostgreSQL 9.4+) +8. REFRESH MATERIALIZED VIEW WITH DATA (after NO DATA) +9. Schema-qualified refresh (PostgresRefreshMaterializedViewExpression) +10. DROP MATERIALIZED VIEW with CASCADE and IF EXISTS """ # ============================================================ @@ -24,6 +30,7 @@ CreateMaterializedViewExpression, DropMaterializedViewExpression, RefreshMaterializedViewExpression, + CreateIndexExpression, GroupByHavingClause, FunctionCall, ) @@ -46,22 +53,35 @@ ) backend = PostgresBackend(connection_config=config) backend.connect() +backend.introspect_and_adapt() dialect = backend.dialect dql_options = ExecutionOptions(stmt_type=StatementType.DQL) +ddl_options = ExecutionOptions(stmt_type=StatementType.DDL) +_drop_mv = DropMaterializedViewExpression( + dialect=dialect, view_name='sales_summary', if_exists=True, cascade=True, +) +backend.execute(*_drop_mv.to_sql()) +_drop_mv = DropMaterializedViewExpression( + dialect=dialect, view_name='sales_daily', if_exists=True, cascade=True, +) +backend.execute(*_drop_mv.to_sql()) +_drop_mv = DropMaterializedViewExpression( + dialect=dialect, view_name='sales_product_summary', if_exists=True, cascade=True, +) +backend.execute(*_drop_mv.to_sql()) drop_table = DropTableExpression( dialect=dialect, - table_name='sales', + table='sales', if_exists=True, cascade=True, ) -sql, params = drop_table.to_sql() -backend.execute(sql, params) +backend.execute(*drop_table.to_sql()) create_table = CreateTableExpression( dialect=dialect, - table_name='sales', + table='sales', columns=[ ColumnDefinition( 'id', @@ -105,18 +125,18 @@ backend.execute(sql, params) # ============================================================ -# SECTION: Create Materialized View +# SECTION: 1. Create Materialized View with Column Aliases and Storage Options # ============================================================ -# Materialized views store the query result physically +# Column aliases rename the output columns of the materialized view. +# Storage options control physical storage behavior via WITH (param = value). -# Build the summary query with aggregations summary_query = QueryExpression( dialect=dialect, select=[ Column(dialect, 'product_id'), - FunctionCall(dialect, 'COUNT', WildcardExpression(dialect)).as_('total_sales'), - FunctionCall(dialect, 'SUM', Column(dialect, 'amount')).as_('total_amount'), - FunctionCall(dialect, 'AVG', Column(dialect, 'amount')).as_('avg_amount'), + FunctionCall(dialect, 'COUNT', WildcardExpression(dialect)), + FunctionCall(dialect, 'SUM', Column(dialect, 'amount')), + FunctionCall(dialect, 'AVG', Column(dialect, 'amount')), ], from_=TableExpression(dialect, 'sales'), group_by_having=GroupByHavingClause( @@ -129,12 +149,18 @@ dialect=dialect, view_name='sales_summary', query=summary_query, + column_aliases=['product_id', 'total_sales', 'total_amount', 'avg_amount'], + storage_options={ + 'fillfactor': 70, + 'autovacuum_enabled': 'true', + }, ) sql, params = create_mv.to_sql() -print(f"Create MV SQL: {sql}") +print(f"Create MV with column aliases and storage options:") +print(f" SQL: {sql}") backend.execute(sql, params) -# Verify the materialized view content +# Verify verify_query = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], @@ -143,53 +169,47 @@ ) sql, params = verify_query.to_sql() result = backend.execute(sql, params, options=dql_options) -print("Materialized view result:") +print("MV result:") for row in result.data or []: print(f" {row}") # ============================================================ -# SECTION: Refresh Materialized View +# SECTION: 2. TABLESPACE (SQL Generation Demo) # ============================================================ -# Refresh to update the data +# Tablespace requires an existing tablespace on the server. +# This section generates the SQL to show the syntax; execution is optional. -# Insert new data -insert_new = InsertExpression( +print() +sales_query = QueryExpression( dialect=dialect, - into='sales', - columns=['product_id', 'amount', 'sale_date'], - source=ValuesSource( - dialect, - [[Literal(dialect, '1'), Literal(dialect, '300.00'), Literal(dialect, "'2024-01-05'")]], - ), + select=[ + Column(dialect, 'product_id'), + Column(dialect, 'amount'), + Column(dialect, 'sale_date'), + ], + from_=TableExpression(dialect, 'sales'), ) -sql, params = insert_new.to_sql() -backend.execute(sql, params) -# Data in view is stale - refresh to see new data -refresh_mv = RefreshMaterializedViewExpression( +mv_tablespace = CreateMaterializedViewExpression( dialect=dialect, - view_name='sales_summary', + view_name='sales_on_fast_storage', + query=sales_query, + tablespace='fast_ssd', ) -sql, params = refresh_mv.to_sql() -backend.execute(sql, params) - -# Verify after refresh -sql, params = verify_query.to_sql() -result = backend.execute(sql, params, options=dql_options) -print("After refresh:") -for row in result.data or []: - print(f" {row}") +sql, params = mv_tablespace.to_sql() +print(f"TABLESPACE example SQL (not executed): {sql}") # ============================================================ -# SECTION: Create with NO DATA +# SECTION: 3. WITH NO DATA — Fast Definition Without Population # ============================================================ -# Use WITH NO DATA to create without populating +# Create a materialized view without initial data. The MV is registered +# but cannot be queried until a REFRESH populates it. daily_query = QueryExpression( dialect=dialect, select=[ Column(dialect, 'sale_date'), - FunctionCall(dialect, 'SUM', Column(dialect, 'amount')).as_('daily_total'), + FunctionCall(dialect, 'SUM', Column(dialect, 'amount')), ], from_=TableExpression(dialect, 'sales'), group_by_having=GroupByHavingClause( @@ -202,12 +222,14 @@ dialect=dialect, view_name='sales_daily', query=daily_query, + column_aliases=['sale_date', 'daily_total'], with_data=False, ) sql, params = create_mv_nodata.to_sql() +print(f"\nWITH NO DATA SQL: {sql}") backend.execute(sql, params) -# Verify WITH NO DATA - query should fail or return empty +# Confirm it's unqueriable nodata_query = QueryExpression( dialect=dialect, select=[WildcardExpression(dialect)], @@ -218,17 +240,150 @@ result = backend.execute(sql, params, options=dql_options) print(f"WITH NO DATA result: {result.data}") except Exception as e: - print(f"WITH NO DATA query error (expected): {e}") + print(f"WITH NO DATA query error (expected): {type(e).__name__}: {e}") # ============================================================ -# SECTION: Drop Materialized View +# SECTION: 4. REFRESH MATERIALIZED VIEW (Normal) # ============================================================ +# Refresh recalculates the query and atomically replaces the MV content. + +# Insert new data so refresh has an effect +insert_new = InsertExpression( + dialect=dialect, + into='sales', + columns=['product_id', 'amount', 'sale_date'], + source=ValuesSource( + dialect, + [[Literal(dialect, '1'), Literal(dialect, '300.00'), Literal(dialect, "'2024-01-05'")]], + ), +) +sql, params = insert_new.to_sql() +backend.execute(sql, params) + +# Refresh sales_summary +refresh_mv = RefreshMaterializedViewExpression( + dialect=dialect, + view_name='sales_summary', +) +sql, params = refresh_mv.to_sql() +print(f"\nREFRESH SQL: {sql}") +backend.execute(sql, params) + +sql, params = verify_query.to_sql() +result = backend.execute(sql, params, options=dql_options) +print("After normal refresh:") +for row in result.data or []: + print(f" {row}") + +# ============================================================ +# SECTION: 5. REFRESH MATERIALIZED VIEW CONCURRENTLY (PG 9.4+) +# ============================================================ +# CONCURRENTLY allows the MV to be read during the refresh but +# requires a UNIQUE index on every row of the materialized view. + +# Create a unique index prerequisite +create_unique_idx = CreateIndexExpression( + dialect=dialect, + index_name='sales_summary_product_id_idx', + table_name='sales_summary', + columns=['product_id'], + unique=True, +) +sql, params = create_unique_idx.to_sql() +backend.execute(sql, params, options=ddl_options) + +# Insert more data +insert_more = InsertExpression( + dialect=dialect, + into='sales', + columns=['product_id', 'amount', 'sale_date'], + source=ValuesSource( + dialect, + [[Literal(dialect, '2'), Literal(dialect, '400.00'), Literal(dialect, "'2024-01-06'")]], + ), +) +sql, params = insert_more.to_sql() +backend.execute(sql, params) + +refresh_concurrent = RefreshMaterializedViewExpression( + dialect=dialect, + view_name='sales_summary', + concurrent=True, +) +sql, params = refresh_concurrent.to_sql() +print(f"\nREFRESH CONCURRENTLY SQL: {sql}") +backend.execute(sql, params) + +sql, params = verify_query.to_sql() +result = backend.execute(sql, params, options=dql_options) +print("After CONCURRENTLY refresh:") +for row in result.data or []: + print(f" {row}") + +# ============================================================ +# SECTION: 6. REFRESH WITH DATA — Populate a NO DATA View +# ============================================================ +# An MV created with WITH NO DATA can be populated later via +# REFRESH ... WITH DATA. + +refresh_with_data = RefreshMaterializedViewExpression( + dialect=dialect, + view_name='sales_daily', + with_data=True, +) +sql, params = refresh_with_data.to_sql() +print(f"\nREFRESH WITH DATA SQL: {sql}") +backend.execute(sql, params) + +# Now the NO-DATA view is queryable +sql, params = nodata_query.to_sql() +result = backend.execute(sql, params, options=dql_options) +print("After REFRESH WITH DATA (sales_daily):") +for row in result.data or []: + print(f" {row}") + +# ============================================================ +# SECTION: 7. Schema-Qualified Refresh (PostgresRefreshMaterializedViewExpression) +# ============================================================ +# The PostgreSQL backend provides an extended refresh expression that +# supports specifying the schema for the materialized view. + +from rhosocial.activerecord.backend.impl.postgres.expression.ddl.mv import ( + PostgresRefreshMaterializedViewExpression, +) + +# Refresh with schema (public is PG's default schema) +pg_refresh = PostgresRefreshMaterializedViewExpression( + dialect=dialect, + name='sales_summary', + schema='public', +) +sql, params = pg_refresh.to_sql() +print(f"\nSchema-qualified REFRESH SQL: {sql}") +backend.execute(sql, params) + +# Also supports backward-compatible aliases: name=, concurrently= +pg_refresh_c = PostgresRefreshMaterializedViewExpression( + dialect=dialect, + name='sales_summary', + concurrently=True, +) +sql, params = pg_refresh_c.to_sql() +print(f"Backward-compat CONCURRENTLY SQL: {sql}") + +# ============================================================ +# SECTION: 8. DROP MATERIALIZED VIEW with CASCADE and IF EXISTS +# ============================================================ +# CASCADE drops any objects that depend on the MV. + drop_mv1 = DropMaterializedViewExpression( dialect=dialect, view_name='sales_summary', if_exists=True, + cascade=True, ) sql, params = drop_mv1.to_sql() +print(f"\nDROP CASCADE SQL: {sql}") backend.execute(sql, params) drop_mv2 = DropMaterializedViewExpression( @@ -239,12 +394,26 @@ sql, params = drop_mv2.to_sql() backend.execute(sql, params) +# ============================================================ +# SECTION: 9. DROP MATERIALIZED VIEW Without IF EXISTS (SQL Generation Demo) +# ============================================================ +# Dropping a non-existent view without IF EXISTS will raise an error +# at runtime. This section shows the SQL generation to avoid that. + +drop_noexist = DropMaterializedViewExpression( + dialect=dialect, + view_name='nonexistent_mv', +) +sql, params = drop_noexist.to_sql() +print(f"DROP without IF EXISTS SQL (not executed): {sql}") +print(" Note: This would fail at runtime if the MV does not exist.") + # ============================================================ # SECTION: Teardown (necessary for execution, reference only) # ============================================================ drop_table = DropTableExpression( dialect=dialect, - table_name='sales', + table='sales', if_exists=True, cascade=True, ) @@ -257,8 +426,15 @@ # ============================================================ # Key points: # 1. Use CreateMaterializedViewExpression to create MVs -# 2. Use RefreshMaterializedViewExpression to update MV data -# 3. Use with_data=False for WITH NO DATA (lazy population) -# 4. Use DropMaterializedViewExpression to drop MVs -# 5. Requires PostgreSQL 9.3+ -# 6. Use concurrent=True for CONCURRENTLY refresh (PG 9.4+) +# 2. Use column_aliases to name the MV output columns +# 3. Use storage_options for physical storage tuning (fillfactor, autovacuum, etc.) +# 4. Use tablespace to assign the MV to a specific storage location +# 5. Use with_data=False for WITH NO DATA (lazy population, MV unqueriable) +# 6. Use RefreshMaterializedViewExpression for standard refresh +# 7. Use RefreshMaterializedViewExpression with concurrent=True for CONCURRENTLY refresh (PG 9.4+, requires UNIQUE index) +# 8. Use RefreshMaterializedViewExpression with with_data=True to populate a NO-DATA MV +# 9. Use PostgresRefreshMaterializedViewExpression for schema-qualified refresh +# 10. Use DropMaterializedViewExpression with cascade=True to drop dependent objects +# 11. Use DropMaterializedViewExpression with if_exists=True for safe drop +# 12. Requires PostgreSQL 9.3+ for basic MV support +# 13. Requires PostgreSQL 9.4+ for CONCURRENTLY refresh \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py new file mode 100644 index 00000000..5b0583c7 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/array_type_diff.py @@ -0,0 +1,141 @@ +""" +Schema diff: PostgreSQL array type dimension changes. + +PostgresArrayType.is_equivalent() handles dimension comparison +internally. Column type changes like INTEGER[] → INTEGER[][] +are detected via parsed_data_type equivalence checks. + +Supported versions: PostgreSQL 8.4+ (arrays available since PG 8.4) +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, ArrayType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_v1 = DropTableExpression(dialect, "scores_v1", if_exists=True, cascade=True) +drop_v2 = DropTableExpression(dialect, "scores_v2", if_exists=True, cascade=True) +sql, params = drop_v1.to_sql() +backend.execute(sql, params) +sql, params = drop_v2.to_sql() +backend.execute(sql, params) + +# One-dimensional array column +scores_v1_table = CreateTableExpression( + dialect=dialect, table="scores_v1", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("values", ArrayType(IntegerType(), dimensions=1)), + ] +) +sql, params = scores_v1_table.to_sql() +backend.execute(sql, params) + +# Two-dimensional array column +scores_v2_table = CreateTableExpression( + dialect=dialect, table="scores_v2", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("values", ArrayType(IntegerType(), dimensions=2)), + ] +) +sql, params = scores_v2_table.to_sql() +backend.execute(sql, params) + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SyncSchemaSnapshotBuilder, +) +from rhosocial.activerecord.backend.impl.postgres.schema.differ import ( # noqa: E402 + PostgresSchemaDiffer, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) +snapshot = builder.build(schema="public") + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +for tbl_name in ("scores_v1", "scores_v2"): + if tbl_name in snapshot.tables: + for col in snapshot.tables[tbl_name].columns: + if col.name == "values": + pt = col.parsed_data_type + dims = pt.dimensions if hasattr(pt, "dimensions") else "N/A" + print(f"{tbl_name}.values: data_type='{col.data_type}', dimensions={dims}") + +# Manually construct two snapshots that differ only in array dimensions +from rhosocial.activerecord.backend.introspection.types import ( # noqa: E402 + TableInfo, ColumnInfo, ColumnNullable, TableType, DatabaseInfo, +) +from rhosocial.activerecord.backend.expression.types import IntegerType, ArrayType # noqa: E402 +from datetime import datetime, timezone # noqa: E402 + +db_info = DatabaseInfo(name="test", version="16", version_tuple=(16, 0, 0), vendor="postgresql") + +# V1: 1-d array +col1d = ColumnInfo( + name="values", table_name="t", ordinal_position=1, + data_type="INTEGER[]", parsed_data_type=ArrayType(IntegerType(), dimensions=1), + nullable=ColumnNullable.NULLABLE, +) +snap1d = snapshot.__class__( + dialect_class=snapshot.dialect_class, + captured_at=datetime.now(tz=timezone.utc), + database_info=db_info, + tables={"t": TableInfo(name="t", columns=[col1d], table_type=TableType.BASE_TABLE)}, +) + +# V2: 2-d array +col2d = ColumnInfo( + name="values", table_name="t", ordinal_position=1, + data_type="INTEGER[][]", parsed_data_type=ArrayType(IntegerType(), dimensions=2), + nullable=ColumnNullable.NULLABLE, +) +snap2d = snapshot.__class__( + dialect_class=snapshot.dialect_class, + captured_at=datetime.now(tz=timezone.utc), + database_info=db_info, + tables={"t": TableInfo(name="t", columns=[col2d], table_type=TableType.BASE_TABLE)}, +) + +differ = PostgresSchemaDiffer() +diff = differ.compare(snap1d, snap2d) +print("\nDiff 1-d vs 2-d array:") +print(f" Modified tables: {diff.modified_tables}") +print(f" Diff is empty: {diff.is_empty}") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_v1.to_sql() +backend.execute(sql, params) +sql, params = drop_v2.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py new file mode 100644 index 00000000..1015e988 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/column_order_irrelevant.py @@ -0,0 +1,103 @@ +""" +Schema diff: PostgreSQL column order has no semantic meaning. + +Unlike MySQL, PostgreSQL does not assign semantic significance to +column ordinal position. Reordering columns produces no diff because +PostgresSchemaDiffer only compares type, nullability, and default value. + +Supported versions: PostgreSQL 9+ +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) +from rhosocial.activerecord.backend.expression.statements.ddl_alter import ( + AlterTableExpression, AddColumn, DropColumn, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_demo = DropTableExpression(dialect, "demo", if_exists=True, cascade=True) +sql, params = drop_demo.to_sql() +backend.execute(sql, params) + +demo_table = CreateTableExpression( + dialect=dialect, table="demo", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(100)), + ColumnDefinition("email", VarCharType(200)), + ] +) +sql, params = demo_table.to_sql() +backend.execute(sql, params) + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SyncSchemaSnapshotBuilder, +) +from rhosocial.activerecord.backend.impl.postgres.schema.differ import ( # noqa: E402 + PostgresSchemaDiffer, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) +snapshot_before = builder.build(schema="public") + +# Add a column between name and email — shifts ordinal positions +# but PostgresSchemaDiffer ignores ordinal_position. +add_age = AlterTableExpression(dialect, "demo", [ + AddColumn(dialect, ColumnDefinition("age", IntegerType())) +]) +sql, params = add_age.to_sql() +backend.execute(sql, params) + +# Now drop it — column set is identical +drop_age = AlterTableExpression(dialect, "demo", [ + DropColumn(dialect, "age") +]) +sql, params = drop_age.to_sql() +backend.execute(sql, params) + +snapshot_after = builder.build(schema="public") + +differ = PostgresSchemaDiffer() +diff = differ.compare(snapshot_before, snapshot_after) + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +print(f"Modified tables: {diff.modified_tables}") +print(f"Diff is empty: {diff.is_empty}") +print("(No diff expected — column set unchanged regardless of position)") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_demo.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py new file mode 100644 index 00000000..ca9c7a29 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/index_type_diff.py @@ -0,0 +1,103 @@ +""" +Schema diff: PostgreSQL-specific index types (BTREE, GIN, GiST, HASH). + +PostgreSQL supports multiple index access methods. The differ checks +index_type for equivalence, so switching from BTREE to GIN or GiST +will be reported as a removed+added index pair. + +Supported versions: PostgreSQL 9+ +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + VarCharType, ArrayType, IntegerType, TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) +from rhosocial.activerecord.backend.expression.statements.ddl_index import ( + CreateIndexExpression, DropIndexExpression, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_demo = DropTableExpression(dialect, "demo", if_exists=True, cascade=True) +sql, params = drop_demo.to_sql() +backend.execute(sql, params) + +demo_table = CreateTableExpression( + dialect=dialect, table="demo", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(200)), + ColumnDefinition("tags", ArrayType(TextType(), dimensions=1)), + ] +) +sql, params = demo_table.to_sql() +backend.execute(sql, params) + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SyncSchemaSnapshotBuilder, +) +from rhosocial.activerecord.backend.impl.postgres.schema.differ import ( # noqa: E402 + PostgresSchemaDiffer, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) + +# Snapshot with BTREE index +idx_btree = CreateIndexExpression(dialect, "idx_name", "demo", ["name"], index_type="BTREE") +sql, params = idx_btree.to_sql() +backend.execute(sql, params) +snap_btree = builder.build(schema="public") + +# Drop BTREE, create GIN index on array column — different index type +drop_idx = DropIndexExpression(dialect, "idx_name", "demo", if_exists=True) +sql, params = drop_idx.to_sql() +backend.execute(sql, params) +idx_gin = CreateIndexExpression(dialect, "idx_tags", "demo", ["tags"], index_type="GIN") +sql, params = idx_gin.to_sql() +backend.execute(sql, params) +snap_gin = builder.build(schema="public") + +differ = PostgresSchemaDiffer() +diff = differ.compare(snap_btree, snap_gin) + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +print(f"Modified tables: {diff.modified_tables}") + +if "demo" in diff.table_diffs: + td = diff.table_diffs["demo"] + print(f" Removed indexes: {[(idx.name, idx.index_type.value) for idx in td.removed_indexes]}") + print(f" Added indexes: {[(idx.name, idx.index_type.value) for idx in td.added_indexes]}") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_demo.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py new file mode 100644 index 00000000..7075b33e --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/postgres10_identity.py @@ -0,0 +1,116 @@ +""" +Schema diff: PostgreSQL 9 vs 10+ — SERIAL vs GENERATED AS IDENTITY. + +PostgreSQL 9: SERIAL creates a sequence + DEFAULT nextval(). +PostgreSQL 10+: GENERATED AS IDENTITY is the SQL-standard way. + +When comparing snapshots, a SERIAL column and an IDENTITY column +will appear differently in introspected metadata: +- SERIAL: default_value = 'nextval(''tab_col_seq''::regclass)' +- IDENTITY: is_auto_increment = True, special metadata + +Supported versions: PostgreSQL 9 — SERIAL only. + PostgreSQL 10+ — GENERATED AS IDENTITY available. +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_pg9 = DropTableExpression(dialect, "pg9_style", if_exists=True, cascade=True) +drop_pg10 = DropTableExpression(dialect, "pg10_style", if_exists=True, cascade=True) +sql, params = drop_pg9.to_sql() +backend.execute(sql, params) +sql, params = drop_pg10.to_sql() +backend.execute(sql, params) + +# PostgreSQL 9-compatible: SERIAL shorthand +pg9_table = CreateTableExpression( + dialect=dialect, table="pg9_style", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(100)), + ] +) +sql, params = pg9_table.to_sql() +backend.execute(sql, params) + +# PostgreSQL 10+: GENERATED AS IDENTITY +# Note: dialect_options={'identity': 'ALWAYS'} triggers GENERATED ALWAYS AS IDENTITY +# in the PostgreSQL dialect's format_column_definition +try: + pg10_table = CreateTableExpression( + dialect=dialect, table="pg10_style", columns=[ + ColumnDefinition("id", IntegerType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)], + dialect_options={"identity": "ALWAYS"}), + ColumnDefinition("name", VarCharType(100)), + ] + ) + sql, params = pg10_table.to_sql() + backend.execute(sql, params) +except Exception as e: + print(f"(GENERATED AS IDENTITY not available on this server: {e})") + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SyncSchemaSnapshotBuilder, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) +snapshot = builder.build(schema="public") + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +for tbl_name in ("pg9_style", "pg10_style"): + if tbl_name in snapshot.tables: + tbl = snapshot.tables[tbl_name] + for col in tbl.columns: + if col.is_auto_increment or col.is_primary_key: + print(f"{tbl_name}.{col.name}:") + print(f" data_type: {col.data_type}") + print(f" is_auto_increment:{col.is_auto_increment}") + print(f" is_generated: {col.is_generated}") + print(f" default_value: {col.default_value}") + print(f" generated_expr: {col.generated_expression}") + +print() +print("Note: SERIAL vs IDENTITY columns produce different metadata.") +print("When diff-ing PG9 vs PG10+ snapshots, these differences will appear.") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_pg9.to_sql() +backend.execute(sql, params) +sql, params = drop_pg10.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py new file mode 100644 index 00000000..ca683414 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py @@ -0,0 +1,110 @@ +""" +Schema diff: snapshot serialization roundtrip across sessions. + +Serialized snapshots can be stored as JSON files and compared +later — useful for CI/CD pipeline schema validation. + +Supported versions: PostgreSQL 9+ +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import json +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + VarCharType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) +from rhosocial.activerecord.backend.expression.statements.ddl_alter import ( + AlterTableExpression, AddColumn, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_users = DropTableExpression(dialect, "users", if_exists=True, cascade=True) +sql, params = drop_users.to_sql() +backend.execute(sql, params) + +users_table = CreateTableExpression( + dialect=dialect, table="users", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(100)), + ColumnDefinition("email", VarCharType(200)), + ] +) +sql, params = users_table.to_sql() +backend.execute(sql, params) + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SchemaSnapshot, + SyncSchemaSnapshotBuilder, +) +from rhosocial.activerecord.backend.impl.postgres.schema.differ import ( # noqa: E402 + PostgresSchemaDiffer, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) +snapshot_before = builder.build(schema="public") + +# Serialize to JSON and deserialize back +snapshot_json = json.dumps(snapshot_before.to_dict(), default=str) +snapshot_loaded = SchemaSnapshot.from_dict(json.loads(snapshot_json)) + +# Modify the database +add_phone = AlterTableExpression(dialect, "users", [ + AddColumn(dialect, ColumnDefinition("phone", VarCharType(20))) +]) +sql, params = add_phone.to_sql() +backend.execute(sql, params) + +snapshot_after = builder.build(schema="public") + +# Diff: loaded snapshot (before) vs current (after) +differ = PostgresSchemaDiffer() +diff = differ.compare(snapshot_loaded, snapshot_after) + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +print(f"Snapshot size (JSON): {len(snapshot_json)} chars") +print(f"Roundtrip preserves dialect: {snapshot_loaded.dialect_class == snapshot_before.dialect_class}") +print(f"Added tables: {diff.added_tables}") +print(f"Removed tables: {diff.removed_tables}") +print(f"Modified tables: {diff.modified_tables}") + +if "users" in diff.table_diffs: + td = diff.table_diffs["users"] + db_info = snapshot_after.database_info + print(f"Database version: {db_info.version} (vendor={db_info.vendor})") + for cd in td.column_diffs: + print(f" Column '{cd.column_name}': added") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_users.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py new file mode 100644 index 00000000..e9665e0b --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/table_changes.py @@ -0,0 +1,109 @@ +""" +Schema diff: basic table add/remove detection in PostgreSQL. + +PostgreSQL uses multi-level namespaces (schema.table), and the +PostgresSchemaDiffer treats column order as semantically irrelevant. + +Supported versions: PostgreSQL 9+ +""" + +# ============================================================ +# SECTION: Setup (necessary for execution, reference only) +# ============================================================ +import os +from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.expression import ( + CreateTableExpression, DropTableExpression, ColumnDefinition, + ColumnConstraint, ColumnConstraintType, TableConstraint, TableConstraintType, +) +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, VarCharType, DecimalType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) + +config = PostgresConnectionConfig( + host=os.getenv("POSTGRES_HOST", "localhost"), + port=int(os.getenv("POSTGRES_PORT", "5432")), + database=os.getenv("POSTGRES_DATABASE", "test"), + username=os.getenv("POSTGRES_USER", "postgres"), + password=os.getenv("POSTGRES_PASSWORD", ""), +) +backend = PostgresBackend(connection_config=config) +backend.connect() +backend.introspect_and_adapt() +dialect = backend.dialect + +drop_users = DropTableExpression(dialect, "users", if_exists=True, cascade=True) +drop_orders = DropTableExpression(dialect, "orders", if_exists=True, cascade=True) +sql, params = drop_users.to_sql() +backend.execute(sql, params) +sql, params = drop_orders.to_sql() +backend.execute(sql, params) + +# ============================================================ +# SECTION: Business Logic (the pattern to learn) +# ============================================================ +from rhosocial.activerecord.backend.schema import ( # noqa: E402 + SyncSchemaSnapshotBuilder, +) +from rhosocial.activerecord.backend.impl.postgres.schema.differ import ( # noqa: E402 + PostgresSchemaDiffer, +) + +builder = SyncSchemaSnapshotBuilder(backend.introspector, dialect) +snapshot_before = builder.build(schema="public") + +users_table = CreateTableExpression( + dialect=dialect, table="users", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("name", VarCharType(100), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.NOT_NULL)]), + ] +) +sql, params = users_table.to_sql() +backend.execute(sql, params) + +orders_table = CreateTableExpression( + dialect=dialect, table="orders", columns=[ + ColumnDefinition("id", PostgresSerialType(), + constraints=[ColumnConstraint(constraint_type=ColumnConstraintType.PRIMARY_KEY)]), + ColumnDefinition("user_id", IntegerType()), + ColumnDefinition("amount", DecimalType(10, 2)), + ], + table_constraints=[ + TableConstraint( + constraint_type=TableConstraintType.FOREIGN_KEY, + columns=["user_id"], + foreign_key_table="users", + foreign_key_columns=["id"], + ) + ] +) +sql, params = orders_table.to_sql() +backend.execute(sql, params) + +snapshot_after = builder.build(schema="public") + +differ = PostgresSchemaDiffer() +diff = differ.compare(snapshot_before, snapshot_after) + +# ============================================================ +# SECTION: Execution (run the expression) +# ============================================================ +print(f"Added tables: {diff.added_tables}") +print(f"Removed tables: {diff.removed_tables}") +print(f"Modified tables:{diff.modified_tables}") +print(f"Diff is empty: {diff.is_empty}") + +# ============================================================ +# SECTION: Teardown (necessary for execution, reference only) +# ============================================================ +sql, params = drop_users.to_sql() +backend.execute(sql, params) +sql, params = drop_orders.to_sql() +backend.execute(sql, params) +backend.disconnect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py index 77e7c10f..66290066 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py @@ -40,13 +40,20 @@ def format_create_table_like(self, expr) -> tuple: def format_column_definition(self, col_def) -> Tuple[str, tuple]: from rhosocial.activerecord.backend.dialect.base import SQLDialectBase all_params: List[Any] = [] - data_type = col_def.data_type - if not re.fullmatch(r"[A-Za-z0-9\s(),\[\]]+", data_type): + type_sql, _ = col_def.data_type.to_sql(self) + if not re.fullmatch(r"[A-Za-z0-9\s(),\[\]]+", type_sql): raise ValueError( - f"Invalid data type '{data_type}': " + f"Invalid data type '{type_sql}': " "must contain only alphanumeric characters, spaces, parentheses, commas, and brackets." ) - col_sql = f"{self.format_identifier(col_def.name)} {data_type}" + col_sql = f"{self.format_identifier(col_def.name)} {type_sql}" + dialect_opts = col_def.dialect_options or {} + identity = dialect_opts.get("identity") + if identity: + if identity.upper() in ("ALWAYS", "BY DEFAULT"): + col_sql += f" GENERATED {identity.upper()} AS IDENTITY" + else: + raise ValueError(f"Invalid identity option '{identity}': must be 'ALWAYS' or 'BY DEFAULT'") for constraint in col_def.constraints: suffix, params = self.format_column_constraint(constraint) col_sql += suffix From dc9100d57ab9b4bc6d68bdbf85ab18aa3e773bcb Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 21 Jun 2026 15:43:56 +0800 Subject: [PATCH 15/22] refactor(types): remove _default_sql(), make format_data_type return Tuple[str, tuple] - Delete all _default_sql() methods from Postgres expression types - Change all PostgresTypeFormatSupportMixin formatters to return Tuple[str, tuple] - Inline _default_sql() logic directly into each format_data_type_* method - Unpack format_data_type() call in array formatter --- .../backend/impl/postgres/expression/types.py | 152 +-------- .../mixins/types/data_type_formatting.py | 318 +++++++++--------- 2 files changed, 163 insertions(+), 307 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py index 903cc165..ed58ecb8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -41,11 +41,6 @@ class PostgresCharacterVaryingType(VarCharType, backend="postgres"): def synonyms(cls) -> Set[str]: return {'VarCharType'} - def _default_sql(self) -> str: - if self.length is not None: - return f"CHARACTER VARYING({self.length})" - return "CHARACTER VARYING" - # --------------------------------------------------------------------------- # Binary data: BYTEA @@ -58,9 +53,6 @@ class PostgresByteaType(BlobType, backend="postgres"): def synonyms(cls) -> Set[str]: return {'BlobType'} - def _default_sql(self) -> str: - return "BYTEA" - # --------------------------------------------------------------------------- # Serial (auto-increment) types @@ -73,9 +65,6 @@ class PostgresSmallSerialType(DataType, backend="postgres"): def synonyms(cls) -> Set[str]: return {'SmallSerialType', 'SmallIntType'} - def _default_sql(self) -> str: - return "SMALLSERIAL" - class PostgresSerialType(DataType, backend="postgres"): """PostgreSQL ``SERIAL`` — auto-incrementing INTEGER (4 bytes).""" @@ -84,9 +73,6 @@ class PostgresSerialType(DataType, backend="postgres"): def synonyms(cls) -> Set[str]: return {'SerialType', 'IntegerType'} - def _default_sql(self) -> str: - return "SERIAL" - class PostgresBigSerialType(DataType, backend="postgres"): """PostgreSQL ``BIGSERIAL`` — auto-incrementing BIGINT (8 bytes).""" @@ -95,9 +81,6 @@ class PostgresBigSerialType(DataType, backend="postgres"): def synonyms(cls) -> Set[str]: return {'BigSerialType', 'BigIntType'} - def _default_sql(self) -> str: - return "BIGSERIAL" - # --------------------------------------------------------------------------- # UUID @@ -106,9 +89,6 @@ def _default_sql(self) -> str: class PostgresUUIDType(DataType, backend="postgres"): """PostgreSQL ``UUID`` — universally unique identifier.""" - def _default_sql(self) -> str: - return "UUID" - # --------------------------------------------------------------------------- # XML @@ -117,9 +97,6 @@ def _default_sql(self) -> str: class PostgresXMLType(DataType, backend="postgres"): """PostgreSQL ``XML`` — XML data type.""" - def _default_sql(self) -> str: - return "XML" - # --------------------------------------------------------------------------- # Text search @@ -128,16 +105,10 @@ def _default_sql(self) -> str: class PostgresTSVectorType(DataType, backend="postgres"): """PostgreSQL ``TSVECTOR`` — text search document.""" - def _default_sql(self) -> str: - return "TSVECTOR" - class PostgresTSQueryType(DataType, backend="postgres"): """PostgreSQL ``TSQUERY`` — text search query.""" - def _default_sql(self) -> str: - return "TSQUERY" - # --------------------------------------------------------------------------- # JSON path @@ -146,9 +117,6 @@ def _default_sql(self) -> str: class PostgresJsonPathType(DataType, backend="postgres"): """PostgreSQL ``JSONPATH`` — SQL/JSON path expression (PG 12+).""" - def _default_sql(self) -> str: - return "JSONPATH" - # --------------------------------------------------------------------------- # Bit string types @@ -171,11 +139,6 @@ def __eq__(self, other: object) -> bool: def __hash__(self) -> int: return hash((type(self), self.n)) - def _default_sql(self) -> str: - if self.n is not None: - return f"BIT({self.n})" - return "BIT" - class PostgresVarBitType(DataType, backend="postgres"): """PostgreSQL ``VARBIT(n)`` — variable-length bit string.""" @@ -194,11 +157,6 @@ def __eq__(self, other: object) -> bool: def __hash__(self) -> int: return hash((type(self), self.n)) - def _default_sql(self) -> str: - if self.n is not None: - return f"VARBIT({self.n})" - return "VARBIT" - # --------------------------------------------------------------------------- # Network address types @@ -207,30 +165,18 @@ def _default_sql(self) -> str: class PostgresInetType(DataType, backend="postgres"): """PostgreSQL ``INET`` — IPv4 or IPv6 address.""" - def _default_sql(self) -> str: - return "INET" - class PostgresCidrType(DataType, backend="postgres"): """PostgreSQL ``CIDR`` — IPv4 or IPv6 network.""" - def _default_sql(self) -> str: - return "CIDR" - class PostgresMacAddrType(DataType, backend="postgres"): """PostgreSQL ``MACADDR`` — MAC address (EUI-48).""" - def _default_sql(self) -> str: - return "MACADDR" - class PostgresMacAddr8Type(DataType, backend="postgres"): """PostgreSQL ``MACADDR8`` — MAC address (EUI-64, PG 10+).""" - def _default_sql(self) -> str: - return "MACADDR8" - # --------------------------------------------------------------------------- # Geometric types @@ -239,51 +185,30 @@ def _default_sql(self) -> str: class PostgresPointType(DataType, backend="postgres"): """PostgreSQL ``POINT`` — geometric point (x, y).""" - def _default_sql(self) -> str: - return "POINT" - class PostgresLineType(DataType, backend="postgres"): """PostgreSQL ``LINE`` — infinite line.""" - def _default_sql(self) -> str: - return "LINE" - class PostgresLineSegmentType(DataType, backend="postgres"): """PostgreSQL ``LSEG`` — line segment.""" - def _default_sql(self) -> str: - return "LSEG" - class PostgresBoxType(DataType, backend="postgres"): """PostgreSQL ``BOX`` — rectangular box.""" - def _default_sql(self) -> str: - return "BOX" - class PostgresPathType(DataType, backend="postgres"): """PostgreSQL ``PATH`` — open or closed geometric path.""" - def _default_sql(self) -> str: - return "PATH" - class PostgresPolygonType(DataType, backend="postgres"): """PostgreSQL ``POLYGON`` — closed geometric polygon.""" - def _default_sql(self) -> str: - return "POLYGON" - class PostgresCircleType(DataType, backend="postgres"): """PostgreSQL ``CIRCLE`` — circle (center + radius).""" - def _default_sql(self) -> str: - return "CIRCLE" - # --------------------------------------------------------------------------- # Monetary type @@ -292,9 +217,6 @@ def _default_sql(self) -> str: class PostgresMoneyType(DataType, backend="postgres"): """PostgreSQL ``MONEY`` — currency amount.""" - def _default_sql(self) -> str: - return "MONEY" - # --------------------------------------------------------------------------- # Range types @@ -303,44 +225,26 @@ def _default_sql(self) -> str: class PostgresInt4RangeType(DataType, backend="postgres"): """PostgreSQL ``INT4RANGE`` — range of integer.""" - def _default_sql(self) -> str: - return "INT4RANGE" - class PostgresInt8RangeType(DataType, backend="postgres"): """PostgreSQL ``INT8RANGE`` — range of bigint.""" - def _default_sql(self) -> str: - return "INT8RANGE" - class PostgresNumRangeType(DataType, backend="postgres"): """PostgreSQL ``NUMRANGE`` — range of numeric.""" - def _default_sql(self) -> str: - return "NUMRANGE" - class PostgresTsRangeType(DataType, backend="postgres"): """PostgreSQL ``TSRANGE`` — range of timestamp without time zone.""" - def _default_sql(self) -> str: - return "TSRANGE" - class PostgresTsTzRangeType(DataType, backend="postgres"): """PostgreSQL ``TSTZRANGE`` — range of timestamp with time zone.""" - def _default_sql(self) -> str: - return "TSTZRANGE" - class PostgresDateRangeType(DataType, backend="postgres"): """PostgreSQL ``DATERANGE`` — range of date.""" - def _default_sql(self) -> str: - return "DATERANGE" - # --------------------------------------------------------------------------- # Multirange types (PG 14+) @@ -349,44 +253,26 @@ def _default_sql(self) -> str: class PostgresInt4MultirangeType(DataType, backend="postgres"): """PostgreSQL ``INT4MULTIRANGE`` — multirange of integer (PG 14+).""" - def _default_sql(self) -> str: - return "INT4MULTIRANGE" - class PostgresInt8MultirangeType(DataType, backend="postgres"): """PostgreSQL ``INT8MULTIRANGE`` — multirange of bigint (PG 14+).""" - def _default_sql(self) -> str: - return "INT8MULTIRANGE" - class PostgresNumMultirangeType(DataType, backend="postgres"): """PostgreSQL ``NUMMULTIRANGE`` — multirange of numeric (PG 14+).""" - def _default_sql(self) -> str: - return "NUMMULTIRANGE" - class PostgresTsMultirangeType(DataType, backend="postgres"): """PostgreSQL ``TSMULTIRANGE`` — multirange of timestamp (PG 14+).""" - def _default_sql(self) -> str: - return "TSMULTIRANGE" - class PostgresTsTzMultirangeType(DataType, backend="postgres"): """PostgreSQL ``TSTZMULTIRANGE`` — multirange of timestamptz (PG 14+).""" - def _default_sql(self) -> str: - return "TSTZMULTIRANGE" - class PostgresDateMultirangeType(DataType, backend="postgres"): """PostgreSQL ``DATEMULTIRANGE`` — multirange of date (PG 14+).""" - def _default_sql(self) -> str: - return "DATEMULTIRANGE" - # --------------------------------------------------------------------------- # Object identifier types @@ -395,51 +281,30 @@ def _default_sql(self) -> str: class PostgresOIDType(DataType, backend="postgres"): """PostgreSQL ``OID`` — object identifier.""" - def _default_sql(self) -> str: - return "OID" - class PostgresRegClassType(DataType, backend="postgres"): """PostgreSQL ``REGCLASS`` — relation name (OID alias).""" - def _default_sql(self) -> str: - return "REGCLASS" - class PostgresRegTypeType(DataType, backend="postgres"): """PostgreSQL ``REGTYPE`` — type name (OID alias).""" - def _default_sql(self) -> str: - return "REGTYPE" - class PostgresXIDType(DataType, backend="postgres"): """PostgreSQL ``XID`` — transaction ID.""" - def _default_sql(self) -> str: - return "XID" - class PostgresXID8Type(DataType, backend="postgres"): """PostgreSQL ``XID8`` — 64-bit transaction ID (PG 13+).""" - def _default_sql(self) -> str: - return "XID8" - class PostgresCIDType(DataType, backend="postgres"): """PostgreSQL ``CID`` — command ID.""" - def _default_sql(self) -> str: - return "CID" - class PostgresTIDType(DataType, backend="postgres"): """PostgreSQL ``TID`` — tuple ID (page, tuple).""" - def _default_sql(self) -> str: - return "TID" - # --------------------------------------------------------------------------- # pg_lsn type @@ -448,9 +313,6 @@ def _default_sql(self) -> str: class PostgresPgLSNType(DataType, backend="postgres"): """PostgreSQL ``PG_LSN`` — WAL log sequence number.""" - def _default_sql(self) -> str: - return "PG_LSN" - # --------------------------------------------------------------------------- # Extension-provided types (minimal DataType wrappers) @@ -459,9 +321,6 @@ def _default_sql(self) -> str: class PostgresHstoreType(DataType, backend="postgres"): """PostgreSQL ``HSTORE`` — key-value store (hstore extension).""" - def _default_sql(self) -> str: - return "HSTORE" - class PostgresGeometryType(DataType, backend="postgres"): """PostGIS ``GEOMETRY`` — generic spatial geometry (PostGIS extension). @@ -469,16 +328,10 @@ class PostgresGeometryType(DataType, backend="postgres"): For production use, subclass with SRID support as needed. """ - def _default_sql(self) -> str: - return "GEOMETRY" - class PostgresGeographyType(DataType, backend="postgres"): """PostGIS ``GEOGRAPHY`` — geodetic spatial type (PostGIS extension).""" - def _default_sql(self) -> str: - return "GEOGRAPHY" - class PostgresVectorType(DataType, backend="postgres"): """pgvector ``VECTOR(n)`` — vector embedding (pgvector extension). @@ -501,9 +354,6 @@ def __eq__(self, other: object) -> bool: def __hash__(self) -> int: return hash((type(self), self.dim)) - def _default_sql(self) -> str: - return f"VECTOR({self.dim})" - # --------------------------------------------------------------------------- # Array container @@ -522,4 +372,4 @@ class PostgresArrayType(ArrayType): def is_equivalent(self, other: "DataType") -> bool: if not isinstance(other, ArrayType): return False - return self.element_type.is_equivalent(other.element_type) + return self.element_type.is_equivalent(other.element_type) \ No newline at end of file diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 2d25ee44..082aa13a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -106,199 +106,205 @@ class PostgresTypeFormatSupportMixin(DDLTypeMixin, DDLTypeSupport): # --- PostgreSQL-specific formatters --- @DDLTypeMixin.handles(PostgresByteaType) - def format_data_type_bytea(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_bytea(self, data_type): + return "BYTEA", () @DDLTypeMixin.handles(PostgresSmallSerialType) - def format_data_type_small_serial(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_small_serial(self, data_type): + return "SMALLSERIAL", () @DDLTypeMixin.handles(PostgresSerialType) - def format_data_type_serial(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_serial(self, data_type): + return "SERIAL", () @DDLTypeMixin.handles(PostgresBigSerialType) - def format_data_type_big_serial(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_big_serial(self, data_type): + return "BIGSERIAL", () @DDLTypeMixin.handles(PostgresUUIDType) - def format_data_type_uuid(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_uuid(self, data_type): + return "UUID", () @DDLTypeMixin.handles(PostgresXMLType) - def format_data_type_xml(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_xml(self, data_type): + return "XML", () @DDLTypeMixin.handles(PostgresCharacterVaryingType) - def format_data_type_character_varying(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_character_varying(self, data_type): + if data_type.length is not None: + return f"CHARACTER VARYING({data_type.length})", () + return "CHARACTER VARYING", () @DDLTypeMixin.handles(PostgresTSVectorType) - def format_data_type_ts_vector(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_ts_vector(self, data_type): + return "TSVECTOR", () @DDLTypeMixin.handles(PostgresTSQueryType) - def format_data_type_ts_query(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_ts_query(self, data_type): + return "TSQUERY", () @DDLTypeMixin.handles(PostgresJsonPathType) - def format_data_type_json_path(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_json_path(self, data_type): + return "JSONPATH", () @DDLTypeMixin.handles(PostgresBitType) - def format_data_type_bit(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_bit(self, data_type): + if data_type.n is not None: + return f"BIT({data_type.n})", () + return "BIT", () @DDLTypeMixin.handles(PostgresVarBitType) - def format_data_type_var_bit(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_var_bit(self, data_type): + if data_type.n is not None: + return f"VARBIT({data_type.n})", () + return "VARBIT", () @DDLTypeMixin.handles(PostgresInetType) - def format_data_type_inet(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_inet(self, data_type): + return "INET", () @DDLTypeMixin.handles(PostgresCidrType) - def format_data_type_cidr(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_cidr(self, data_type): + return "CIDR", () @DDLTypeMixin.handles(PostgresMacAddrType) - def format_data_type_mac_addr(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_mac_addr(self, data_type): + return "MACADDR", () @DDLTypeMixin.handles(PostgresMacAddr8Type) - def format_data_type_mac_addr8(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_mac_addr8(self, data_type): + return "MACADDR8", () @DDLTypeMixin.handles(PostgresPointType) - def format_data_type_point(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_point(self, data_type): + return "POINT", () @DDLTypeMixin.handles(PostgresLineType) - def format_data_type_line(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_line(self, data_type): + return "LINE", () @DDLTypeMixin.handles(PostgresLineSegmentType) - def format_data_type_line_segment(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_line_segment(self, data_type): + return "LSEG", () @DDLTypeMixin.handles(PostgresBoxType) - def format_data_type_box(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_box(self, data_type): + return "BOX", () @DDLTypeMixin.handles(PostgresPathType) - def format_data_type_path(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_path(self, data_type): + return "PATH", () @DDLTypeMixin.handles(PostgresPolygonType) - def format_data_type_polygon(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_polygon(self, data_type): + return "POLYGON", () @DDLTypeMixin.handles(PostgresCircleType) - def format_data_type_circle(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_circle(self, data_type): + return "CIRCLE", () @DDLTypeMixin.handles(PostgresMoneyType) - def format_data_type_money(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_money(self, data_type): + return "MONEY", () @DDLTypeMixin.handles(PostgresInt4RangeType) - def format_data_type_int4_range(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_int4_range(self, data_type): + return "INT4RANGE", () @DDLTypeMixin.handles(PostgresInt8RangeType) - def format_data_type_int8_range(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_int8_range(self, data_type): + return "INT8RANGE", () @DDLTypeMixin.handles(PostgresNumRangeType) - def format_data_type_num_range(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_num_range(self, data_type): + return "NUMRANGE", () @DDLTypeMixin.handles(PostgresTsRangeType) - def format_data_type_ts_range(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_ts_range(self, data_type): + return "TSRANGE", () @DDLTypeMixin.handles(PostgresTsTzRangeType) - def format_data_type_ts_tz_range(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_ts_tz_range(self, data_type): + return "TSTZRANGE", () @DDLTypeMixin.handles(PostgresDateRangeType) - def format_data_type_date_range(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_date_range(self, data_type): + return "DATERANGE", () @DDLTypeMixin.handles(PostgresInt4MultirangeType) - def format_data_type_int4_multirange(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_int4_multirange(self, data_type): + return "INT4MULTIRANGE", () @DDLTypeMixin.handles(PostgresInt8MultirangeType) - def format_data_type_int8_multirange(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_int8_multirange(self, data_type): + return "INT8MULTIRANGE", () @DDLTypeMixin.handles(PostgresNumMultirangeType) - def format_data_type_num_multirange(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_num_multirange(self, data_type): + return "NUMMULTIRANGE", () @DDLTypeMixin.handles(PostgresTsMultirangeType) - def format_data_type_ts_multirange(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_ts_multirange(self, data_type): + return "TSMULTIRANGE", () @DDLTypeMixin.handles(PostgresTsTzMultirangeType) - def format_data_type_ts_tz_multirange(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_ts_tz_multirange(self, data_type): + return "TSTZMULTIRANGE", () @DDLTypeMixin.handles(PostgresDateMultirangeType) - def format_data_type_date_multirange(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_date_multirange(self, data_type): + return "DATEMULTIRANGE", () @DDLTypeMixin.handles(PostgresOIDType) - def format_data_type_oid(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_oid(self, data_type): + return "OID", () @DDLTypeMixin.handles(PostgresRegClassType) - def format_data_type_reg_class(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_reg_class(self, data_type): + return "REGCLASS", () @DDLTypeMixin.handles(PostgresRegTypeType) - def format_data_type_reg_type(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_reg_type(self, data_type): + return "REGTYPE", () @DDLTypeMixin.handles(PostgresXIDType) - def format_data_type_xid(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_xid(self, data_type): + return "XID", () @DDLTypeMixin.handles(PostgresXID8Type) - def format_data_type_xid8(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_xid8(self, data_type): + return "XID8", () @DDLTypeMixin.handles(PostgresCIDType) - def format_data_type_cid(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_cid(self, data_type): + return "CID", () @DDLTypeMixin.handles(PostgresTIDType) - def format_data_type_tid(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_tid(self, data_type): + return "TID", () @DDLTypeMixin.handles(PostgresPgLSNType) - def format_data_type_pg_lsn(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_pg_lsn(self, data_type): + return "PG_LSN", () @DDLTypeMixin.handles(PostgresHstoreType) - def format_data_type_hstore(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_hstore(self, data_type): + return "HSTORE", () @DDLTypeMixin.handles(PostgresGeometryType) - def format_data_type_geometry(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_geometry(self, data_type): + return "GEOMETRY", () @DDLTypeMixin.handles(PostgresGeographyType) - def format_data_type_geography(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_geography(self, data_type): + return "GEOGRAPHY", () @DDLTypeMixin.handles(PostgresVectorType) - def format_data_type_vector(self, data_type) -> str: - return data_type._default_sql() + def format_data_type_vector(self, data_type): + return f"VECTOR({data_type.dim})", () @DDLTypeMixin.handles(ArrayType) - def format_data_type_array(self, data_type: ArrayType) -> str: + def format_data_type_array(self, data_type: ArrayType): elem = data_type.element_type elem_cls = type(elem) if elem_cls not in self._type_formatters: @@ -307,122 +313,122 @@ def format_data_type_array(self, data_type: ArrayType) -> str: f"by {type(self).__name__}. Use a PostgreSQL-specific DataType " f"as the element type (e.g. PostgresIntegerType)." ) - element_sql = self.format_data_type(elem) - return element_sql + "[]" * data_type.dimensions + element_sql, _ = self.format_data_type(elem) + return element_sql + "[]" * data_type.dimensions, () # --- Core formatters (PostgreSQL specialized) --- @DDLTypeMixin.handles(TinyIntType) - def format_data_type_tiny_int(self, data_type: TinyIntType) -> str: - return "SMALLINT" + def format_data_type_tiny_int(self, data_type: TinyIntType): + return "SMALLINT", () @DDLTypeMixin.handles(SmallIntType) - def format_data_type_small_int(self, data_type: SmallIntType) -> str: - return "SMALLINT" + def format_data_type_small_int(self, data_type: SmallIntType): + return "SMALLINT", () @DDLTypeMixin.handles(IntType) - def format_data_type_int(self, data_type: IntType) -> str: - return "INTEGER" + def format_data_type_int(self, data_type: IntType): + return "INTEGER", () @DDLTypeMixin.handles(IntegerType) - def format_data_type_integer(self, data_type: IntegerType) -> str: - return "INTEGER" + def format_data_type_integer(self, data_type: IntegerType): + return "INTEGER", () @DDLTypeMixin.handles(BigIntType) - def format_data_type_big_int(self, data_type: BigIntType) -> str: - return "BIGINT" + def format_data_type_big_int(self, data_type: BigIntType): + return "BIGINT", () @DDLTypeMixin.handles(FloatType) - def format_data_type_float(self, data_type: FloatType) -> str: + def format_data_type_float(self, data_type: FloatType): if data_type.precision is not None: - return f"FLOAT({data_type.precision})" - return "REAL" + return f"FLOAT({data_type.precision})", () + return "REAL", () @DDLTypeMixin.handles(RealType) - def format_data_type_real(self, data_type: RealType) -> str: - return "REAL" + def format_data_type_real(self, data_type: RealType): + return "REAL", () @DDLTypeMixin.handles(DoubleType) - def format_data_type_double(self, data_type: DoubleType) -> str: - return "DOUBLE PRECISION" + def format_data_type_double(self, data_type: DoubleType): + return "DOUBLE PRECISION", () @DDLTypeMixin.handles(DecimalType) - def format_data_type_decimal(self, data_type: DecimalType) -> str: + def format_data_type_decimal(self, data_type: DecimalType): if data_type.precision is not None and data_type.scale is not None: - return f"DECIMAL({data_type.precision},{data_type.scale})" + return f"DECIMAL({data_type.precision},{data_type.scale})", () if data_type.precision is not None: - return f"DECIMAL({data_type.precision})" - return "DECIMAL" + return f"DECIMAL({data_type.precision})", () + return "DECIMAL", () @DDLTypeMixin.handles(CharType) - def format_data_type_char(self, data_type: CharType) -> str: + def format_data_type_char(self, data_type: CharType): if data_type.length is not None: - return f"CHAR({data_type.length})" - return "CHAR" + return f"CHAR({data_type.length})", () + return "CHAR", () @DDLTypeMixin.handles(VarCharType) - def format_data_type_var_char(self, data_type: VarCharType) -> str: + def format_data_type_var_char(self, data_type: VarCharType): if data_type.length is not None: - return f"VARCHAR({data_type.length})" - return "VARCHAR" + return f"VARCHAR({data_type.length})", () + return "VARCHAR", () @DDLTypeMixin.handles(TextType) - def format_data_type_text(self, data_type: TextType) -> str: - return "TEXT" + def format_data_type_text(self, data_type: TextType): + return "TEXT", () @DDLTypeMixin.handles(BooleanType) - def format_data_type_boolean(self, data_type: BooleanType) -> str: - return "BOOLEAN" + def format_data_type_boolean(self, data_type: BooleanType): + return "BOOLEAN", () @DDLTypeMixin.handles(BlobType) - def format_data_type_blob(self, data_type: BlobType) -> str: - return "BYTEA" + def format_data_type_blob(self, data_type: BlobType): + return "BYTEA", () @DDLTypeMixin.handles(DateType) - def format_data_type_date(self, data_type: DateType) -> str: - return "DATE" + def format_data_type_date(self, data_type: DateType): + return "DATE", () @DDLTypeMixin.handles(TimeType) - def format_data_type_time(self, data_type: TimeType) -> str: + def format_data_type_time(self, data_type: TimeType): if data_type.precision is not None: - return f"TIME({data_type.precision})" - return "TIME" + return f"TIME({data_type.precision})", () + return "TIME", () @DDLTypeMixin.handles(TimeTzType) - def format_data_type_time_tz(self, data_type: TimeTzType) -> str: + def format_data_type_time_tz(self, data_type: TimeTzType): if data_type.precision is not None: - return f"TIME({data_type.precision}) WITH TIME ZONE" - return "TIME WITH TIME ZONE" + return f"TIME({data_type.precision}) WITH TIME ZONE", () + return "TIME WITH TIME ZONE", () @DDLTypeMixin.handles(DateTimeType) - def format_data_type_date_time(self, data_type: DateTimeType) -> str: - return "TIMESTAMP" + def format_data_type_date_time(self, data_type: DateTimeType): + return "TIMESTAMP", () @DDLTypeMixin.handles(TimestampType) - def format_data_type_timestamp(self, data_type: TimestampType) -> str: + def format_data_type_timestamp(self, data_type: TimestampType): if data_type.precision is not None: - return f"TIMESTAMP({data_type.precision})" - return "TIMESTAMP" + return f"TIMESTAMP({data_type.precision})", () + return "TIMESTAMP", () @DDLTypeMixin.handles(TimestampTzType) - def format_data_type_timestamp_tz(self, data_type: TimestampTzType) -> str: + def format_data_type_timestamp_tz(self, data_type: TimestampTzType): if data_type.precision is not None: - return f"TIMESTAMP({data_type.precision}) WITH TIME ZONE" - return "TIMESTAMP WITH TIME ZONE" + return f"TIMESTAMP({data_type.precision}) WITH TIME ZONE", () + return "TIMESTAMP WITH TIME ZONE", () @DDLTypeMixin.handles(IntervalType) - def format_data_type_interval(self, data_type: IntervalType) -> str: + def format_data_type_interval(self, data_type: IntervalType): if data_type.fields is not None: - return f"INTERVAL {data_type.fields}" - return "INTERVAL" + return f"INTERVAL {data_type.fields}", () + return "INTERVAL", () @DDLTypeMixin.handles(JsonType) - def format_data_type_json(self, data_type: JsonType) -> str: - return "JSON" + def format_data_type_json(self, data_type: JsonType): + return "JSON", () @DDLTypeMixin.handles(JsonBType) - def format_data_type_json_b(self, data_type: JsonBType) -> str: - return "JSONB" + def format_data_type_json_b(self, data_type: JsonBType): + return "JSONB", () # ------------------------------------------------------------------ # DDLTypeSupport — parsing @@ -792,4 +798,4 @@ def parse_type(self, raw: str) -> DataType: # Fallback from rhosocial.activerecord.backend.expression.types import CustomType - return CustomType(stripped) + return CustomType(stripped) \ No newline at end of file From 8109ce4f5ea015a1e4ffaebc326d5e1dd4bf7878 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 21 Jun 2026 22:28:08 +0800 Subject: [PATCH 16/22] style(types): add complete type annotations to all format_data_type_* methods --- .../mixins/types/data_type_formatting.py | 146 +++++++++--------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 082aa13a..880f9093 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -4,7 +4,7 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING +from typing import Tuple, Tuple, TYPE_CHECKING from rhosocial.activerecord.backend.dialect.mixins.ddl_type import DDLTypeMixin from rhosocial.activerecord.backend.dialect.protocols import DDLTypeSupport @@ -106,205 +106,205 @@ class PostgresTypeFormatSupportMixin(DDLTypeMixin, DDLTypeSupport): # --- PostgreSQL-specific formatters --- @DDLTypeMixin.handles(PostgresByteaType) - def format_data_type_bytea(self, data_type): + def format_data_type_bytea(self, data_type) -> Tuple[str, tuple]: return "BYTEA", () @DDLTypeMixin.handles(PostgresSmallSerialType) - def format_data_type_small_serial(self, data_type): + def format_data_type_small_serial(self, data_type) -> Tuple[str, tuple]: return "SMALLSERIAL", () @DDLTypeMixin.handles(PostgresSerialType) - def format_data_type_serial(self, data_type): + def format_data_type_serial(self, data_type) -> Tuple[str, tuple]: return "SERIAL", () @DDLTypeMixin.handles(PostgresBigSerialType) - def format_data_type_big_serial(self, data_type): + def format_data_type_big_serial(self, data_type) -> Tuple[str, tuple]: return "BIGSERIAL", () @DDLTypeMixin.handles(PostgresUUIDType) - def format_data_type_uuid(self, data_type): + def format_data_type_uuid(self, data_type) -> Tuple[str, tuple]: return "UUID", () @DDLTypeMixin.handles(PostgresXMLType) - def format_data_type_xml(self, data_type): + def format_data_type_xml(self, data_type) -> Tuple[str, tuple]: return "XML", () @DDLTypeMixin.handles(PostgresCharacterVaryingType) - def format_data_type_character_varying(self, data_type): + def format_data_type_character_varying(self, data_type: PostgresCharacterVaryingType) -> Tuple[str, tuple]: if data_type.length is not None: return f"CHARACTER VARYING({data_type.length})", () return "CHARACTER VARYING", () @DDLTypeMixin.handles(PostgresTSVectorType) - def format_data_type_ts_vector(self, data_type): + def format_data_type_ts_vector(self, data_type) -> Tuple[str, tuple]: return "TSVECTOR", () @DDLTypeMixin.handles(PostgresTSQueryType) - def format_data_type_ts_query(self, data_type): + def format_data_type_ts_query(self, data_type) -> Tuple[str, tuple]: return "TSQUERY", () @DDLTypeMixin.handles(PostgresJsonPathType) - def format_data_type_json_path(self, data_type): + def format_data_type_json_path(self, data_type) -> Tuple[str, tuple]: return "JSONPATH", () @DDLTypeMixin.handles(PostgresBitType) - def format_data_type_bit(self, data_type): + def format_data_type_bit(self, data_type) -> Tuple[str, tuple]: if data_type.n is not None: return f"BIT({data_type.n})", () return "BIT", () @DDLTypeMixin.handles(PostgresVarBitType) - def format_data_type_var_bit(self, data_type): + def format_data_type_var_bit(self, data_type) -> Tuple[str, tuple]: if data_type.n is not None: return f"VARBIT({data_type.n})", () return "VARBIT", () @DDLTypeMixin.handles(PostgresInetType) - def format_data_type_inet(self, data_type): + def format_data_type_inet(self, data_type) -> Tuple[str, tuple]: return "INET", () @DDLTypeMixin.handles(PostgresCidrType) - def format_data_type_cidr(self, data_type): + def format_data_type_cidr(self, data_type) -> Tuple[str, tuple]: return "CIDR", () @DDLTypeMixin.handles(PostgresMacAddrType) - def format_data_type_mac_addr(self, data_type): + def format_data_type_mac_addr(self, data_type) -> Tuple[str, tuple]: return "MACADDR", () @DDLTypeMixin.handles(PostgresMacAddr8Type) - def format_data_type_mac_addr8(self, data_type): + def format_data_type_mac_addr8(self, data_type) -> Tuple[str, tuple]: return "MACADDR8", () @DDLTypeMixin.handles(PostgresPointType) - def format_data_type_point(self, data_type): + def format_data_type_point(self, data_type) -> Tuple[str, tuple]: return "POINT", () @DDLTypeMixin.handles(PostgresLineType) - def format_data_type_line(self, data_type): + def format_data_type_line(self, data_type) -> Tuple[str, tuple]: return "LINE", () @DDLTypeMixin.handles(PostgresLineSegmentType) - def format_data_type_line_segment(self, data_type): + def format_data_type_line_segment(self, data_type) -> Tuple[str, tuple]: return "LSEG", () @DDLTypeMixin.handles(PostgresBoxType) - def format_data_type_box(self, data_type): + def format_data_type_box(self, data_type) -> Tuple[str, tuple]: return "BOX", () @DDLTypeMixin.handles(PostgresPathType) - def format_data_type_path(self, data_type): + def format_data_type_path(self, data_type) -> Tuple[str, tuple]: return "PATH", () @DDLTypeMixin.handles(PostgresPolygonType) - def format_data_type_polygon(self, data_type): + def format_data_type_polygon(self, data_type) -> Tuple[str, tuple]: return "POLYGON", () @DDLTypeMixin.handles(PostgresCircleType) - def format_data_type_circle(self, data_type): + def format_data_type_circle(self, data_type) -> Tuple[str, tuple]: return "CIRCLE", () @DDLTypeMixin.handles(PostgresMoneyType) - def format_data_type_money(self, data_type): + def format_data_type_money(self, data_type) -> Tuple[str, tuple]: return "MONEY", () @DDLTypeMixin.handles(PostgresInt4RangeType) - def format_data_type_int4_range(self, data_type): + def format_data_type_int4_range(self, data_type) -> Tuple[str, tuple]: return "INT4RANGE", () @DDLTypeMixin.handles(PostgresInt8RangeType) - def format_data_type_int8_range(self, data_type): + def format_data_type_int8_range(self, data_type) -> Tuple[str, tuple]: return "INT8RANGE", () @DDLTypeMixin.handles(PostgresNumRangeType) - def format_data_type_num_range(self, data_type): + def format_data_type_num_range(self, data_type) -> Tuple[str, tuple]: return "NUMRANGE", () @DDLTypeMixin.handles(PostgresTsRangeType) - def format_data_type_ts_range(self, data_type): + def format_data_type_ts_range(self, data_type) -> Tuple[str, tuple]: return "TSRANGE", () @DDLTypeMixin.handles(PostgresTsTzRangeType) - def format_data_type_ts_tz_range(self, data_type): + def format_data_type_ts_tz_range(self, data_type) -> Tuple[str, tuple]: return "TSTZRANGE", () @DDLTypeMixin.handles(PostgresDateRangeType) - def format_data_type_date_range(self, data_type): + def format_data_type_date_range(self, data_type) -> Tuple[str, tuple]: return "DATERANGE", () @DDLTypeMixin.handles(PostgresInt4MultirangeType) - def format_data_type_int4_multirange(self, data_type): + def format_data_type_int4_multirange(self, data_type) -> Tuple[str, tuple]: return "INT4MULTIRANGE", () @DDLTypeMixin.handles(PostgresInt8MultirangeType) - def format_data_type_int8_multirange(self, data_type): + def format_data_type_int8_multirange(self, data_type) -> Tuple[str, tuple]: return "INT8MULTIRANGE", () @DDLTypeMixin.handles(PostgresNumMultirangeType) - def format_data_type_num_multirange(self, data_type): + def format_data_type_num_multirange(self, data_type) -> Tuple[str, tuple]: return "NUMMULTIRANGE", () @DDLTypeMixin.handles(PostgresTsMultirangeType) - def format_data_type_ts_multirange(self, data_type): + def format_data_type_ts_multirange(self, data_type) -> Tuple[str, tuple]: return "TSMULTIRANGE", () @DDLTypeMixin.handles(PostgresTsTzMultirangeType) - def format_data_type_ts_tz_multirange(self, data_type): + def format_data_type_ts_tz_multirange(self, data_type) -> Tuple[str, tuple]: return "TSTZMULTIRANGE", () @DDLTypeMixin.handles(PostgresDateMultirangeType) - def format_data_type_date_multirange(self, data_type): + def format_data_type_date_multirange(self, data_type) -> Tuple[str, tuple]: return "DATEMULTIRANGE", () @DDLTypeMixin.handles(PostgresOIDType) - def format_data_type_oid(self, data_type): + def format_data_type_oid(self, data_type) -> Tuple[str, tuple]: return "OID", () @DDLTypeMixin.handles(PostgresRegClassType) - def format_data_type_reg_class(self, data_type): + def format_data_type_reg_class(self, data_type) -> Tuple[str, tuple]: return "REGCLASS", () @DDLTypeMixin.handles(PostgresRegTypeType) - def format_data_type_reg_type(self, data_type): + def format_data_type_reg_type(self, data_type) -> Tuple[str, tuple]: return "REGTYPE", () @DDLTypeMixin.handles(PostgresXIDType) - def format_data_type_xid(self, data_type): + def format_data_type_xid(self, data_type) -> Tuple[str, tuple]: return "XID", () @DDLTypeMixin.handles(PostgresXID8Type) - def format_data_type_xid8(self, data_type): + def format_data_type_xid8(self, data_type) -> Tuple[str, tuple]: return "XID8", () @DDLTypeMixin.handles(PostgresCIDType) - def format_data_type_cid(self, data_type): + def format_data_type_cid(self, data_type) -> Tuple[str, tuple]: return "CID", () @DDLTypeMixin.handles(PostgresTIDType) - def format_data_type_tid(self, data_type): + def format_data_type_tid(self, data_type) -> Tuple[str, tuple]: return "TID", () @DDLTypeMixin.handles(PostgresPgLSNType) - def format_data_type_pg_lsn(self, data_type): + def format_data_type_pg_lsn(self, data_type) -> Tuple[str, tuple]: return "PG_LSN", () @DDLTypeMixin.handles(PostgresHstoreType) - def format_data_type_hstore(self, data_type): + def format_data_type_hstore(self, data_type) -> Tuple[str, tuple]: return "HSTORE", () @DDLTypeMixin.handles(PostgresGeometryType) - def format_data_type_geometry(self, data_type): + def format_data_type_geometry(self, data_type) -> Tuple[str, tuple]: return "GEOMETRY", () @DDLTypeMixin.handles(PostgresGeographyType) - def format_data_type_geography(self, data_type): + def format_data_type_geography(self, data_type) -> Tuple[str, tuple]: return "GEOGRAPHY", () @DDLTypeMixin.handles(PostgresVectorType) - def format_data_type_vector(self, data_type): + def format_data_type_vector(self, data_type) -> Tuple[str, tuple]: return f"VECTOR({data_type.dim})", () @DDLTypeMixin.handles(ArrayType) - def format_data_type_array(self, data_type: ArrayType): + def format_data_type_array(self, data_type: ArrayType) -> Tuple[str, tuple]: elem = data_type.element_type elem_cls = type(elem) if elem_cls not in self._type_formatters: @@ -319,41 +319,41 @@ def format_data_type_array(self, data_type: ArrayType): # --- Core formatters (PostgreSQL specialized) --- @DDLTypeMixin.handles(TinyIntType) - def format_data_type_tiny_int(self, data_type: TinyIntType): + def format_data_type_tiny_int(self, data_type: TinyIntType) -> Tuple[str, tuple]: return "SMALLINT", () @DDLTypeMixin.handles(SmallIntType) - def format_data_type_small_int(self, data_type: SmallIntType): + def format_data_type_small_int(self, data_type: SmallIntType) -> Tuple[str, tuple]: return "SMALLINT", () @DDLTypeMixin.handles(IntType) - def format_data_type_int(self, data_type: IntType): + def format_data_type_int(self, data_type: IntType) -> Tuple[str, tuple]: return "INTEGER", () @DDLTypeMixin.handles(IntegerType) - def format_data_type_integer(self, data_type: IntegerType): + def format_data_type_integer(self, data_type: IntegerType) -> Tuple[str, tuple]: return "INTEGER", () @DDLTypeMixin.handles(BigIntType) - def format_data_type_big_int(self, data_type: BigIntType): + def format_data_type_big_int(self, data_type: BigIntType) -> Tuple[str, tuple]: return "BIGINT", () @DDLTypeMixin.handles(FloatType) - def format_data_type_float(self, data_type: FloatType): + def format_data_type_float(self, data_type: FloatType) -> Tuple[str, tuple]: if data_type.precision is not None: return f"FLOAT({data_type.precision})", () return "REAL", () @DDLTypeMixin.handles(RealType) - def format_data_type_real(self, data_type: RealType): + def format_data_type_real(self, data_type: RealType) -> Tuple[str, tuple]: return "REAL", () @DDLTypeMixin.handles(DoubleType) - def format_data_type_double(self, data_type: DoubleType): + def format_data_type_double(self, data_type: DoubleType) -> Tuple[str, tuple]: return "DOUBLE PRECISION", () @DDLTypeMixin.handles(DecimalType) - def format_data_type_decimal(self, data_type: DecimalType): + def format_data_type_decimal(self, data_type: DecimalType) -> Tuple[str, tuple]: if data_type.precision is not None and data_type.scale is not None: return f"DECIMAL({data_type.precision},{data_type.scale})", () if data_type.precision is not None: @@ -361,73 +361,73 @@ def format_data_type_decimal(self, data_type: DecimalType): return "DECIMAL", () @DDLTypeMixin.handles(CharType) - def format_data_type_char(self, data_type: CharType): + def format_data_type_char(self, data_type: CharType) -> Tuple[str, tuple]: if data_type.length is not None: return f"CHAR({data_type.length})", () return "CHAR", () @DDLTypeMixin.handles(VarCharType) - def format_data_type_var_char(self, data_type: VarCharType): + def format_data_type_var_char(self, data_type: VarCharType) -> Tuple[str, tuple]: if data_type.length is not None: return f"VARCHAR({data_type.length})", () return "VARCHAR", () @DDLTypeMixin.handles(TextType) - def format_data_type_text(self, data_type: TextType): + def format_data_type_text(self, data_type: TextType) -> Tuple[str, tuple]: return "TEXT", () @DDLTypeMixin.handles(BooleanType) - def format_data_type_boolean(self, data_type: BooleanType): + def format_data_type_boolean(self, data_type: BooleanType) -> Tuple[str, tuple]: return "BOOLEAN", () @DDLTypeMixin.handles(BlobType) - def format_data_type_blob(self, data_type: BlobType): + def format_data_type_blob(self, data_type: BlobType) -> Tuple[str, tuple]: return "BYTEA", () @DDLTypeMixin.handles(DateType) - def format_data_type_date(self, data_type: DateType): + def format_data_type_date(self, data_type: DateType) -> Tuple[str, tuple]: return "DATE", () @DDLTypeMixin.handles(TimeType) - def format_data_type_time(self, data_type: TimeType): + def format_data_type_time(self, data_type: TimeType) -> Tuple[str, tuple]: if data_type.precision is not None: return f"TIME({data_type.precision})", () return "TIME", () @DDLTypeMixin.handles(TimeTzType) - def format_data_type_time_tz(self, data_type: TimeTzType): + def format_data_type_time_tz(self, data_type: TimeTzType) -> Tuple[str, tuple]: if data_type.precision is not None: return f"TIME({data_type.precision}) WITH TIME ZONE", () return "TIME WITH TIME ZONE", () @DDLTypeMixin.handles(DateTimeType) - def format_data_type_date_time(self, data_type: DateTimeType): + def format_data_type_date_time(self, data_type: DateTimeType) -> Tuple[str, tuple]: return "TIMESTAMP", () @DDLTypeMixin.handles(TimestampType) - def format_data_type_timestamp(self, data_type: TimestampType): + def format_data_type_timestamp(self, data_type: TimestampType) -> Tuple[str, tuple]: if data_type.precision is not None: return f"TIMESTAMP({data_type.precision})", () return "TIMESTAMP", () @DDLTypeMixin.handles(TimestampTzType) - def format_data_type_timestamp_tz(self, data_type: TimestampTzType): + def format_data_type_timestamp_tz(self, data_type: TimestampTzType) -> Tuple[str, tuple]: if data_type.precision is not None: return f"TIMESTAMP({data_type.precision}) WITH TIME ZONE", () return "TIMESTAMP WITH TIME ZONE", () @DDLTypeMixin.handles(IntervalType) - def format_data_type_interval(self, data_type: IntervalType): + def format_data_type_interval(self, data_type: IntervalType) -> Tuple[str, tuple]: if data_type.fields is not None: return f"INTERVAL {data_type.fields}", () return "INTERVAL", () @DDLTypeMixin.handles(JsonType) - def format_data_type_json(self, data_type: JsonType): + def format_data_type_json(self, data_type: JsonType) -> Tuple[str, tuple]: return "JSON", () @DDLTypeMixin.handles(JsonBType) - def format_data_type_json_b(self, data_type: JsonBType): + def format_data_type_json_b(self, data_type: JsonBType) -> Tuple[str, tuple]: return "JSONB", () # ------------------------------------------------------------------ From 65de5c619d8292f4bde27e4646c2c7883a44c3de Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Sun, 21 Jun 2026 22:41:32 +0800 Subject: [PATCH 17/22] feat(examples): strengthen serialization roundtrip with table data assertions --- .../schema_diff/serialization_roundtrip.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py index ca683414..b630b4f0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/schema_diff/serialization_roundtrip.py @@ -91,6 +91,23 @@ # ============================================================ print(f"Snapshot size (JSON): {len(snapshot_json)} chars") print(f"Roundtrip preserves dialect: {snapshot_loaded.dialect_class == snapshot_before.dialect_class}") +# Verify table data survived the roundtrip +assert snapshot_loaded.dialect_class == snapshot_before.dialect_class +assert list(snapshot_loaded.tables.keys()) == list(snapshot_before.tables.keys()), ( + f"table names mismatch: {list(snapshot_loaded.tables.keys())} " + f"!= {list(snapshot_before.tables.keys())}" +) +for name in snapshot_before.tables: + before_t = snapshot_before.tables[name] + after_t = snapshot_loaded.tables[name] + assert [c.name for c in before_t.columns] == [c.name for c in after_t.columns], ( + f"columns mismatch in table '{name}'" + ) + for col_b, col_a in zip(before_t.columns, after_t.columns): + assert col_b.data_type.__class__.__name__ == col_a.data_type.__class__.__name__, ( + f"type mismatch in {name}.{col_b.name}" + ) +print(f"Table data roundtrip: all columns and types match") print(f"Added tables: {diff.added_tables}") print(f"Removed tables: {diff.removed_tables}") print(f"Modified tables: {diff.modified_tables}") From 7c56a730615e9a0d548896b77e11fed6d9ed04ae Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Wed, 24 Jun 2026 10:22:17 +0800 Subject: [PATCH 18/22] fix(tests): replace string data_type with DataType instances across 22 test files ColumnDefinition now validates that data_type must be a DataType instance. Replace all string data_type values (SERIAL, TEXT, INTEGER, etc.) with proper instance references. Also add PostgresCitextType, PostgresCubeType, PostgresLtreeType, PostgresRasterType missing extension DataType subclasses. Update security injection tests to verify TypeError at construction time instead of ValueError at format time. --- .../backend/impl/postgres/expression/types.py | 16 ++++++ .../extensions/test_bloom_integration.py | 27 ++++++---- .../extensions/test_btree_gin_integration.py | 25 ++++++--- .../extensions/test_btree_gist_integration.py | 25 ++++++--- .../extensions/test_citext_integration.py | 52 +++++++++++-------- .../extensions/test_cube_integration.py | 29 ++++++----- .../test_earthdistance_integration.py | 13 +++-- .../test_fuzzystrmatch_integration.py | 22 +++++--- .../extensions/test_hstore_integration.py | 8 ++- .../extensions/test_hypopg_integration.py | 18 ++++--- .../extensions/test_intarray_integration.py | 47 ++++++++++------- .../extensions/test_ltree_integration.py | 51 ++++++++++-------- .../extensions/test_orafce_integration.py | 32 +++++++----- .../extensions/test_pg_trgm_integration.py | 38 ++++++++------ .../extensions/test_pgcrypto_integration.py | 27 ++++++---- .../extensions/test_pgrouting_integration.py | 43 ++++++++------- .../extensions/test_pgvector_integration.py | 8 ++- .../extensions/test_postgis_integration.py | 45 +++++++++------- .../test_postgis_raster_integration.py | 19 ++++--- .../extensions/test_tablefunc_integration.py | 34 +++++++----- .../extensions/test_uuid_ossp_integration.py | 22 +++++--- .../test_dialect_security_integration.py | 20 +++---- .../backend/test_postgres_dialect_security.py | 21 ++++---- 23 files changed, 391 insertions(+), 251 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py index ed58ecb8..5a1508fd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/types.py @@ -333,6 +333,22 @@ class PostgresGeographyType(DataType, backend="postgres"): """PostGIS ``GEOGRAPHY`` — geodetic spatial type (PostGIS extension).""" +class PostgresCitextType(DataType, backend="postgres"): + """PostgreSQL ``CITEXT`` — case-insensitive text (citext extension).""" + + +class PostgresCubeType(DataType, backend="postgres"): + """PostgreSQL ``CUBE`` — multi-dimensional cube (cube extension).""" + + +class PostgresLtreeType(DataType, backend="postgres"): + """PostgreSQL ``LTREE`` — label tree (ltree extension).""" + + +class PostgresRasterType(DataType, backend="postgres"): + """PostgreSQL ``RASTER`` — raster (PostGIS raster extension).""" + + class PostgresVectorType(DataType, backend="postgres"): """pgvector ``VECTOR(n)`` — vector embedding (pgvector extension). diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py index 814d970b..b0509aff 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_bloom_integration.py @@ -33,6 +33,13 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) + TABLE_NAME = "test_bloom" ASYNC_TABLE_NAME = "test_bloom_async" @@ -55,13 +62,13 @@ def bloom_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="email", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="email", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -159,12 +166,12 @@ def test_bloom_index_with_options(self, bloom_env): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="code", data_type="TEXT"), + ColumnDefinition(name="code", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -232,13 +239,13 @@ async def async_bloom_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="email", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="email", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -336,12 +343,12 @@ async def test_async_bloom_index_with_options(self, async_bloom_env): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="code", data_type="TEXT"), + ColumnDefinition(name="code", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py index a94ded0a..a6fd3461 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gin_integration.py @@ -39,6 +39,15 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresArrayType, +) + @pytest.fixture def btree_gin_env(postgres_backend_single): @@ -51,14 +60,14 @@ def btree_gin_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="value", data_type="INTEGER"), - ColumnDefinition(name="tags", data_type="TEXT[]"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="value", data_type=IntegerType()), + ColumnDefinition(name="tags", data_type=PostgresArrayType(TextType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -206,14 +215,14 @@ async def async_btree_gin_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="value", data_type="INTEGER"), - ColumnDefinition(name="tags", data_type="TEXT[]"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="value", data_type=IntegerType()), + ColumnDefinition(name="tags", data_type=PostgresArrayType(TextType())), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py index 39636320..8e6fc040 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_btree_gist_integration.py @@ -41,6 +41,15 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + TextType, + TimestampType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) + @pytest.fixture def btree_gist_env(postgres_backend_single): @@ -53,14 +62,14 @@ def btree_gist_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="created_at", data_type="TIMESTAMP"), - ColumnDefinition(name="value", data_type="INTEGER"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="created_at", data_type=TimestampType()), + ColumnDefinition(name="value", data_type=IntegerType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -217,14 +226,14 @@ async def async_btree_gist_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="created_at", data_type="TIMESTAMP"), - ColumnDefinition(name="value", data_type="INTEGER"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="created_at", data_type=TimestampType()), + ColumnDefinition(name="value", data_type=IntegerType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py index c87b27d2..19277faf 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_citext_integration.py @@ -38,6 +38,14 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresCitextType, +) + # --- Helper functions --- @@ -47,12 +55,12 @@ def _setup_citext_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="CITEXT"), + ColumnDefinition(name="name", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -81,14 +89,14 @@ def _setup_citext_unique_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), ColumnDefinition( name="email", - data_type="CITEXT", + data_type=PostgresCitextType(), constraints=[ ColumnConstraint(ColumnConstraintType.UNIQUE), ], @@ -121,12 +129,12 @@ def _setup_citext_like_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="CITEXT"), + ColumnDefinition(name="name", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -158,12 +166,12 @@ def _setup_citext_users_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="CITEXT"), + ColumnDefinition(name="username", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -192,13 +200,13 @@ def _setup_citext_roles_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="CITEXT"), - ColumnDefinition(name="role", data_type="TEXT"), + ColumnDefinition(name="username", data_type=PostgresCitextType()), + ColumnDefinition(name="role", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -238,12 +246,12 @@ async def _async_setup_citext_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="CITEXT"), + ColumnDefinition(name="name", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -272,14 +280,14 @@ async def _async_setup_citext_unique_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), ColumnDefinition( name="email", - data_type="CITEXT", + data_type=PostgresCitextType(), constraints=[ ColumnConstraint(ColumnConstraintType.UNIQUE), ], @@ -312,12 +320,12 @@ async def _async_setup_citext_like_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="CITEXT"), + ColumnDefinition(name="name", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -349,12 +357,12 @@ async def _async_setup_citext_users_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="CITEXT"), + ColumnDefinition(name="username", data_type=PostgresCitextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -383,13 +391,13 @@ async def _async_setup_citext_roles_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="CITEXT"), - ColumnDefinition(name="role", data_type="TEXT"), + ColumnDefinition(name="username", data_type=PostgresCitextType()), + ColumnDefinition(name="role", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py index a7be5d5f..d2966b0d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_cube_integration.py @@ -38,6 +38,11 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresCubeType, + PostgresSerialType, +) + # --- Sync fixture and tests --- @@ -62,12 +67,12 @@ def cube_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) @@ -97,12 +102,12 @@ def cube_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) @@ -132,12 +137,12 @@ def cube_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) @@ -348,12 +353,12 @@ async def async_cube_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) @@ -383,12 +388,12 @@ async def async_cube_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) @@ -418,12 +423,12 @@ async def async_cube_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="c", data_type="CUBE"), + ColumnDefinition(name="c", data_type=PostgresCubeType()), ], if_not_exists=True, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py index 3af2872a..90298a2c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_earthdistance_integration.py @@ -29,6 +29,11 @@ from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresPointType, + PostgresSerialType, +) + TABLE_NAME = "test_earth" ASYNC_TABLE_NAME = "test_earth_async" @@ -51,12 +56,12 @@ def earthdistance_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="loc", data_type="POINT"), + ColumnDefinition(name="loc", data_type=PostgresPointType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -163,12 +168,12 @@ async def async_earthdistance_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="loc", data_type="POINT"), + ColumnDefinition(name="loc", data_type=PostgresPointType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py index 311d54ee..64be7549 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_fuzzystrmatch_integration.py @@ -36,6 +36,12 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) TABLE_NAME = "test_fuzzy_lev" @@ -49,12 +55,12 @@ def _setup_lev_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="word", data_type="TEXT"), + ColumnDefinition(name="word", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -86,12 +92,12 @@ def _setup_diff_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -133,12 +139,12 @@ async def _async_setup_lev_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="word", data_type="TEXT"), + ColumnDefinition(name="word", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -170,12 +176,12 @@ async def _async_setup_diff_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py index f8b99261..1530b978 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hstore_integration.py @@ -38,6 +38,10 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresHstoreType, +) # --- Helper functions --- @@ -48,12 +52,12 @@ def _make_hstore_columns(): return [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="data", data_type="HSTORE"), + ColumnDefinition(name="data", data_type=PostgresHstoreType()), ] diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py index d477159b..7cf7ab52 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_hypopg_integration.py @@ -33,6 +33,12 @@ ) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) @pytest.fixture @@ -46,13 +52,13 @@ def hypopg_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="email", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="email", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -262,13 +268,13 @@ async def async_hypopg_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="email", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="email", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py index 8a70b4ff..ccd5a613 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_intarray_integration.py @@ -39,6 +39,13 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresArrayType, +) # --- Helper functions --- @@ -49,12 +56,12 @@ def _setup_intarray_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -87,12 +94,12 @@ def _setup_intarray_overlap_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -125,12 +132,12 @@ def _setup_intarray_contained_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -163,12 +170,12 @@ def _setup_intarray_idx_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -201,12 +208,12 @@ def _setup_intarray_gin_table(backend, dialect, table, index_name): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -253,12 +260,12 @@ async def _async_setup_intarray_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -291,12 +298,12 @@ async def _async_setup_intarray_overlap_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -329,12 +336,12 @@ async def _async_setup_intarray_contained_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -367,12 +374,12 @@ async def _async_setup_intarray_idx_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, @@ -405,12 +412,12 @@ async def _async_setup_intarray_gin_table(backend, dialect, table, index_name): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="tags", data_type="INTEGER[]"), + ColumnDefinition(name="tags", data_type=PostgresArrayType(IntegerType())), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py index d0c56ecc..6a968c93 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_ltree_integration.py @@ -37,6 +37,13 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresLtreeType, +) # --- Sync fixture and tests --- @@ -63,12 +70,12 @@ def ltree_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -101,13 +108,13 @@ def ltree_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), - ColumnDefinition(name="label", data_type="TEXT"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(name="label", data_type=TextType()), ], if_not_exists=True, ) @@ -140,12 +147,12 @@ def ltree_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -172,12 +179,12 @@ def ltree_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -220,12 +227,12 @@ def ltree_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -569,12 +576,12 @@ async def async_ltree_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -607,13 +614,13 @@ async def async_ltree_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), - ColumnDefinition(name="label", data_type="TEXT"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), + ColumnDefinition(name="label", data_type=TextType()), ], if_not_exists=True, ) @@ -646,12 +653,12 @@ async def async_ltree_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -678,12 +685,12 @@ async def async_ltree_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) @@ -726,12 +733,12 @@ async def async_ltree_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="path", data_type="LTREE"), + ColumnDefinition(name="path", data_type=PostgresLtreeType()), ], if_not_exists=True, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py index 59463877..eb0243b8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_orafce_integration.py @@ -38,6 +38,14 @@ ensure_extension_installed, async_ensure_extension_installed, ) +from rhosocial.activerecord.backend.expression.types import ( + TextType, + DateType, + DecimalType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) TABLE_NAME = "test_orafce_dates" @@ -55,16 +63,16 @@ def orafce_backend(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="dt", data_type="DATE"), - ColumnDefinition(name="ref_dt", data_type="DATE"), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="discount", data_type="NUMERIC"), - ColumnDefinition(name="description", data_type="TEXT"), + ColumnDefinition(name="dt", data_type=DateType()), + ColumnDefinition(name="ref_dt", data_type=DateType()), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="discount", data_type=DecimalType()), + ColumnDefinition(name="description", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -316,16 +324,16 @@ async def async_orafce_backend(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="dt", data_type="DATE"), - ColumnDefinition(name="ref_dt", data_type="DATE"), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="discount", data_type="NUMERIC"), - ColumnDefinition(name="description", data_type="TEXT"), + ColumnDefinition(name="dt", data_type=DateType()), + ColumnDefinition(name="ref_dt", data_type=DateType()), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="discount", data_type=DecimalType()), + ColumnDefinition(name="description", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py index 29c0be98..b022fb98 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pg_trgm_integration.py @@ -36,6 +36,12 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) # Table names for sync tests @@ -56,12 +62,12 @@ def _setup_trgm_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -93,12 +99,12 @@ def _setup_trgm_ops_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -134,12 +140,12 @@ def _setup_trgm_idx_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -174,12 +180,12 @@ def _setup_trgm_word_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="description", data_type="TEXT"), + ColumnDefinition(name="description", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -219,12 +225,12 @@ async def _async_setup_trgm_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -256,12 +262,12 @@ async def _async_setup_trgm_ops_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -297,12 +303,12 @@ async def _async_setup_trgm_idx_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -337,12 +343,12 @@ async def _async_setup_trgm_word_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="description", data_type="TEXT"), + ColumnDefinition(name="description", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py index 5a6e331f..7005a7dd 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgcrypto_integration.py @@ -36,6 +36,13 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresByteaType, +) # Table names for sync tests @@ -52,13 +59,13 @@ def _setup_users_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="TEXT"), - ColumnDefinition(name="password_hash", data_type="TEXT"), + ColumnDefinition(name="username", data_type=TextType()), + ColumnDefinition(name="password_hash", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -93,12 +100,12 @@ def _setup_encrypt_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="secret_data", data_type="BYTEA"), + ColumnDefinition(name="secret_data", data_type=PostgresByteaType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -144,13 +151,13 @@ async def _async_setup_users_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="username", data_type="TEXT"), - ColumnDefinition(name="password_hash", data_type="TEXT"), + ColumnDefinition(name="username", data_type=TextType()), + ColumnDefinition(name="password_hash", data_type=TextType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -185,12 +192,12 @@ async def _async_setup_encrypt_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="secret_data", data_type="BYTEA"), + ColumnDefinition(name="secret_data", data_type=PostgresByteaType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py index 4452dc02..622b86d7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgrouting_integration.py @@ -30,6 +30,13 @@ ) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, + FloatType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) @pytest.fixture @@ -43,19 +50,19 @@ def pgrouting_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="source", data_type="INT"), - ColumnDefinition(name="target", data_type="INT"), - ColumnDefinition(name="cost", data_type="FLOAT"), - ColumnDefinition(name="reverse_cost", data_type="FLOAT"), - ColumnDefinition(name="x1", data_type="FLOAT"), - ColumnDefinition(name="y1", data_type="FLOAT"), - ColumnDefinition(name="x2", data_type="FLOAT"), - ColumnDefinition(name="y2", data_type="FLOAT"), + ColumnDefinition(name="source", data_type=IntegerType()), + ColumnDefinition(name="target", data_type=IntegerType()), + ColumnDefinition(name="cost", data_type=FloatType()), + ColumnDefinition(name="reverse_cost", data_type=FloatType()), + ColumnDefinition(name="x1", data_type=FloatType()), + ColumnDefinition(name="y1", data_type=FloatType()), + ColumnDefinition(name="x2", data_type=FloatType()), + ColumnDefinition(name="y2", data_type=FloatType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -172,19 +179,19 @@ async def async_pgrouting_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="source", data_type="INT"), - ColumnDefinition(name="target", data_type="INT"), - ColumnDefinition(name="cost", data_type="FLOAT"), - ColumnDefinition(name="reverse_cost", data_type="FLOAT"), - ColumnDefinition(name="x1", data_type="FLOAT"), - ColumnDefinition(name="y1", data_type="FLOAT"), - ColumnDefinition(name="x2", data_type="FLOAT"), - ColumnDefinition(name="y2", data_type="FLOAT"), + ColumnDefinition(name="source", data_type=IntegerType()), + ColumnDefinition(name="target", data_type=IntegerType()), + ColumnDefinition(name="cost", data_type=FloatType()), + ColumnDefinition(name="reverse_cost", data_type=FloatType()), + ColumnDefinition(name="x1", data_type=FloatType()), + ColumnDefinition(name="y1", data_type=FloatType()), + ColumnDefinition(name="x2", data_type=FloatType()), + ColumnDefinition(name="y2", data_type=FloatType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py index 8a775349..1541c41b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_pgvector_integration.py @@ -37,6 +37,10 @@ from rhosocial.activerecord.backend.expression.query_parts import OrderByClause from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresVectorType, +) # --- Helper: common VECTOR(3) column definitions --- @@ -45,7 +49,7 @@ def _id_column(): return ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], @@ -53,7 +57,7 @@ def _id_column(): def _embedding_column(dim=3): - return ColumnDefinition(name="embedding", data_type=f"VECTOR({dim})") + return ColumnDefinition(name="embedding", data_type=PostgresVectorType(dim)) def _vector_literal(value, vector_type="vector"): diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py index aefaf055..a0a89473 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_integration.py @@ -33,6 +33,11 @@ from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresGeometryType, + PostgresGeographyType, +) # --- Helper functions --- @@ -43,12 +48,12 @@ def _setup_geo_points_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -80,12 +85,12 @@ def _setup_geo_distance_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -117,12 +122,12 @@ def _setup_geo_contains_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POLYGON, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -160,12 +165,12 @@ def _setup_geo_index_table(backend, dialect, table, index_name): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -207,12 +212,12 @@ def _setup_geog_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geog", data_type="GEOGRAPHY(POINT, 4326)"), + ColumnDefinition(name="geog", data_type=PostgresGeographyType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -254,12 +259,12 @@ async def _async_setup_geo_points_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -291,12 +296,12 @@ async def _async_setup_geo_distance_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -328,12 +333,12 @@ async def _async_setup_geo_contains_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POLYGON, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -371,12 +376,12 @@ async def _async_setup_geo_index_table(backend, dialect, table, index_name): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geom", data_type="GEOMETRY(POINT, 4326)"), + ColumnDefinition(name="geom", data_type=PostgresGeometryType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -418,12 +423,12 @@ async def _async_setup_geog_table(backend, dialect, table): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="geog", data_type="GEOGRAPHY(POINT, 4326)"), + ColumnDefinition(name="geog", data_type=PostgresGeographyType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py index 0f1fef5d..e5b462e2 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_postgis_raster_integration.py @@ -36,6 +36,13 @@ ) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, + PostgresRasterType, +) TABLE_NAME = "test_raster_data" @@ -83,13 +90,13 @@ def raster_env(postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="rast", data_type="RASTER"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="rast", data_type=PostgresRasterType()), ] create_expr = CreateTableExpression( dialect=dialect, @@ -202,13 +209,13 @@ async def async_raster_env(async_postgres_backend_single): columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="name", data_type="TEXT"), - ColumnDefinition(name="rast", data_type="RASTER"), + ColumnDefinition(name="name", data_type=TextType()), + ColumnDefinition(name="rast", data_type=PostgresRasterType()), ] create_expr = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py index b5e58b99..7b1fa9df 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_tablefunc_integration.py @@ -36,6 +36,12 @@ ) from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal +from rhosocial.activerecord.backend.expression.types import ( + TextType, IntegerType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresSerialType, +) from rhosocial.activerecord.backend.impl.postgres.functions.tablefunc import ( normal_rand, crosstab, @@ -56,14 +62,14 @@ def tablefunc_env(postgres_backend_single): sales_columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="month", data_type="TEXT"), - ColumnDefinition(name="category", data_type="TEXT"), - ColumnDefinition(name="amount", data_type="INT"), + ColumnDefinition(name="month", data_type=TextType()), + ColumnDefinition(name="category", data_type=TextType()), + ColumnDefinition(name="amount", data_type=IntegerType()), ] create_sales = CreateTableExpression( dialect=dialect, @@ -96,13 +102,13 @@ def tablefunc_env(postgres_backend_single): tree_columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="node_name", data_type="TEXT"), - ColumnDefinition(name="parent_id", data_type="INT"), + ColumnDefinition(name="node_name", data_type=TextType()), + ColumnDefinition(name="parent_id", data_type=IntegerType()), ] create_tree = CreateTableExpression( dialect=dialect, @@ -334,14 +340,14 @@ async def async_tablefunc_env(async_postgres_backend_single): sales_columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="month", data_type="TEXT"), - ColumnDefinition(name="category", data_type="TEXT"), - ColumnDefinition(name="amount", data_type="INT"), + ColumnDefinition(name="month", data_type=TextType()), + ColumnDefinition(name="category", data_type=TextType()), + ColumnDefinition(name="amount", data_type=IntegerType()), ] create_sales = CreateTableExpression( dialect=dialect, @@ -374,13 +380,13 @@ async def async_tablefunc_env(async_postgres_backend_single): tree_columns = [ ColumnDefinition( name="id", - data_type="SERIAL", + data_type=PostgresSerialType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="node_name", data_type="TEXT"), - ColumnDefinition(name="parent_id", data_type="INT"), + ColumnDefinition(name="node_name", data_type=TextType()), + ColumnDefinition(name="parent_id", data_type=IntegerType()), ] create_tree = CreateTableExpression( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py index 9f409c00..388bee98 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/extensions/test_uuid_ossp_integration.py @@ -33,6 +33,12 @@ from rhosocial.activerecord.backend.expression.statements import ValuesSource from rhosocial.activerecord.backend.expression.core import Literal, FunctionCall from rhosocial.activerecord.backend.expression.predicates import ComparisonPredicate +from rhosocial.activerecord.backend.expression.types import ( + TextType, +) +from rhosocial.activerecord.backend.impl.postgres.expression.types import ( + PostgresUUIDType, +) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType @@ -71,7 +77,7 @@ def uuid_ossp_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="UUID", + data_type=PostgresUUIDType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ColumnConstraint( @@ -80,7 +86,7 @@ def uuid_ossp_env(postgres_backend_single): ), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ], if_not_exists=True, ) @@ -107,12 +113,12 @@ def uuid_ossp_env(postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="UUID", + data_type=PostgresUUIDType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="label", data_type="TEXT"), + ColumnDefinition(name="label", data_type=TextType()), ], if_not_exists=True, ) @@ -276,7 +282,7 @@ async def async_uuid_ossp_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="UUID", + data_type=PostgresUUIDType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ColumnConstraint( @@ -285,7 +291,7 @@ async def async_uuid_ossp_env(async_postgres_backend_single): ), ], ), - ColumnDefinition(name="name", data_type="TEXT"), + ColumnDefinition(name="name", data_type=TextType()), ], if_not_exists=True, ) @@ -312,12 +318,12 @@ async def async_uuid_ossp_env(async_postgres_backend_single): columns=[ ColumnDefinition( name="id", - data_type="UUID", + data_type=PostgresUUIDType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY), ], ), - ColumnDefinition(name="label", data_type="TEXT"), + ColumnDefinition(name="label", data_type=TextType()), ], if_not_exists=True, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py index 6f03f418..f8fa1597 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_dialect_security_integration.py @@ -62,20 +62,16 @@ def test_valid_data_type_works(self, postgres_backend, test_table_for_data_type) """) # noqa: F541 assert result is not None - def test_malicious_data_type_rejected_at_dialect_level(self, postgres_backend): - """Test that malicious data type is rejected at dialect level before DB execution.""" + def test_malicious_data_type_rejected_at_construction(self, postgres_backend): + """Test that malicious data type string is rejected at ColumnDefinition construction.""" from rhosocial.activerecord.backend.expression.statements import ColumnDefinition - dialect = postgres_backend.dialect - - # This should raise ValueError before reaching the database - col_def = ColumnDefinition( - name="test_col", - data_type="VARCHAR(255); DROP TABLE users--", - ) - - with pytest.raises(ValueError, match="Invalid data type"): - dialect.format_column_definition(col_def) + # ColumnDefinition now rejects string data_type values at construction time + with pytest.raises(TypeError, match="data_type must be a DataType instance"): + ColumnDefinition( + name="test_col", + data_type="VARCHAR(255); DROP TABLE users--", + ) class TestPostgresExcludeConstraintSecurityIntegration: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py index fe763442..dca80715 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_dialect_security.py @@ -17,6 +17,7 @@ TableConstraint, TableConstraintType, ) +from rhosocial.activerecord.backend.expression.types import VarCharType from typing import Tuple, Any # noqa: F401 @@ -41,25 +42,23 @@ def test_postgres_validate_data_type(dialect): def test_postgres_format_column_definition_data_type_validation(dialect): - """Test column definition validates data_type.""" + """Test column definition formats data_type correctly.""" col_def = ColumnDefinition( name="test_col", - data_type="VARCHAR(255)", + data_type=VarCharType(255), ) sql, params = dialect.format_column_definition(col_def) assert "VARCHAR(255)" in sql -def test_postgres_format_column_definition_data_type_rejects_injection(dialect): - """Test that malicious data_type is rejected.""" - col_def = ColumnDefinition( - name="test_col", - data_type="VARCHAR(255); DROP TABLE users--", - ) - - with pytest.raises(ValueError, match="Invalid data type"): - dialect.format_column_definition(col_def) +def test_postgres_column_definition_rejects_string_data_type(dialect): + """Test that ColumnDefinition rejects string data_type at construction.""" + with pytest.raises(TypeError, match="data_type must be a DataType instance"): + ColumnDefinition( + name="test_col", + data_type="VARCHAR(255); DROP TABLE users--", + ) def test_postgres_format_default_constraint_string_escaping(dialect): From 3ee0c2807ada8ae6fc0ce4bc4515775e61919386 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Wed, 24 Jun 2026 10:22:27 +0800 Subject: [PATCH 19/22] docs: add changelog fragment for PR #46 --- changelog.d/46.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/46.added.md diff --git a/changelog.d/46.added.md b/changelog.d/46.added.md new file mode 100644 index 00000000..cf118163 --- /dev/null +++ b/changelog.d/46.added.md @@ -0,0 +1 @@ +Added PostgreSQL-specific DataType subclasses and schema-diff support with `PostgresSchemaDiffer` for accurate schema introspection and column comparison. From 18b65efad51943a8470fdd549aae61e296830f01 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Wed, 24 Jun 2026 10:41:45 +0800 Subject: [PATCH 20/22] fix(tests): replace positional string data_type with DataType instances in 6 more files --- tests/providers/partition.py | 17 +++++-- .../backend/postgres/test_ddl_expressions.py | 26 ++++++---- .../test_property_graph_query_scenarios.py | 51 ++++++++++--------- .../feature/backend/test_create_table_like.py | 11 ++-- .../test_partition_advanced_operations.py | 34 ++++++++----- .../backend/test_partition_operations.py | 20 +++++--- 6 files changed, 95 insertions(+), 64 deletions(-) diff --git a/tests/providers/partition.py b/tests/providers/partition.py index 2f99935c..64a73d68 100644 --- a/tests/providers/partition.py +++ b/tests/providers/partition.py @@ -25,6 +25,13 @@ from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord from rhosocial.activerecord.testsuite.feature.partition.interfaces import IPartitionProvider +from rhosocial.activerecord.backend.expression.types import ( + BigIntType, IntegerType, TextType, TimestampType, DecimalType, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnConstraint, ColumnConstraintType, +) + from .scenarios import get_enabled_scenarios, get_scenario @@ -245,11 +252,11 @@ def _create_parent_sql(self, dialect): dialect=dialect, table=self.TABLE_NAME, columns=[ - ColumnDefinition("id", "BIGINT GENERATED BY DEFAULT AS IDENTITY"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), - ColumnDefinition("tenant_id", "INTEGER NOT NULL"), - ColumnDefinition("payload", "TEXT"), - ColumnDefinition("amount", "NUMERIC(12, 2)"), + ColumnDefinition("id", BigIntType(), dialect_options={"identity": "BY DEFAULT"}), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("tenant_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType()), + ColumnDefinition("amount", DecimalType(12, 2)), ], partition=PartitionClause( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py index 28c02950..e304adc8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_ddl_expressions.py @@ -32,6 +32,12 @@ PostgresVacuumExpression, # noqa: F401 PostgresAnalyzeExpression, # noqa: F401 ) +from rhosocial.activerecord.backend.expression.types import ( + BigIntType, TextType, TimestampType, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnConstraint, ColumnConstraintType, +) from rhosocial.activerecord.backend.impl.postgres.mixins.dml.extended_statistics import ( PostgresExtendedStatisticsMixin, # noqa: F401 ) @@ -242,8 +248,8 @@ def test_create_range_partitioned_parent_table(self, dialect): dialect=dialect, table="events", columns=[ - ColumnDefinition("id", "BIGINT"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), + ColumnDefinition("id", BigIntType()), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -263,8 +269,8 @@ def test_create_list_partitioned_parent_table(self, dialect): dialect=dialect, table="events", columns=[ - ColumnDefinition("id", "BIGINT"), - ColumnDefinition("status", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType()), + ColumnDefinition("status", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -283,7 +289,7 @@ def test_create_hash_partitioned_parent_table_pg10(self): expr = CreateTableExpression( dialect=dialect, table="events", - columns=[ColumnDefinition("tenant_id", "BIGINT NOT NULL")], + columns=[ColumnDefinition("tenant_id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)])], partition=PartitionClause( dialect=dialect, method=PartitionStrategy.HASH, @@ -300,7 +306,7 @@ def test_create_hash_partitioned_parent_table_pg11(self): expr = CreateTableExpression( dialect=dialect, table="events", - columns=[ColumnDefinition("tenant_id", "BIGINT NOT NULL")], + columns=[ColumnDefinition("tenant_id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)])], partition=PartitionClause( dialect=dialect, method=PartitionStrategy.HASH, @@ -318,7 +324,7 @@ def test_create_partitioned_parent_table_pg9(self): expr = CreateTableExpression( dialect=dialect, table="events", - columns=[ColumnDefinition("created_at", "TIMESTAMP NOT NULL")], + columns=[ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)])], partition=PartitionClause( dialect=dialect, method=PartitionStrategy.RANGE, @@ -344,9 +350,9 @@ def test_multi_column_range_partitioned_parent_table(self, dialect): dialect=dialect, table="tenanted_events", columns=[ - ColumnDefinition("tenant_id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), - ColumnDefinition("payload", "TEXT"), + ColumnDefinition("tenant_id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType()), ], partition=PartitionClause( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py index be4e111c..49f5e03c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_property_graph_query_scenarios.py @@ -26,6 +26,9 @@ from rhosocial.activerecord.backend.expression.core import Column from rhosocial.activerecord.backend.expression.query_parts import JoinExpression, JoinType from rhosocial.activerecord.backend.expression.literals import Identifier +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, TextType, +) GRAPH_NAME = "social_graph" @@ -42,48 +45,48 @@ def social_data(postgres_backend): backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) people_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", "TEXT"), - ColumnDefinition("email", "TEXT"), - ColumnDefinition("city", "TEXT"), + ColumnDefinition("name", TextType()), + ColumnDefinition("email", TextType()), + ColumnDefinition("city", TextType()), ] backend.execute(*CreateTableExpression(dialect, "people", people_cols).to_sql()) follows_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("follower_id", "INTEGER", + ColumnDefinition("follower_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("followed_id", "INTEGER", + ColumnDefinition("followed_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("since", "TEXT"), + ColumnDefinition("since", TextType()), ] backend.execute(*CreateTableExpression(dialect, "follows", follows_cols).to_sql()) posts_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("author_id", "INTEGER", + ColumnDefinition("author_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("content", "TEXT"), - ColumnDefinition("created_at", "TEXT"), + ColumnDefinition("content", TextType()), + ColumnDefinition("created_at", TextType()), ] backend.execute(*CreateTableExpression(dialect, "posts", posts_cols).to_sql()) likes_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("user_id", "INTEGER", + ColumnDefinition("user_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("post_id", "INTEGER", + ColumnDefinition("post_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("posts", ["id"]))]), - ColumnDefinition("created_at", "TEXT"), + ColumnDefinition("created_at", TextType()), ] backend.execute(*CreateTableExpression(dialect, "likes", likes_cols).to_sql()) @@ -324,24 +327,24 @@ async def async_social_data(self, async_postgres_backend): await backend.execute(*DropTableExpression(dialect, t, if_exists=True, cascade=True).to_sql()) people_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("name", "TEXT"), - ColumnDefinition("email", "TEXT"), - ColumnDefinition("city", "TEXT"), + ColumnDefinition("name", TextType()), + ColumnDefinition("email", TextType()), + ColumnDefinition("city", TextType()), ] await backend.execute(*CreateTableExpression(dialect, "people", people_cols).to_sql()) follows_cols = [ - ColumnDefinition("id", "INTEGER", + ColumnDefinition("id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.PRIMARY_KEY)]), - ColumnDefinition("follower_id", "INTEGER", + ColumnDefinition("follower_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("followed_id", "INTEGER", + ColumnDefinition("followed_id", IntegerType(), constraints=[ColumnConstraint(ColumnConstraintType.FOREIGN_KEY, foreign_key_reference=("people", ["id"]))]), - ColumnDefinition("since", "TEXT"), + ColumnDefinition("since", TextType()), ] await backend.execute(*CreateTableExpression(dialect, "follows", follows_cols).to_sql()) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py index 2b9afaff..f8485637 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_create_table_like.py @@ -8,6 +8,9 @@ import pytest # noqa: F401 from rhosocial.activerecord.backend.expression import CreateTableExpression, ColumnDefinition from rhosocial.activerecord.backend.expression.statements import ColumnConstraint, ColumnConstraintType +from rhosocial.activerecord.backend.expression.types import ( + IntegerType, VarCharType, +) from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect @@ -204,10 +207,10 @@ def test_like_ignores_columns(self): """Test that LIKE syntax ignores columns parameter.""" dialect = PostgresDialect() columns = [ - ColumnDefinition("id", "INTEGER", constraints=[ + ColumnDefinition("id", IntegerType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY) ]), - ColumnDefinition("name", "VARCHAR(255)") + ColumnDefinition("name", VarCharType(255)) ] create_expr = CreateTableExpression( dialect=dialect, @@ -258,10 +261,10 @@ def test_fallback_to_base_when_no_like(self): """Test that base implementation is used when LIKE is not specified.""" dialect = PostgresDialect() columns = [ - ColumnDefinition("id", "INTEGER", constraints=[ + ColumnDefinition("id", IntegerType(), constraints=[ ColumnConstraint(ColumnConstraintType.PRIMARY_KEY) ]), - ColumnDefinition("name", "VARCHAR(255)", constraints=[ + ColumnDefinition("name", VarCharType(255), constraints=[ ColumnConstraint(ColumnConstraintType.NOT_NULL) ]) ] diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py index 6f0a7343..fd4677d8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_advanced_operations.py @@ -32,6 +32,12 @@ PostgresDetachPartitionExpression, PostgresPartitionMetadataExpression, ) +from rhosocial.activerecord.backend.expression.types import ( + BigIntType, TextType, TimestampType, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnConstraint, ColumnConstraintType, +) LIST_PARENT = "ar_partition_adv_list_events" @@ -59,9 +65,9 @@ def _create_list_parent_expression(dialect): dialect=dialect, table=LIST_PARENT, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("status", "TEXT NOT NULL"), - ColumnDefinition("payload", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("status", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -87,9 +93,9 @@ def _create_hash_parent_expression(dialect): dialect=dialect, table=HASH_PARENT, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("bucket", "BIGINT NOT NULL"), - ColumnDefinition("payload", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("bucket", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -115,9 +121,9 @@ def _create_range_parent_expression(dialect): dialect=dialect, table=RANGE_PARENT, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), - ColumnDefinition("payload", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, @@ -318,9 +324,9 @@ def test_default_attach_and_detach_workflow(self, postgres_backend, postgres_ran dialect=dialect, table=default_partition_name, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), - ColumnDefinition("payload", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], ) postgres_backend.execute(*create_default.to_sql()) @@ -378,8 +384,8 @@ def test_metadata_include_partitions_false_and_schema_filter(self, postgres_back dialect=dialect, table=TableExpression(dialect, SCHEMA_PARENT, schema_name=schema), columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], partition=PartitionClause( dialect=dialect, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py index 5ded526f..fed659ae 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_operations.py @@ -51,6 +51,12 @@ ensure_extension_installed, async_ensure_extension_installed, ) +from rhosocial.activerecord.backend.expression.types import ( + BigIntType, TextType, TimestampType, +) +from rhosocial.activerecord.backend.expression.statements import ( + ColumnConstraint, ColumnConstraintType, +) PARTITION_TABLES = ( @@ -76,9 +82,9 @@ def _create_partitioned_parent_sql(dialect, table_name: str): dialect=dialect, table=table_name, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP NOT NULL"), - ColumnDefinition("payload", "TEXT"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType()), ], partition=PartitionClause( dialect=dialect, @@ -200,10 +206,10 @@ def _create_production_parent_sql(dialect): dialect=dialect, table=PRODUCTION_PARTITION_TABLE, columns=[ - ColumnDefinition("id", "BIGINT NOT NULL"), - ColumnDefinition("created_at", "TIMESTAMP(6) NOT NULL"), - ColumnDefinition("tenant_id", "BIGINT NOT NULL"), - ColumnDefinition("payload", "TEXT NOT NULL"), + ColumnDefinition("id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("created_at", TimestampType(6), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("tenant_id", BigIntType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), + ColumnDefinition("payload", TextType(), constraints=[ColumnConstraint(ColumnConstraintType.NOT_NULL)]), ], table_constraints=[ TableConstraint( From bafef4075e821e6c430b435689774314855806c8 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Wed, 24 Jun 2026 10:54:50 +0800 Subject: [PATCH 21/22] fix(types): register PostgresCitextType, PostgresCubeType, PostgresLtreeType, PostgresRasterType formatters --- .../mixins/types/data_type_formatting.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index 880f9093..cf488413 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -83,8 +83,11 @@ PostgresXID8Type, PostgresXIDType, PostgresXMLType, - PostgresCidrType, PostgresCharacterVaryingType, + PostgresCitextType, + PostgresCubeType, + PostgresLtreeType, + PostgresRasterType, ) if TYPE_CHECKING: @@ -303,6 +306,22 @@ def format_data_type_geography(self, data_type) -> Tuple[str, tuple]: def format_data_type_vector(self, data_type) -> Tuple[str, tuple]: return f"VECTOR({data_type.dim})", () + @DDLTypeMixin.handles(PostgresCitextType) + def format_data_type_citext(self, data_type) -> Tuple[str, tuple]: + return "CITEXT", () + + @DDLTypeMixin.handles(PostgresCubeType) + def format_data_type_cube(self, data_type) -> Tuple[str, tuple]: + return "CUBE", () + + @DDLTypeMixin.handles(PostgresLtreeType) + def format_data_type_ltree(self, data_type) -> Tuple[str, tuple]: + return "LTREE", () + + @DDLTypeMixin.handles(PostgresRasterType) + def format_data_type_raster(self, data_type) -> Tuple[str, tuple]: + return "RASTER", () + @DDLTypeMixin.handles(ArrayType) def format_data_type_array(self, data_type: ArrayType) -> Tuple[str, tuple]: elem = data_type.element_type From bfa91deb9a8bb9b4c32a93306ee3feb127ab5fda Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Wed, 24 Jun 2026 11:02:50 +0800 Subject: [PATCH 22/22] fix: restore PostgresCidrType import accidentally removed in type formatter registration --- .../backend/impl/postgres/mixins/types/data_type_formatting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py index cf488413..bb41a98e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/types/data_type_formatting.py @@ -83,6 +83,7 @@ PostgresXID8Type, PostgresXIDType, PostgresXMLType, + PostgresCidrType, PostgresCharacterVaryingType, PostgresCitextType, PostgresCubeType,