From 534309d9ca4e6b3098aa8ef5c85d16f3f85f4b32 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 00:07:21 +0800 Subject: [PATCH 01/19] refactor(postgres): split types into type_values and expression namespaces Every BaseExpression subclass must live under the backend's expression namespace, and the name `types` should mean exactly one thing per layer. Two problems are fixed here. First, PostgresEnumType is an expression (it renders a type reference for casts and column definitions, and its own docstring says it is not for DDL), but it was defined in types/enum.py alongside a manager. Move it to expression/enum_.py, matching the enum_.py naming already used by the core expression types package. EnumTypeManager is a DDL facade, so it moves to a top-level enum_type_manager.py, alongside transaction.py and its TransactionManager. Second, types/ held 44 domain value types (PostgresVector, PostgresMoney, TsQueryNode, ...) that have nothing to do with expressions. Renaming the package to type_values/ frees `types` to mean DataType expressions, so the boundary becomes checkable. The full impl.postgres. API is unchanged; only the impl.postgres.types.* path moves, with no compatibility shim. Imports are rewritten by resolving each relative import against the filesystem, so the 17 sibling `from .types import` sites in expression/, protocols/, mixins/ and explain/ - which resolve to their own DataType, Protocol, mixin and DTO modules - are deliberately left untouched. Adds a guardrail test that imports every backend module and asserts that no BaseExpression subclass is defined outside the expression namespace. --- .../postgres_specific_features/dialect.md | 6 +- .../postgres_specific_features/field_types.md | 2 +- .../postgres_specific_features/dialect.md | 6 +- .../postgres_specific_features/field_types.md | 2 +- .../backend/impl/postgres/__init__.py | 30 ++-- .../backend/impl/postgres/adapters/base.py | 2 +- .../impl/postgres/adapters/bit_string.py | 2 +- .../impl/postgres/adapters/geometric.py | 2 +- .../backend/impl/postgres/adapters/hstore.py | 2 +- .../backend/impl/postgres/adapters/json.py | 2 +- .../impl/postgres/adapters/monetary.py | 2 +- .../impl/postgres/adapters/network_address.py | 2 +- .../postgres/adapters/object_identifier.py | 2 +- .../backend/impl/postgres/adapters/pg_lsn.py | 2 +- .../impl/postgres/adapters/pgvector.py | 2 +- .../backend/impl/postgres/adapters/postgis.py | 2 +- .../backend/impl/postgres/adapters/range.py | 2 +- .../impl/postgres/adapters/text_search.py | 2 +- .../backend/impl/postgres/adapters/xml.py | 2 +- .../backend/impl/postgres/backend/base.py | 16 +- .../{types/enum.py => enum_type_manager.py} | 163 +---------------- .../backend/impl/postgres/expression/enum_.py | 168 ++++++++++++++++++ .../backend/impl/postgres/functions/hstore.py | 2 +- .../backend/impl/postgres/functions/json.py | 14 +- .../impl/postgres/functions/pgvector.py | 2 +- .../impl/postgres/functions/postgis.py | 2 +- .../backend/impl/postgres/functions/range.py | 2 +- .../backend/impl/postgres/functions/xml.py | 2 +- .../{types => type_values}/__init__.py | 10 +- .../{types => type_values}/bit_string.py | 2 +- .../{types => type_values}/constants.py | 4 +- .../{types => type_values}/geometric.py | 2 +- .../postgres/{types => type_values}/hstore.py | 2 +- .../postgres/{types => type_values}/json.py | 2 +- .../{types => type_values}/monetary.py | 2 +- .../{types => type_values}/network_address.py | 2 +- .../object_identifier.py | 2 +- .../postgres/{types => type_values}/pg_lsn.py | 2 +- .../{types => type_values}/pgvector.py | 2 +- .../{types => type_values}/postgis.py | 2 +- .../postgres/{types => type_values}/range.py | 4 +- .../{types => type_values}/text_search.py | 2 +- .../postgres/{types => type_values}/xml.py | 2 +- .../extensions/test_pgvector_integration.py | 2 +- .../postgres/test_bitstring_integration.py | 2 +- .../backend/postgres/test_bitstring_types.py | 2 +- .../backend/postgres/test_enum_integration.py | 4 +- .../backend/postgres/test_enum_types.py | 2 +- .../backend/postgres/test_geometry_types.py | 2 +- .../backend/postgres/test_hstore_types.py | 2 +- .../backend/postgres/test_jsonpath_types.py | 2 +- .../backend/postgres/test_lsn_integration.py | 2 +- .../backend/postgres/test_lsn_types.py | 2 +- .../backend/postgres/test_macaddr8_types.py | 2 +- .../postgres/test_money_integration.py | 2 +- .../backend/postgres/test_money_types.py | 2 +- .../test_network_address_integration.py | 2 +- .../backend/postgres/test_oid_integration.py | 2 +- .../backend/postgres/test_oid_types.py | 2 +- .../backend/postgres/test_pgvector_types.py | 2 +- .../backend/postgres/test_postgis_types.py | 2 +- .../test_postgres_hstore_functions.py | 2 +- .../test_postgres_pgvector_functions.py | 2 +- .../test_postgres_postgis_functions.py | 2 +- .../backend/postgres/test_range_functions.py | 2 +- .../backend/postgres/test_range_types.py | 2 +- .../postgres/test_range_types_integration.py | 2 +- .../backend/postgres/test_textsearch_types.py | 2 +- .../backend/postgres/test_type_casting.py | 4 +- .../backend/postgres/test_xml_integration.py | 2 +- .../backend/postgres/test_xml_types.py | 2 +- .../feature/backend/test_adapters.py | 2 +- .../backend/test_expression_namespace.py | 126 +++++++++++++ .../types/test_postgres_type_protocol.py | 6 +- 74 files changed, 410 insertions(+), 271 deletions(-) rename src/rhosocial/activerecord/backend/impl/postgres/{types/enum.py => enum_type_manager.py} (56%) create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/__init__.py (97%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/bit_string.py (97%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/constants.py (98%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/geometric.py (99%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/hstore.py (98%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/json.py (98%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/monetary.py (98%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/network_address.py (98%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/object_identifier.py (99%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/pg_lsn.py (98%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/pgvector.py (98%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/postgis.py (98%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/range.py (99%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/text_search.py (99%) rename src/rhosocial/activerecord/backend/impl/postgres/{types => type_values}/xml.py (96%) create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py diff --git a/docs/en_US/postgres_specific_features/dialect.md b/docs/en_US/postgres_specific_features/dialect.md index f6c95709..ccd64547 100644 --- a/docs/en_US/postgres_specific_features/dialect.md +++ b/docs/en_US/postgres_specific_features/dialect.md @@ -9,7 +9,7 @@ PostgreSQL uses the `::` operator for type casting, which is PostgreSQL-specific ```python from rhosocial.activerecord.backend.expression import Column from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types.constants import INTEGER, NUMERIC, MONEY +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import INTEGER, NUMERIC, MONEY dialect = PostgresDialect() @@ -33,7 +33,7 @@ PostgreSQL supports chained type conversions: ```python from rhosocial.activerecord.backend.expression import Column from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types.constants import MONEY, NUMERIC, FLOAT8 +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import MONEY, NUMERIC, FLOAT8 dialect = PostgresDialect() col = Column(dialect, "amount") @@ -63,7 +63,7 @@ When using incompatible type conversions, the system will emit a warning to guid For convenience, PostgreSQL type constants are provided: ```python -from rhosocial.activerecord.backend.impl.postgres.types.constants import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import ( # Numeric types SMALLINT, INTEGER, BIGINT, NUMERIC, DECIMAL, REAL, DOUBLE_PRECISION, FLOAT8, FLOAT4, diff --git a/docs/en_US/postgres_specific_features/field_types.md b/docs/en_US/postgres_specific_features/field_types.md index faf6cc32..109aee70 100644 --- a/docs/en_US/postgres_specific_features/field_types.md +++ b/docs/en_US/postgres_specific_features/field_types.md @@ -5,7 +5,7 @@ For convenience, PostgreSQL type constants are provided. These constants are string values that can be used in type casting: ```python -from rhosocial.activerecord.backend.impl.postgres.types.constants import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import ( # Numeric types SMALLINT, INTEGER, BIGINT, NUMERIC, DECIMAL, REAL, DOUBLE_PRECISION, FLOAT8, FLOAT4, diff --git a/docs/zh_CN/postgres_specific_features/dialect.md b/docs/zh_CN/postgres_specific_features/dialect.md index cadc1d1b..4a1c8d92 100644 --- a/docs/zh_CN/postgres_specific_features/dialect.md +++ b/docs/zh_CN/postgres_specific_features/dialect.md @@ -9,7 +9,7 @@ PostgreSQL 使用 `::` 操作符进行类型转换,这是 PostgreSQL 特有的 ```python from rhosocial.activerecord.backend.expression import Column from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types.constants import INTEGER, NUMERIC, MONEY +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import INTEGER, NUMERIC, MONEY dialect = PostgresDialect() @@ -33,7 +33,7 @@ PostgreSQL 支持链式类型转换: ```python from rhosocial.activerecord.backend.expression import Column from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types.constants import MONEY, NUMERIC, FLOAT8 +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import MONEY, NUMERIC, FLOAT8 dialect = PostgresDialect() col = Column(dialect, "amount") @@ -63,7 +63,7 @@ col.cast("money").cast("numeric").cast("float8") 为方便使用,提供了 PostgreSQL 类型常量: ```python -from rhosocial.activerecord.backend.impl.postgres.types.constants import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import ( # 数值类型 SMALLINT, INTEGER, BIGINT, NUMERIC, DECIMAL, REAL, DOUBLE_PRECISION, FLOAT8, FLOAT4, diff --git a/docs/zh_CN/postgres_specific_features/field_types.md b/docs/zh_CN/postgres_specific_features/field_types.md index 4f8dc73d..80b97d52 100644 --- a/docs/zh_CN/postgres_specific_features/field_types.md +++ b/docs/zh_CN/postgres_specific_features/field_types.md @@ -5,7 +5,7 @@ 为方便使用,提供了 PostgreSQL 类型常量。这些常量是字符串值,可在类型转换中使用: ```python -from rhosocial.activerecord.backend.impl.postgres.types.constants import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import ( # 数值类型 SMALLINT, INTEGER, BIGINT, NUMERIC, DECIMAL, REAL, DOUBLE_PRECISION, FLOAT8, FLOAT4, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py index 1bd9a409..0ee95195 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py @@ -26,7 +26,7 @@ PostgresStorageParameterValueType, ) from .transaction import PostgresTransactionManager, AsyncPostgresTransactionManager -from .types import PostgresEnumType +from .expression.enum_ import PostgresEnumType from .expression.types import ( PostgresBigSerialType, PostgresBitType, @@ -125,11 +125,11 @@ PostgresRangeAdapter, PostgresMultirangeAdapter, ) -from .types.range import ( +from .type_values.range import ( PostgresRange, PostgresMultirange, ) -from .types.geometric import ( +from .type_values.geometric import ( Point, Line, LineSegment, @@ -139,22 +139,22 @@ Circle, ) from .adapters.geometric import PostgresGeometryAdapter -from .types.bit_string import PostgresBitString +from .type_values.bit_string import PostgresBitString from .adapters.bit_string import PostgresBitStringAdapter -from .types.monetary import PostgresMoney +from .type_values.monetary import PostgresMoney from .adapters.monetary import PostgresMoneyAdapter -from .types.xml import PostgresXML +from .type_values.xml import PostgresXML from .adapters.xml import PostgresXMLAdapter -from .types.network_address import PostgresMacaddr, PostgresMacaddr8 +from .type_values.network_address import PostgresMacaddr, PostgresMacaddr8 from .adapters.network_address import PostgresMacaddrAdapter, PostgresMacaddr8Adapter from .adapters.uuid import PostgresUUIDAdapter -from .types.pgvector import PostgresVector +from .type_values.pgvector import PostgresVector from .adapters.pgvector import PostgresVectorAdapter -from .types.postgis import PostgresGeometry +from .type_values.postgis import PostgresGeometry from .adapters.postgis import PostgresPostGISAdapter -from .types.hstore import PostgresHstore +from .type_values.hstore import PostgresHstore from .adapters.hstore import PostgresHstoreAdapter -from .types.text_search import ( +from .type_values.text_search import ( PostgresTsVector, PostgresTsQuery, ) @@ -175,9 +175,9 @@ tsvector_length, ) from .adapters.text_search import PostgresTsVectorAdapter, PostgresTsQueryAdapter -from .types.pg_lsn import PostgresLsn +from .type_values.pg_lsn import PostgresLsn from .adapters.pg_lsn import PostgresLsnAdapter -from .types.object_identifier import ( +from .type_values.object_identifier import ( OID, RegClass, RegType, @@ -196,9 +196,9 @@ TID, ) from .adapters.object_identifier import PostgresOidAdapter, PostgresXidAdapter, PostgresTidAdapter -from .types.json import PostgresJsonPath +from .type_values.json import PostgresJsonPath from .adapters.json import PostgresJsonPathAdapter -from .types.enum import EnumTypeManager +from .enum_type_manager import EnumTypeManager # Import functions from functions module from .functions import ( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py index 6d5c7ab3..893567ef 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/base.py @@ -280,7 +280,7 @@ def _do_to_database(self, value: Any, target_type: Type, options: Optional[Dict[ # Validate if enum_type provided if options and "enum_type" in options: - from ..types import PostgresEnumType + from ..expression.enum_ import PostgresEnumType enum_type = options["enum_type"] if isinstance(enum_type, PostgresEnumType): diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/bit_string.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/bit_string.py index 89e2037b..49a38bc6 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/bit_string.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/bit_string.py @@ -15,7 +15,7 @@ from typing import Any, Dict, Optional, Set, Type -from ..types.bit_string import PostgresBitString +from ..type_values.bit_string import PostgresBitString class PostgresBitStringAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/geometric.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/geometric.py index 498ab095..8c7f4abf 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/geometric.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/geometric.py @@ -18,7 +18,7 @@ from typing import Any, Dict, List, Optional, Set, Type -from ..types.geometric import ( +from ..type_values.geometric import ( Point, Line, LineSegment, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/hstore.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/hstore.py index 670b8d8f..a2048f30 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/hstore.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/hstore.py @@ -12,7 +12,7 @@ from typing import Any, Dict, Optional, Type, Union, List from rhosocial.activerecord.backend.type_adapter import BaseSQLTypeAdapter -from rhosocial.activerecord.backend.impl.postgres.types.hstore import PostgresHstore +from rhosocial.activerecord.backend.impl.postgres.type_values.hstore import PostgresHstore class PostgresHstoreAdapter(BaseSQLTypeAdapter): diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py index b249c173..69dce651 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py @@ -22,7 +22,7 @@ from rhosocial.activerecord.backend.type_adapter import BaseSQLTypeAdapter -from ..types.json import PostgresJsonPath +from ..type_values.json import PostgresJsonPath class PostgresJSONBAdapter(BaseSQLTypeAdapter): diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py index 5faec0da..b41404e0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py @@ -12,7 +12,7 @@ from typing import Any, Dict, Optional, Set, Type -from ..types.monetary import PostgresMoney +from ..type_values.monetary import PostgresMoney class PostgresMoneyAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/network_address.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/network_address.py index b8506e1c..ef8db097 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/network_address.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/network_address.py @@ -15,7 +15,7 @@ from typing import Any, Dict, List, Optional, Set, Type -from ..types.network_address import PostgresMacaddr, PostgresMacaddr8 +from ..type_values.network_address import PostgresMacaddr, PostgresMacaddr8 class PostgresNetworkAddressAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/object_identifier.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/object_identifier.py index 891c71e0..37954ac1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/object_identifier.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/object_identifier.py @@ -29,7 +29,7 @@ from typing import Any, Dict, List, Optional, Set, Type, Union -from ..types.object_identifier import ( +from ..type_values.object_identifier import ( OID, RegClass, RegType, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/pg_lsn.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/pg_lsn.py index 3e2ea464..a317eb3c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/pg_lsn.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/pg_lsn.py @@ -13,7 +13,7 @@ from typing import Any, Dict, List, Optional, Set, Type -from ..types.pg_lsn import PostgresLsn +from ..type_values.pg_lsn import PostgresLsn class PostgresLsnAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/pgvector.py index 8f06890b..2887793a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/pgvector.py @@ -14,7 +14,7 @@ from typing import Any, Dict, Optional, Set, Type -from rhosocial.activerecord.backend.impl.postgres.types.pgvector import PostgresVector +from rhosocial.activerecord.backend.impl.postgres.type_values.pgvector import PostgresVector class PostgresVectorAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/postgis.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/postgis.py index 9c888109..59aeed28 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/postgis.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/postgis.py @@ -14,7 +14,7 @@ from typing import Any, Dict, Optional, Set, Type -from rhosocial.activerecord.backend.impl.postgres.types.postgis import PostgresGeometry +from rhosocial.activerecord.backend.impl.postgres.type_values.postgis import PostgresGeometry class PostgresPostGISAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/range.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/range.py index 6e6894ca..8a8c583c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/range.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/range.py @@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Set, Type, Union -from ..types.range import PostgresRange, PostgresMultirange +from ..type_values.range import PostgresRange, PostgresMultirange class PostgresRangeAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/text_search.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/text_search.py index 3a6b36f0..776b2126 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/text_search.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/text_search.py @@ -16,7 +16,7 @@ from typing import Any, Dict, List, Optional, Set, Type, Union -from ..types.text_search import PostgresTsVector, PostgresTsQuery +from ..type_values.text_search import PostgresTsVector, PostgresTsQuery class PostgresTsVectorAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py index 86d15b04..f1662f53 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py @@ -12,7 +12,7 @@ from typing import Any, Dict, List, Optional, Set, Type -from ..types.xml import PostgresXML +from ..type_values.xml import PostgresXML class PostgresXMLAdapter: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py index d14bbe9d..7abb647e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py @@ -285,14 +285,14 @@ def get_default_adapter_suggestions(self) -> Dict[Type, Tuple["SQLTypeAdapter", from enum import Enum # Import PostgreSQL-specific types - from ..types.range import PostgresRange, PostgresMultirange - from ..types.geometric import Point, Line, LineSegment, Box, Path, Polygon, Circle - from ..types.text_search import PostgresTsVector, PostgresTsQuery - from ..types.monetary import PostgresMoney - from ..types.network_address import PostgresMacaddr, PostgresMacaddr8 - from ..types.pg_lsn import PostgresLsn - from ..types.json import PostgresJsonPath - from ..types.object_identifier import ( + from ..type_values.range import PostgresRange, PostgresMultirange + from ..type_values.geometric import Point, Line, LineSegment, Box, Path, Polygon, Circle + from ..type_values.text_search import PostgresTsVector, PostgresTsQuery + from ..type_values.monetary import PostgresMoney + from ..type_values.network_address import PostgresMacaddr, PostgresMacaddr8 + from ..type_values.pg_lsn import PostgresLsn + from ..type_values.json import PostgresJsonPath + from ..type_values.object_identifier import ( OID, RegClass, RegType, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/enum.py b/src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py similarity index 56% rename from src/rhosocial/activerecord/backend/impl/postgres/types/enum.py rename to src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py index e27cc15f..3efb5e3a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/enum.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py @@ -1,10 +1,8 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/enum.py -"""PostgreSQL ENUM type support. +# src/rhosocial/activerecord/backend/impl/postgres/enum_type_manager.py +"""PostgreSQL ENUM type lifecycle management. This module provides: -- PostgresEnumType: Type reference expression for ENUM types -- EnumTypeManager for lifecycle management -- Enum utility functions (enum_range, enum_first, enum_last, etc.) +- EnumTypeManager for lifecycle management of PostgreSQL ENUM types PostgreSQL Documentation: https://www.postgresql.org/docs/current/datatype-enum.html @@ -16,166 +14,20 @@ - Adding values: PostgreSQL 9.1+ """ -from typing import TYPE_CHECKING, Any, Dict, List, Optional - -from rhosocial.activerecord.backend.expression.bases import BaseExpression - -if TYPE_CHECKING: - from rhosocial.activerecord.backend.dialect import SQLDialectBase +from typing import Any, Dict, List, Optional from rhosocial.activerecord.backend.expression.statements.ddl_type import ( AlterTypeExpression, CreateTypeExpression, ) -from ..expression.ddl import ( +from .expression.ddl import ( PostgresAddEnumValueAction, PostgresDropTypeExpression, PostgresEnumTypeDefinition, PostgresRenameEnumValueAction, ) - - -class PostgresEnumType(BaseExpression): - """PostgreSQL ENUM type reference expression. - - This class represents a PostgreSQL ENUM type reference as a SQL expression. - It is used in column definitions and type casts, NOT for DDL operations. - - For DDL operations (CREATE TYPE, DROP TYPE, etc.), use: - - PostgresCreateEnumTypeExpression - - PostgresDropEnumTypeExpression - - PostgresAlterEnumTypeAddValueExpression - - PostgresAlterEnumTypeRenameValueExpression - - PostgreSQL ENUM types are custom types created with CREATE TYPE. - They are reusable across tables, unlike MySQL's inline ENUM. - - Attributes: - name: Enum type name - values: List of allowed values - schema: Optional schema name - - Examples: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect - >>> dialect = PostgresDialect() - >>> status_enum = PostgresEnumType( - ... dialect=dialect, - ... name='video_status', - ... values=['pending', 'processing', 'ready', 'failed'] - ... ) - >>> status_enum.to_sql() - ('video_status', ()) - - >>> # With schema - >>> status_enum = PostgresEnumType( - ... dialect=dialect, - ... name='video_status', - ... values=['draft', 'published'], - ... schema='app' - ... ) - >>> status_enum.to_sql() - ('app.video_status', ()) - """ - - def __init__(self, dialect: "SQLDialectBase", name: str, values: List[str], schema: Optional[str] = None): - """Initialize PostgreSQL ENUM type reference expression. - - Args: - dialect: SQL dialect instance - name: Enum type name - values: List of allowed values - schema: Optional schema name - - Raises: - ValueError: If name is empty or values is empty - """ - super().__init__(dialect) - - if not name: - raise ValueError("Enum type name cannot be empty") - if not values: - raise ValueError("ENUM must have at least one value") - if len(values) != len(set(values)): - raise ValueError("ENUM values must be unique") - - self._name = name - self._values = list(values) - self._schema = schema - - @property - def name(self) -> str: - """Enum type name.""" - return self._name - - @property - def values(self) -> List[str]: - """List of allowed values.""" - return list(self._values) - - @property - def schema(self) -> Optional[str]: - """Schema name, if any.""" - return self._schema - - @property - def format_method(self) -> str: - """The dialect formatting method that renders this expression.""" - return "format_enum_type_expression" - - def validate_value(self, value: str) -> bool: - """Check if a value is valid for this enum. - - Args: - value: Value to validate - - Returns: - True if value is valid - """ - return value in self._values - - @classmethod - def from_python_enum( - cls, dialect: "SQLDialectBase", enum_class: type, name: Optional[str] = None, schema: Optional[str] = None - ) -> "PostgresEnumType": - """Create PostgresEnumType from Python Enum class. - - Args: - dialect: SQL dialect instance - enum_class: Python Enum class - name: Optional type name (defaults to enum class name in lowercase) - schema: Optional schema name - - Returns: - PostgresEnumType instance - """ - from enum import Enum - - if not issubclass(enum_class, Enum): - raise TypeError("enum_class must be a Python Enum class") - - type_name = name or enum_class.__name__.lower() - values = [e.name for e in enum_class] - return cls(dialect=dialect, name=type_name, values=values, schema=schema) - - def __str__(self) -> str: - if self._schema: - return f"{self._schema}.{self._name}" - return self._name - - def __repr__(self) -> str: - return ( - f"PostgresEnumType(dialect={self._dialect!r}, name={self._name!r}, " - f"values={self._values!r}, schema={self._schema!r})" - ) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, PostgresEnumType): - return NotImplemented - return self._name == other._name and self._values == other._values and self._schema == other._schema - - def __hash__(self) -> int: - return hash((self._name, tuple(self._values), self._schema)) +from .expression.enum_ import PostgresEnumType class EnumTypeManager: @@ -364,4 +216,5 @@ def get_type_values(self, name: str, schema: Optional[str] = None) -> Optional[L return [r["enumlabel"] for r in results] if results else None -__all__ = ["PostgresEnumType", "EnumTypeManager"] + +__all__ = ["EnumTypeManager"] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py new file mode 100644 index 00000000..29aa625f --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py @@ -0,0 +1,168 @@ +# src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py +"""PostgreSQL ENUM type reference expression. + +This module provides: +- PostgresEnumType: Type reference expression for ENUM types +- Enum utility functions (enum_range, enum_first, enum_last, etc.) + +PostgreSQL Documentation: https://www.postgresql.org/docs/current/datatype-enum.html + +PostgreSQL ENUM types are custom types created with CREATE TYPE. +They are reusable across tables. + +Version requirements: +- Basic ENUM: PostgreSQL 8.3+ +- Adding values: PostgreSQL 9.1+ +""" + +from typing import TYPE_CHECKING, List, Optional + +from rhosocial.activerecord.backend.expression.bases import BaseExpression + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.dialect import SQLDialectBase + + +class PostgresEnumType(BaseExpression): + """PostgreSQL ENUM type reference expression. + + This class represents a PostgreSQL ENUM type reference as a SQL expression. + It is used in column definitions and type casts, NOT for DDL operations. + + For DDL operations (CREATE TYPE, DROP TYPE, etc.), use: + - PostgresCreateEnumTypeExpression + - PostgresDropEnumTypeExpression + - PostgresAlterEnumTypeAddValueExpression + - PostgresAlterEnumTypeRenameValueExpression + + PostgreSQL ENUM types are custom types created with CREATE TYPE. + They are reusable across tables, unlike MySQL's inline ENUM. + + Attributes: + name: Enum type name + values: List of allowed values + schema: Optional schema name + + Examples: + >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> dialect = PostgresDialect() + >>> status_enum = PostgresEnumType( + ... dialect=dialect, + ... name='video_status', + ... values=['pending', 'processing', 'ready', 'failed'] + ... ) + >>> status_enum.to_sql() + ('video_status', ()) + + >>> # With schema + >>> status_enum = PostgresEnumType( + ... dialect=dialect, + ... name='video_status', + ... values=['draft', 'published'], + ... schema='app' + ... ) + >>> status_enum.to_sql() + ('app.video_status', ()) + """ + + def __init__(self, dialect: "SQLDialectBase", name: str, values: List[str], schema: Optional[str] = None): + """Initialize PostgreSQL ENUM type reference expression. + + Args: + dialect: SQL dialect instance + name: Enum type name + values: List of allowed values + schema: Optional schema name + + Raises: + ValueError: If name is empty or values is empty + """ + super().__init__(dialect) + + if not name: + raise ValueError("Enum type name cannot be empty") + if not values: + raise ValueError("ENUM must have at least one value") + if len(values) != len(set(values)): + raise ValueError("ENUM values must be unique") + + self._name = name + self._values = list(values) + self._schema = schema + + @property + def name(self) -> str: + """Enum type name.""" + return self._name + + @property + def values(self) -> List[str]: + """List of allowed values.""" + return list(self._values) + + @property + def schema(self) -> Optional[str]: + """Schema name, if any.""" + return self._schema + + @property + def format_method(self) -> str: + """The dialect formatting method that renders this expression.""" + return "format_enum_type_expression" + + def validate_value(self, value: str) -> bool: + """Check if a value is valid for this enum. + + Args: + value: Value to validate + + Returns: + True if value is valid + """ + return value in self._values + + @classmethod + def from_python_enum( + cls, dialect: "SQLDialectBase", enum_class: type, name: Optional[str] = None, schema: Optional[str] = None + ) -> "PostgresEnumType": + """Create PostgresEnumType from Python Enum class. + + Args: + dialect: SQL dialect instance + enum_class: Python Enum class + name: Optional type name (defaults to enum class name in lowercase) + schema: Optional schema name + + Returns: + PostgresEnumType instance + """ + from enum import Enum + + if not issubclass(enum_class, Enum): + raise TypeError("enum_class must be a Python Enum class") + + type_name = name or enum_class.__name__.lower() + values = [e.name for e in enum_class] + return cls(dialect=dialect, name=type_name, values=values, schema=schema) + + def __str__(self) -> str: + if self._schema: + return f"{self._schema}.{self._name}" + return self._name + + def __repr__(self) -> str: + return ( + f"PostgresEnumType(dialect={self._dialect!r}, name={self._name!r}, " + f"values={self._values!r}, schema={self._schema!r})" + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PostgresEnumType): + return NotImplemented + return self._name == other._name and self._values == other._values and self._schema == other._schema + + def __hash__(self) -> int: + return hash((self._name, tuple(self._values), self._schema)) + + +__all__ = ["PostgresEnumType"] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py index d834337c..5a953f47 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/hstore.py @@ -38,7 +38,7 @@ from rhosocial.activerecord.backend.expression import bases, core from rhosocial.activerecord.backend.expression.operators import BinaryExpression -from rhosocial.activerecord.backend.impl.postgres.types.hstore import PostgresHstore +from rhosocial.activerecord.backend.impl.postgres.type_values.hstore import PostgresHstore if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/json.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/json.py index 18636ec4..377b961b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/json.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/json.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath def _convert_to_expression( @@ -64,7 +64,7 @@ def _convert_jsonpath( Returns: BaseExpression representing the jsonpath literal """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath if isinstance(path, PostgresJsonPath): return core.Literal(dialect, str(path)) @@ -87,7 +87,7 @@ def json_path_root() -> "PostgresJsonPath": >>> json_path_root() PostgresJsonPath('$') """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath return PostgresJsonPath("$") @@ -109,7 +109,7 @@ def json_path_key(path: Union["PostgresJsonPath", str], key: str) -> "PostgresJs >>> json_path_key('$.store', 'book') PostgresJsonPath('$.store.book') """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath if isinstance(path, PostgresJsonPath): return path.key(key) @@ -133,7 +133,7 @@ def json_path_index(path: Union["PostgresJsonPath", str], index: Union[int, str] >>> json_path_index('$.items', 'last') PostgresJsonPath('$.items[last]') """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath if isinstance(path, PostgresJsonPath): return path.index(index) @@ -160,7 +160,7 @@ def json_path_wildcard(path: Union["PostgresJsonPath", str], array: bool = True) >>> json_path_wildcard('$.items') PostgresJsonPath('$.items[*]') """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath if isinstance(path, PostgresJsonPath): return path.wildcard_array() if array else path.wildcard_object() @@ -185,7 +185,7 @@ def json_path_filter(path: Union["PostgresJsonPath", str], condition: str) -> "P >>> json_path_filter('$.books[*]', '@.author == "King"') PostgresJsonPath('$.books[*]?(@.author == "King")') """ - from ..types.json import PostgresJsonPath + from ..type_values.json import PostgresJsonPath if isinstance(path, PostgresJsonPath): return path.filter(condition) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py index 00e88d31..96bab870 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py @@ -25,7 +25,7 @@ from rhosocial.activerecord.backend.expression import bases, core from rhosocial.activerecord.backend.expression.operators import BinaryArithmeticExpression -from rhosocial.activerecord.backend.impl.postgres.types.pgvector import PostgresVector +from rhosocial.activerecord.backend.impl.postgres.type_values.pgvector import PostgresVector if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis.py index a3b62882..510861ba 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis.py @@ -15,7 +15,7 @@ from typing import Optional, Union, TYPE_CHECKING from rhosocial.activerecord.backend.expression import bases, core -from rhosocial.activerecord.backend.impl.postgres.types.postgis import PostgresGeometry +from rhosocial.activerecord.backend.impl.postgres.type_values.postgis import PostgresGeometry if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py index fd5abdb0..bd78bc94 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py @@ -16,7 +16,7 @@ from rhosocial.activerecord.backend.expression import bases, core from rhosocial.activerecord.backend.expression.operators import BinaryExpression -from rhosocial.activerecord.backend.impl.postgres.types.range import PostgresRange +from rhosocial.activerecord.backend.impl.postgres.type_values.range import PostgresRange if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py index 4810f5a6..d129ed1b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/xml.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: from rhosocial.activerecord.backend.dialect import SQLDialectBase -from ..types.xml import PostgresXML +from ..type_values.xml import PostgresXML def _convert_to_expression( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py similarity index 97% rename from src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py index d67bbdf2..423d066a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/__init__.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/__init__.py """ PostgreSQL type definitions. @@ -21,12 +21,6 @@ - xml: XML type - https://www.postgresql.org/docs/current/datatype-xml.html """ -# Enum types -from .enum import ( - PostgresEnumType, - EnumTypeManager, -) - # Bit string types from .bit_string import ( PostgresBitString, @@ -226,8 +220,6 @@ __all__ = [ # Enum types - "PostgresEnumType", - "EnumTypeManager", # Bit string types "PostgresBitString", # Geometric types diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/bit_string.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/bit_string.py similarity index 97% rename from src/rhosocial/activerecord/backend/impl/postgres/types/bit_string.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/bit_string.py index 7346f57a..fd492ef8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/bit_string.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/bit_string.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/bit_string.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/bit_string.py """ PostgreSQL bit string type representation. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/constants.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/constants.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/constants.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/constants.py index 447331a6..89b15f44 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/constants.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/constants.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/constants.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/constants.py """PostgreSQL data type constants. These constants provide convenient type name references for use with @@ -7,7 +7,7 @@ PostgreSQL Documentation: https://www.postgresql.org/docs/current/datatype.html Example: - >>> from rhosocial.activerecord.backend.impl.postgres.types.constants import MONEY, NUMERIC + >>> from rhosocial.activerecord.backend.impl.postgres.type_values.constants import MONEY, NUMERIC >>> col.cast(MONEY) # Equivalent to col.cast("money") >>> col.cast("money") # Also valid diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/geometric.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/geometric.py similarity index 99% rename from src/rhosocial/activerecord/backend/impl/postgres/types/geometric.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/geometric.py index 1d90e96e..03b50d4c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/geometric.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/geometric.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/geometric.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/geometric.py """ PostgreSQL geometric types representation. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/hstore.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/hstore.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/hstore.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/hstore.py index 10ac04fe..fc626551 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/hstore.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/hstore.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/hstore.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/hstore.py """ PostgreSQL hstore type definition. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/json.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/json.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/json.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/json.py index e66c317c..c7ce70c8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/json.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/json.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/json.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/json.py """PostgreSQL jsonpath type representation. This module provides PostgresJsonPath for representing PostgreSQL diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/monetary.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/monetary.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/monetary.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/monetary.py index db73ddd5..5a8c4d17 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/monetary.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/monetary.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/monetary.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/monetary.py """ PostgreSQL MONEY type representation. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/network_address.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/network_address.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/network_address.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/network_address.py index 14cdd8e5..ead3492c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/network_address.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/network_address.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/network_address.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/network_address.py """PostgreSQL Network Address Types. This module provides type representations for PostgreSQL network address types: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/object_identifier.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/object_identifier.py similarity index 99% rename from src/rhosocial/activerecord/backend/impl/postgres/types/object_identifier.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/object_identifier.py index 562e5831..e4150edd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/object_identifier.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/object_identifier.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/object_identifier.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/object_identifier.py """ PostgreSQL Object Identifier (OID) types representation. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/pg_lsn.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/pg_lsn.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/pg_lsn.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/pg_lsn.py index fc6a37f5..c1d6bffe 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/pg_lsn.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/pg_lsn.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/pg_lsn.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/pg_lsn.py """PostgreSQL pg_lsn type representation. This module provides PostgresLsn for representing PostgreSQL diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/pgvector.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/pgvector.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/pgvector.py index 7f2f174d..660b8265 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/pgvector.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/pgvector.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/pgvector.py """ PostgreSQL pgvector vector type. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/postgis.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/postgis.py similarity index 98% rename from src/rhosocial/activerecord/backend/impl/postgres/types/postgis.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/postgis.py index 0baebe53..9771a78d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/postgis.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/postgis.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/postgis.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/postgis.py """ PostgreSQL PostGIS geometry/geography type. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/range.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/range.py similarity index 99% rename from src/rhosocial/activerecord/backend/impl/postgres/types/range.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/range.py index accf2f4b..a73a3e64 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/range.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/range.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/range.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/range.py """ PostgreSQL range types representation. @@ -45,7 +45,7 @@ 1. **Recommended**: Use PostgresRange for Python code: ```python - from rhosocial.activerecord.backend.impl.postgres.types import PostgresRange + from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresRange range_obj = PostgresRange(1, 10) ``` diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/text_search.py similarity index 99% rename from src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/text_search.py index da10b5e1..f099efc1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/text_search.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/text_search.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/text_search.py """ PostgreSQL text search types representation. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/types/xml.py b/src/rhosocial/activerecord/backend/impl/postgres/type_values/xml.py similarity index 96% rename from src/rhosocial/activerecord/backend/impl/postgres/types/xml.py rename to src/rhosocial/activerecord/backend/impl/postgres/type_values/xml.py index 6e377196..c8b4b0a8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/types/xml.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/type_values/xml.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/types/xml.py +# src/rhosocial/activerecord/backend/impl/postgres/type_values/xml.py """ PostgreSQL XML type representation. 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 d724e920..6403ad0c 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 @@ -17,7 +17,7 @@ async_ensure_extension_installed, ) from rhosocial.activerecord.backend.impl.postgres.adapters.pgvector import PostgresVectorAdapter -from rhosocial.activerecord.backend.impl.postgres.types.pgvector import PostgresVector +from rhosocial.activerecord.backend.impl.postgres.type_values.pgvector import PostgresVector from rhosocial.activerecord.backend.errors import DatabaseError from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_integration.py index be457157..49929f50 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_integration.py @@ -13,7 +13,7 @@ from rhosocial.activerecord.backend.impl.postgres.adapters.bit_string import ( PostgresBitStringAdapter, ) -from rhosocial.activerecord.backend.impl.postgres.types.bit_string import PostgresBitString +from rhosocial.activerecord.backend.impl.postgres.type_values.bit_string import PostgresBitString BITSTRING_TABLE = "test_bitstring_types" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_types.py index 704272f6..138c63cb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_bitstring_types.py @@ -8,7 +8,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.bit_string import PostgresBitString +from rhosocial.activerecord.backend.impl.postgres.type_values.bit_string import PostgresBitString from rhosocial.activerecord.backend.impl.postgres.adapters.bit_string import PostgresBitStringAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_integration.py index 52842575..08f33de7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_integration.py @@ -12,10 +12,10 @@ import pytest_asyncio from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresEnumAdapter -from rhosocial.activerecord.backend.impl.postgres.types.enum import ( +from rhosocial.activerecord.backend.impl.postgres.enum_type_manager import ( EnumTypeManager, - PostgresEnumType, ) +from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType ENUM_TYPE = "test_status_enum" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py index 34acd9a0..54aedf22 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py @@ -12,7 +12,7 @@ from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types import PostgresEnumType +from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresEnumAdapter from rhosocial.activerecord.backend.impl.postgres.protocols import PostgresEnumTypeSupport # noqa: F401 from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_geometry_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_geometry_types.py index 012da4ce..44a7a457 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_geometry_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_geometry_types.py @@ -8,7 +8,7 @@ """ import pytest # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres.types.geometric import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.geometric import ( Point, Line, LineSegment, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_hstore_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_hstore_types.py index 0683dc26..fbe3c3f5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_hstore_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_hstore_types.py @@ -7,7 +7,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.hstore import PostgresHstore +from rhosocial.activerecord.backend.impl.postgres.type_values.hstore import PostgresHstore from rhosocial.activerecord.backend.impl.postgres.adapters.hstore import PostgresHstoreAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_jsonpath_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_jsonpath_types.py index 785db19b..94551e3a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_jsonpath_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_jsonpath_types.py @@ -10,7 +10,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.json import PostgresJsonPath +from rhosocial.activerecord.backend.impl.postgres.type_values.json import PostgresJsonPath from rhosocial.activerecord.backend.impl.postgres.adapters.json import PostgresJsonPathAdapter from rhosocial.activerecord.backend.impl.postgres.functions.json import ( json_path_root, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_integration.py index f01a46d8..7311d209 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_integration.py @@ -12,7 +12,7 @@ import pytest_asyncio from rhosocial.activerecord.backend.impl.postgres.adapters.pg_lsn import PostgresLsnAdapter -from rhosocial.activerecord.backend.impl.postgres.types.pg_lsn import PostgresLsn +from rhosocial.activerecord.backend.impl.postgres.type_values.pg_lsn import PostgresLsn LSN_TABLE = "test_lsn_types" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_types.py index ab404113..9865183d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_lsn_types.py @@ -7,7 +7,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.pg_lsn import PostgresLsn +from rhosocial.activerecord.backend.impl.postgres.type_values.pg_lsn import PostgresLsn from rhosocial.activerecord.backend.impl.postgres.adapters.pg_lsn import PostgresLsnAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_macaddr8_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_macaddr8_types.py index 222539c7..f32ab682 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_macaddr8_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_macaddr8_types.py @@ -7,7 +7,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.network_address import PostgresMacaddr8 +from rhosocial.activerecord.backend.impl.postgres.type_values.network_address import PostgresMacaddr8 from rhosocial.activerecord.backend.impl.postgres.adapters.network_address import PostgresMacaddr8Adapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_integration.py index 392e4a2c..0092b8b5 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_integration.py @@ -15,7 +15,7 @@ from rhosocial.activerecord.backend.impl.postgres.adapters.monetary import ( PostgresMoneyAdapter, ) -from rhosocial.activerecord.backend.impl.postgres.types.monetary import PostgresMoney +from rhosocial.activerecord.backend.impl.postgres.type_values.monetary import PostgresMoney MONEY_TABLE = "test_money_types" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_types.py index 9cdea68a..be49b29a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_money_types.py @@ -8,7 +8,7 @@ import pytest from decimal import Decimal -from rhosocial.activerecord.backend.impl.postgres.types.monetary import PostgresMoney +from rhosocial.activerecord.backend.impl.postgres.type_values.monetary import PostgresMoney from rhosocial.activerecord.backend.impl.postgres.adapters.monetary import PostgresMoneyAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_network_address_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_network_address_integration.py index e76a9555..553888f9 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_network_address_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_network_address_integration.py @@ -18,7 +18,7 @@ PostgresMacaddrAdapter, PostgresNetworkAddressAdapter, ) -from rhosocial.activerecord.backend.impl.postgres.types.network_address import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.network_address import ( PostgresMacaddr, PostgresMacaddr8, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_integration.py index 6bc58336..02d5ef2b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_integration.py @@ -16,7 +16,7 @@ PostgresOidAdapter, PostgresTidAdapter, ) -from rhosocial.activerecord.backend.impl.postgres.types.object_identifier import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.object_identifier import ( OID, RegClass, RegType, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_types.py index 47dc6396..f1bc7d10 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_oid_types.py @@ -9,7 +9,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.object_identifier import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.object_identifier import ( OID, RegClass, RegType, RegProc, RegProcedure, RegOper, RegOperator, RegConfig, RegDictionary, RegNamespace, RegRole, RegCollation, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pgvector_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pgvector_types.py index 65b970e8..4af6d2e0 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pgvector_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pgvector_types.py @@ -7,7 +7,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.pgvector import PostgresVector +from rhosocial.activerecord.backend.impl.postgres.type_values.pgvector import PostgresVector from rhosocial.activerecord.backend.impl.postgres.adapters.pgvector import PostgresVectorAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgis_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgis_types.py index a40e59bf..eb14ce0e 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgis_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgis_types.py @@ -7,7 +7,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.postgis import PostgresGeometry +from rhosocial.activerecord.backend.impl.postgres.type_values.postgis import PostgresGeometry from rhosocial.activerecord.backend.impl.postgres.adapters.postgis import PostgresPostGISAdapter diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_hstore_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_hstore_functions.py index 8d9f9364..ff4be722 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_hstore_functions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_hstore_functions.py @@ -60,7 +60,7 @@ hstore_subscript_get, hstore_subscript_set, ) -from rhosocial.activerecord.backend.impl.postgres.types.hstore import PostgresHstore +from rhosocial.activerecord.backend.impl.postgres.type_values.hstore import PostgresHstore from rhosocial.activerecord.backend.expression import bases from rhosocial.activerecord.backend.expression.core import FunctionCall from rhosocial.activerecord.backend.expression.operators import BinaryExpression diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_pgvector_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_pgvector_functions.py index c406924d..d45cc312 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_pgvector_functions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_pgvector_functions.py @@ -16,7 +16,7 @@ vector_cosine_similarity, vector_literal, ) -from rhosocial.activerecord.backend.impl.postgres.types.pgvector import PostgresVector +from rhosocial.activerecord.backend.impl.postgres.type_values.pgvector import PostgresVector from rhosocial.activerecord.backend.expression.operators import BinaryArithmeticExpression from rhosocial.activerecord.backend.expression import bases diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_postgis_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_postgis_functions.py index a890c3e9..6d42347d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_postgis_functions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_postgis_functions.py @@ -34,7 +34,7 @@ st_envelope, st_centroid, ) -from rhosocial.activerecord.backend.impl.postgres.types.postgis import PostgresGeometry +from rhosocial.activerecord.backend.impl.postgres.type_values.postgis import PostgresGeometry from rhosocial.activerecord.backend.expression import bases from rhosocial.activerecord.backend.expression.core import FunctionCall diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_functions.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_functions.py index af3dc886..7915ca51 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_functions.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_functions.py @@ -31,7 +31,7 @@ range_lower_inf, range_upper_inf, ) -from rhosocial.activerecord.backend.impl.postgres.types.range import PostgresRange +from rhosocial.activerecord.backend.impl.postgres.type_values.range import PostgresRange class TestRangeContains: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types.py index b013d905..79a4464d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types.py @@ -12,7 +12,7 @@ from datetime import date, datetime # noqa: F401 from decimal import Decimal # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres.types.range import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.range import ( PostgresRange, PostgresMultirange, _find_bound_separator, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types_integration.py index 7182072e..e28bc9e2 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_range_types_integration.py @@ -12,7 +12,7 @@ from datetime import date, datetime # noqa: F401 from decimal import Decimal # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres.types.range import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.range import ( PostgresRange, # noqa: F401 PostgresMultirange, # noqa: F401 ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_textsearch_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_textsearch_types.py index 36bbd0c5..9bd78fef 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_textsearch_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_textsearch_types.py @@ -10,7 +10,7 @@ """ import pytest -from rhosocial.activerecord.backend.impl.postgres.types.text_search import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.text_search import ( TsVectorLexeme, PostgresTsVector, TsQueryLexeme, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py index 84bef0e1..60a28b51 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py @@ -12,7 +12,7 @@ from rhosocial.activerecord.backend.expression.core import Column, Literal, CastExpression from rhosocial.activerecord.backend.impl.postgres import PostgresDialect -from rhosocial.activerecord.backend.impl.postgres.types.constants import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.constants import ( MONEY, NUMERIC, FLOAT8, INTEGER, VARCHAR, TEXT, JSONB, TIMESTAMP, TIMESTAMPTZ, BOOLEAN, BIGINT, ) @@ -239,7 +239,7 @@ def test_boolean_constants(self): """Test boolean type constants.""" assert BOOLEAN == "boolean" # BOOL is an alias - from rhosocial.activerecord.backend.impl.postgres.types.constants import BOOL + from rhosocial.activerecord.backend.impl.postgres.type_values.constants import BOOL assert BOOL == "bool" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_integration.py index da3160ac..37d81770 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_integration.py @@ -19,7 +19,7 @@ xpath_exists, xpath_query, ) -from rhosocial.activerecord.backend.impl.postgres.types.xml import PostgresXML +from rhosocial.activerecord.backend.impl.postgres.type_values.xml import PostgresXML XML_TABLE = "test_xml_types" diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py index 776e0b33..66511538 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_xml_types.py @@ -11,7 +11,7 @@ from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.expression import XMLParseExpression, core from rhosocial.activerecord.backend.expression.functions.xml import xmlparse -from rhosocial.activerecord.backend.impl.postgres.types.xml import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.xml import ( PostgresXML, ) from rhosocial.activerecord.backend.impl.postgres.functions.xml import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_adapters.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_adapters.py index 639e509e..92105126 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_adapters.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_adapters.py @@ -19,7 +19,7 @@ from rhosocial.activerecord.backend.impl.postgres.adapters.range import PostgresRangeAdapter, PostgresMultirangeAdapter from rhosocial.activerecord.backend.impl.postgres.adapters.base import PostgresListAdapter, PostgresEnumAdapter from rhosocial.activerecord.backend.impl.postgres.adapters.pgvector import PostgresVectorAdapter -from rhosocial.activerecord.backend.impl.postgres.types.range import ( +from rhosocial.activerecord.backend.impl.postgres.type_values.range import ( PostgresRange as PG_RANGE, PostgresMultirange as PG_MULTIRANGE, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py new file mode 100644 index 00000000..64a5c7cf --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py @@ -0,0 +1,126 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py +"""Guardrail: every expression must live under the backend's expression namespace. + +``BaseExpression`` subclasses render SQL, so they belong in +``rhosocial.activerecord.backend.impl.postgres.expression``. Data types are no +exception: a value-level ``DataType`` belongs in ``expression/types.py`` and a +DDL-level type in ``expression/ddl/type.py``. + +This test failed repeatedly in the past (SHOW expressions sat in ``show/``, and +``PostgresEnumType`` sat in ``types/``), so the invariant is asserted +mechanically rather than left to review. +""" + +import importlib +import inspect +import pkgutil + +import pytest + +from rhosocial.activerecord.backend.expression.bases import BaseExpression +from rhosocial.activerecord.backend.impl import postgres + +EXPRESSION_NS = postgres.__name__ + ".expression" + + +def _iter_subclasses(cls): + """Yield every transitive subclass, breadth-first, cycle-safe.""" + seen = {cls} + queue = [cls] + while queue: + current = queue.pop(0) + for sub in current.__subclasses__(): + if sub not in seen: + seen.add(sub) + queue.append(sub) + yield sub + + +@pytest.fixture(scope="module") +def all_expression_classes(): + """Import every module in the postgres backend, then collect expressions. + + Imports are required so that ``__subclasses__()`` is populated; classes + that are never imported cannot be discovered by introspection. + """ + failures = [] + for mod in pkgutil.walk_packages(postgres.__path__, prefix=postgres.__name__ + "."): + name = mod.name + if ".examples" in name or ".cli" in name: + continue # example/CLI modules have import side effects + try: + importlib.import_module(name) + except Exception as exc: # pragma: no cover - diagnostic path + failures.append(f"{name}: {type(exc).__name__}: {exc}") + assert not failures, "could not import backend modules:\n" + "\n".join(failures) + return sorted(_iter_subclasses(BaseExpression), key=lambda c: (c.__module__, c.__qualname__)) + + +def test_backend_modules_import_cleanly(): + """Sanity check: every non-example backend module is importable.""" + failures = [] + for mod in pkgutil.walk_packages(postgres.__path__, prefix=postgres.__name__ + "."): + if ".examples" in mod.name or ".cli" in mod.name: + continue + try: + importlib.import_module(mod.name) + except Exception as exc: # pragma: no cover - diagnostic path + failures.append(f"{mod.name}: {type(exc).__name__}: {exc}") + assert not failures, "unimportable modules:\n" + "\n".join(failures) + + +def test_no_expression_defined_outside_expression_namespace(all_expression_classes): + """The core invariant: expressions are only defined under ``expression``.""" + offenders = [ + f"{cls.__module__}.{cls.__qualname__}" + for cls in all_expression_classes + if not cls.__module__.startswith(EXPRESSION_NS) + ] + assert not offenders, ( + "BaseExpression subclasses defined outside " + f"{EXPRESSION_NS}:\n " + "\n ".join(sorted(offenders)) + ) + + +def test_type_values_package_holds_no_expressions(all_expression_classes): + """``type_values`` is the domain value-type layer, so it holds no expressions.""" + type_values_ns = postgres.__name__ + ".type_values" + offenders = [ + f"{cls.__module__}.{cls.__qualname__}" + for cls in all_expression_classes + if cls.__module__.startswith(type_values_ns) + ] + assert not offenders, ( + f"expressions leaked into {type_values_ns}:\n " + "\n ".join(sorted(offenders)) + ) + + +def test_every_expression_class_is_inspectable(all_expression_classes): + """Sanity check: the collected set is non-trivial, so the test cannot pass vacuously.""" + assert len(all_expression_classes) > 50, ( + f"only found {len(all_expression_classes)} expression classes; " + "module discovery is probably broken" + ) + + +def test_postgres_enum_type_lives_in_expression_namespace(): + """Regression test for the specific violation this branch fixes.""" + from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType + + assert issubclass(PostgresEnumType, BaseExpression) + assert PostgresEnumType.__module__ == f"{EXPRESSION_NS}.enum_" + assert inspect.getmodule(PostgresEnumType) is not None + + +def test_old_types_package_is_gone(): + """The renamed package must not linger under its old name. + + ``types`` is a heavily overloaded name in this series, so a leftover + ``postgres/types`` package would silently re-open the ambiguity that the + ``type_values`` rename was meant to remove. + """ + import importlib.util + + assert importlib.util.find_spec(f"{postgres.__name__}.types") is None, ( + f"{postgres.__name__}.types still exists; it should have been renamed to type_values" + ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_postgres_type_protocol.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_postgres_type_protocol.py index d3b9c99b..3de4de2d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_postgres_type_protocol.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/types/test_postgres_type_protocol.py @@ -67,7 +67,7 @@ def test_supports_data_type_postgres_enum_exists(dialect): def test_postgres_enum_expression_renders_via_format_enum_type_expression(dialect): """PostgresEnumType (BaseExpression) renders via format_enum_type_expression.""" - from rhosocial.activerecord.backend.impl.postgres.types.enum import PostgresEnumType + from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType enum_ref = PostgresEnumType( dialect=dialect, @@ -80,7 +80,7 @@ def test_postgres_enum_expression_renders_via_format_enum_type_expression(dialec def test_postgres_enum_expression_with_schema_renders_via_dialect(dialect): - from rhosocial.activerecord.backend.impl.postgres.types.enum import PostgresEnumType + from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType enum_ref = PostgresEnumType( dialect=dialect, @@ -353,7 +353,7 @@ def test_time_precision_validation(dialect): def test_postgres_enum_type_is_not_data_type(): """PostgresEnumType extends BaseExpression, not DataType.""" from rhosocial.activerecord.backend.expression.bases import BaseExpression - from rhosocial.activerecord.backend.impl.postgres.types.enum import PostgresEnumType + from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType assert not issubclass(PostgresEnumType, DataType) assert issubclass(PostgresEnumType, BaseExpression) From 37db58ccc836c4e5303d132b0bc7ee3c3800d248 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 00:16:56 +0800 Subject: [PATCH 02/19] test(postgres): scope the namespace guardrail to backend-defined classes The guardrail collected every transitive BaseExpression subclass in the process, which includes the core expressions in rhosocial.activerecord.backend.expression. Those are legitimate, so the assertion flagged all of them and failed. Constrain the check to classes actually defined inside this backend's package: a class under impl.postgres must be under impl.postgres.expression. Classes from the core and from other backends are out of scope. --- .../feature/backend/test_expression_namespace.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py index 64a5c7cf..1670cb17 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_expression_namespace.py @@ -21,6 +21,7 @@ from rhosocial.activerecord.backend.impl import postgres EXPRESSION_NS = postgres.__name__ + ".expression" +BACKEND_NS = postgres.__name__ + "." def _iter_subclasses(cls): @@ -70,14 +71,21 @@ def test_backend_modules_import_cleanly(): def test_no_expression_defined_outside_expression_namespace(all_expression_classes): - """The core invariant: expressions are only defined under ``expression``.""" + """The core invariant: backend expressions are only defined under ``expression``. + + Only classes defined *inside this backend's* package are constrained. Core + expressions live in ``rhosocial.activerecord.backend.expression`` and + expressions belonging to other backends are irrelevant here, so both are + excluded by requiring the module to start with the backend namespace. + """ offenders = [ f"{cls.__module__}.{cls.__qualname__}" for cls in all_expression_classes - if not cls.__module__.startswith(EXPRESSION_NS) + if cls.__module__.startswith(BACKEND_NS) + and not cls.__module__.startswith(EXPRESSION_NS) ] assert not offenders, ( - "BaseExpression subclasses defined outside " + "BaseExpression subclasses defined in this backend but outside " f"{EXPRESSION_NS}:\n " + "\n ".join(sorted(offenders)) ) From 32e308c91691147a7caf69cf6411f2d051691e04 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 04:09:29 +0800 Subject: [PATCH 03/19] perf(postgres): resolve backend exports lazily so expressions import without a driver Every backend __init__ imported all of its submodules eagerly, and both .backend and .adapters import psycopg. Python executes a parent package's __init__ before any submodule, so `impl.postgres.expression` could not be imported without psycopg present - even though no expression class touches it, and type_values/range.py documents that PostgresRange works without psycopg installed. The eager adapters import defeated that intent. Public names are now resolved on first access through a PEP 562 module __getattr__, with the name -> (module, attribute) map derived from the existing import statements. Also repairs two names that __all__ has promised since v1.0.0.dev12 but that no import provides: gen_random_uuid and uuid_default_generator, whose imports were dropped while the __all__ entries were left behind. Both exist in .functions, so `from impl.postgres import *` raised AttributeError on the release branch and now succeeds. The exported surface is otherwise unchanged: attribute access, __all__, dir() and `from impl.postgres import *` all behave as before, and the previous eager imports move into an `if TYPE_CHECKING:` block so mypy and IDEs still resolve them. 296 public names verified to resolve. --- .../backend/impl/postgres/__init__.py | 1023 +++++++++++------ 1 file changed, 656 insertions(+), 367 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py index 0ee95195..1aa861dd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py @@ -1,6 +1,5 @@ # src/rhosocial/activerecord/backend/impl/postgres/__init__.py -""" -PostgreSQL backend implementation for the Python ORM. +"""PostgreSQL backend implementation for the Python ORM. This module provides: - PostgreSQL synchronous backend with connection management and query execution @@ -14,360 +13,659 @@ Architecture: - PostgreSQLBackend: Synchronous implementation using psycopg - AsyncPostgreSQLBackend: Asynchronous implementation using psycopg -- Independent from ORM frameworks - uses only native drivers -""" +- Independent from ORM frameworks - uses only native drivers""" -from .backend import PostgresBackend, AsyncPostgresBackend -from .collation import PostgresCollation # noqa: F401 -from .config import PostgresConnectionConfig -from .dialect import PostgresDialect -from .storage_parameters import ( # noqa: F401 - PostgresStorageParameter, - PostgresStorageParameterValueType, -) -from .transaction import PostgresTransactionManager, AsyncPostgresTransactionManager -from .expression.enum_ 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, - PostgresHalfvecType, - PostgresSparsevecType, - PostgresXID8Type, - PostgresXIDType, - PostgresXMLType, - PostgresCidrType, -) -from .explain import PostgresExplainResult, PostgresExplainPlanLine -from .expression import ( - PostgresCopyFormat, - PostgresCopyFromExpression, - PostgresCopyLogVerbosity, - PostgresCopyOnError, - PostgresCopyToExpression, - PostgresRepackExpression, -) -from .expression.ddl import ( - PostgresAddDomainCheckAction, - PostgresAddEnumValueAction, - PostgresAddTypeAttributeAction, - PostgresAlterDomainExpression, - PostgresAlterEnumTypeAddValueExpression, - PostgresAlterEnumTypeRenameValueExpression, - PostgresAlterTypeAttributeAction, - PostgresBaseTypeDefinition, - PostgresChangeDomainOwnerAction, - PostgresChangeTypeOwnerAction, - PostgresCompositeTypeAttribute, - PostgresCompositeTypeDefinition, - PostgresCreateDomainExpression, - PostgresCreateEnumTypeExpression, - PostgresCreateRangeTypeExpression, - PostgresDropDomainCheckAction, - PostgresDropDomainExpression, - PostgresDropEnumTypeExpression, - PostgresDropTypeAttributeAction, - PostgresDropTypeExpression, - PostgresEnumTypeDefinition, - PostgresRangeTypeDefinition, - PostgresRenameDomainConstraintAction, - PostgresRenameEnumValueAction, - PostgresRenameTypeAction, - PostgresRenameTypeAttributeAction, - PostgresSetDomainSchemaAction, - PostgresSetTypePropertiesAction, - PostgresSetTypeSchemaAction, - PostgresShellTypeDefinition, - PostgresValidateDomainConstraintAction, -) -from .adapters import ( - PostgresEnumAdapter, - PostgresRangeAdapter, - PostgresMultirangeAdapter, -) -from .type_values.range import ( - PostgresRange, - PostgresMultirange, -) -from .type_values.geometric import ( - Point, - Line, - LineSegment, - Box, - Path, - Polygon, - Circle, -) -from .adapters.geometric import PostgresGeometryAdapter -from .type_values.bit_string import PostgresBitString -from .adapters.bit_string import PostgresBitStringAdapter -from .type_values.monetary import PostgresMoney -from .adapters.monetary import PostgresMoneyAdapter -from .type_values.xml import PostgresXML -from .adapters.xml import PostgresXMLAdapter -from .type_values.network_address import PostgresMacaddr, PostgresMacaddr8 -from .adapters.network_address import PostgresMacaddrAdapter, PostgresMacaddr8Adapter -from .adapters.uuid import PostgresUUIDAdapter -from .type_values.pgvector import PostgresVector -from .adapters.pgvector import PostgresVectorAdapter -from .type_values.postgis import PostgresGeometry -from .adapters.postgis import PostgresPostGISAdapter -from .type_values.hstore import PostgresHstore -from .adapters.hstore import PostgresHstoreAdapter -from .type_values.text_search import ( - PostgresTsVector, - PostgresTsQuery, -) -from .functions.text_search import ( - to_tsvector, - to_tsquery, - plainto_tsquery, - phraseto_tsquery, - websearch_to_tsquery, - ts_matches, - ts_matches_expr, - ts_rank, - ts_rank_cd, - ts_headline, - tsvector_concat, - tsvector_strip, - tsvector_setweight, - tsvector_length, -) -from .adapters.text_search import PostgresTsVectorAdapter, PostgresTsQueryAdapter -from .type_values.pg_lsn import PostgresLsn -from .adapters.pg_lsn import PostgresLsnAdapter -from .type_values.object_identifier import ( - OID, - RegClass, - RegType, - RegProc, - RegProcedure, - RegOper, - RegOperator, - RegConfig, - RegDictionary, - RegNamespace, - RegRole, - RegCollation, - XID, - XID8, - CID, - TID, -) -from .adapters.object_identifier import PostgresOidAdapter, PostgresXidAdapter, PostgresTidAdapter -from .type_values.json import PostgresJsonPath -from .adapters.json import PostgresJsonPathAdapter -from .enum_type_manager import EnumTypeManager +# Public names are resolved on first access rather than at package-execution +# time. Importing this package used to import every submodule eagerly, which +# pulled in the DB driver through ``.backend`` and made the expression classes +# unreachable without it: Python executes a parent package's ``__init__`` before +# any submodule, so ``impl..expression`` could not be imported on its own. +# The exports themselves are unchanged -- attribute access, ``__all__``, ``dir()`` +# and ``from ... import *`` all behave as before. +from typing import TYPE_CHECKING -# Import functions from functions module -from .functions import ( - # Range functions - range_contains, - range_contained_by, - range_contains_range, - range_overlaps, - range_adjacent, - range_strictly_left_of, - range_strictly_right_of, - range_not_extend_right, - range_not_extend_left, - range_union, - range_intersection, - range_difference, - range_lower, - range_upper, - range_is_empty, - range_lower_inc, - range_upper_inc, - range_lower_inf, - range_upper_inf, - # Geometry functions - geometry_distance, - geometry_contains, - geometry_contained_by, - geometry_overlaps, - geometry_strictly_left, - geometry_strictly_right, - geometry_not_extend_right, - geometry_not_extend_left, - geometry_area, - geometry_center, - geometry_length, - geometry_width, - geometry_height, - geometry_npoints, - # Enum functions - enum_range, - enum_first, - enum_last, - enum_lt, - enum_le, - enum_gt, - enum_ge, - # Bit string functions - bit_concat, - bit_and, - bit_or, - bit_xor, - bit_not, - bit_shift_left, - bit_shift_right, - bit_length, - bit_length_func, - bit_octet_length, - bit_get_bit, - bit_set_bit, - bit_count, - # JSON functions - json_path_root, - json_path_key, - json_path_index, - json_path_wildcard, - json_path_filter, - jsonb_path_query, - jsonb_path_query_first, - jsonb_path_exists, - jsonb_path_match, - # pgvector functions - vector_l2_distance, - vector_cosine_distance, - vector_inner_product, - vector_cosine_similarity, - vector_literal, - # PostGIS functions - Construction - st_make_point, - st_geom_from_text, - st_geog_from_text, - st_set_srid, - st_transform, - # PostGIS functions - Predicates - st_contains, - st_intersects, - st_within, - st_dwithin, - st_crosses, - st_touches, - st_overlaps, - # PostGIS functions - Measurements - st_distance, - st_area, - st_length, - # PostGIS functions - Output - st_as_geojson, - st_as_text, - # PostGIS functions - Operations - st_buffer, - st_envelope, - st_centroid, - # hstore functions - Constructors - hstore_from_record, - hstore_from_key_value, - # hstore functions - Key/Value Extraction - hstore_akeys, - hstore_skeys, - hstore_avals, - hstore_svals, - hstore_each, - # hstore functions - Conversion - hstore_to_array, - hstore_to_matrix, - hstore_to_json, - hstore_to_jsonb, - hstore_to_json_loose, - hstore_to_jsonb_loose, - # hstore functions - Subset - hstore_slice, - # hstore functions - Existence - hstore_exist, - hstore_defined, - # hstore functions - Delete - hstore_delete, - hstore_delete_keys, - hstore_delete_pairs, - # hstore functions - Record - hstore_populate_record, - # hstore functions - Operators - hstore_get_value, - hstore_get_value_as_text, - hstore_get_values, - hstore_concat, - hstore_key_exists, - hstore_all_keys_exist, - hstore_any_key_exists, - hstore_contains, - hstore_contained_by, - hstore_subtract_key, - hstore_subtract_keys, - hstore_subtract_pairs, - hstore_to_array_operator, - hstore_to_matrix_operator, - hstore_record_update, - # hstore functions - Subscript - hstore_subscript_get, - hstore_subscript_set, -) +#: Public name -> (submodule that defines it, attribute name within it). +#: The two differ when the original import was aliased. +_EXPORTS = { + "AsyncPostgresBackend": (".backend", "AsyncPostgresBackend"), + "AsyncPostgresTransactionManager": (".transaction", "AsyncPostgresTransactionManager"), + "Box": (".type_values.geometric", "Box"), + "CID": (".type_values.object_identifier", "CID"), + "Circle": (".type_values.geometric", "Circle"), + "EnumTypeManager": (".type_values.enum", "EnumTypeManager"), + "Line": (".type_values.geometric", "Line"), + "LineSegment": (".type_values.geometric", "LineSegment"), + "OID": (".type_values.object_identifier", "OID"), + "Path": (".type_values.geometric", "Path"), + "Point": (".type_values.geometric", "Point"), + "Polygon": (".type_values.geometric", "Polygon"), + "PostgresAddDomainCheckAction": (".expression.ddl", "PostgresAddDomainCheckAction"), + "PostgresAddEnumValueAction": (".expression.ddl", "PostgresAddEnumValueAction"), + "PostgresAddTypeAttributeAction": (".expression.ddl", "PostgresAddTypeAttributeAction"), + "PostgresAlterDomainExpression": (".expression.ddl", "PostgresAlterDomainExpression"), + "PostgresAlterEnumTypeAddValueExpression": (".expression.ddl", "PostgresAlterEnumTypeAddValueExpression"), + "PostgresAlterEnumTypeRenameValueExpression": (".expression.ddl", "PostgresAlterEnumTypeRenameValueExpression"), + "PostgresAlterTypeAttributeAction": (".expression.ddl", "PostgresAlterTypeAttributeAction"), + "PostgresBackend": (".backend", "PostgresBackend"), + "PostgresBaseTypeDefinition": (".expression.ddl", "PostgresBaseTypeDefinition"), + "PostgresBigSerialType": (".expression.types", "PostgresBigSerialType"), + "PostgresBitString": (".type_values.bit_string", "PostgresBitString"), + "PostgresBitStringAdapter": (".adapters.bit_string", "PostgresBitStringAdapter"), + "PostgresBitType": (".expression.types", "PostgresBitType"), + "PostgresBoxType": (".expression.types", "PostgresBoxType"), + "PostgresByteaType": (".expression.types", "PostgresByteaType"), + "PostgresCIDType": (".expression.types", "PostgresCIDType"), + "PostgresChangeDomainOwnerAction": (".expression.ddl", "PostgresChangeDomainOwnerAction"), + "PostgresChangeTypeOwnerAction": (".expression.ddl", "PostgresChangeTypeOwnerAction"), + "PostgresCidrType": (".expression.types", "PostgresCidrType"), + "PostgresCircleType": (".expression.types", "PostgresCircleType"), + "PostgresCollation": (".collation", "PostgresCollation"), + "PostgresCompositeTypeAttribute": (".expression.ddl", "PostgresCompositeTypeAttribute"), + "PostgresCompositeTypeDefinition": (".expression.ddl", "PostgresCompositeTypeDefinition"), + "PostgresConnectionConfig": (".config", "PostgresConnectionConfig"), + "PostgresCopyFormat": (".expression", "PostgresCopyFormat"), + "PostgresCopyFromExpression": (".expression", "PostgresCopyFromExpression"), + "PostgresCopyLogVerbosity": (".expression", "PostgresCopyLogVerbosity"), + "PostgresCopyOnError": (".expression", "PostgresCopyOnError"), + "PostgresCopyToExpression": (".expression", "PostgresCopyToExpression"), + "PostgresCreateDomainExpression": (".expression.ddl", "PostgresCreateDomainExpression"), + "PostgresCreateEnumTypeExpression": (".expression.ddl", "PostgresCreateEnumTypeExpression"), + "PostgresCreateRangeTypeExpression": (".expression.ddl", "PostgresCreateRangeTypeExpression"), + "PostgresDateMultirangeType": (".expression.types", "PostgresDateMultirangeType"), + "PostgresDateRangeType": (".expression.types", "PostgresDateRangeType"), + "PostgresDialect": (".dialect", "PostgresDialect"), + "PostgresDropDomainCheckAction": (".expression.ddl", "PostgresDropDomainCheckAction"), + "PostgresDropDomainExpression": (".expression.ddl", "PostgresDropDomainExpression"), + "PostgresDropEnumTypeExpression": (".expression.ddl", "PostgresDropEnumTypeExpression"), + "PostgresDropTypeAttributeAction": (".expression.ddl", "PostgresDropTypeAttributeAction"), + "PostgresDropTypeExpression": (".expression.ddl", "PostgresDropTypeExpression"), + "PostgresEnumAdapter": (".adapters", "PostgresEnumAdapter"), + "PostgresEnumType": (".type_values", "PostgresEnumType"), + "PostgresEnumTypeDefinition": (".expression.ddl", "PostgresEnumTypeDefinition"), + "PostgresExplainPlanLine": (".explain", "PostgresExplainPlanLine"), + "PostgresExplainResult": (".explain", "PostgresExplainResult"), + "PostgresGeographyType": (".expression.types", "PostgresGeographyType"), + "PostgresGeometry": (".type_values.postgis", "PostgresGeometry"), + "PostgresGeometryAdapter": (".adapters.geometric", "PostgresGeometryAdapter"), + "PostgresGeometryType": (".expression.types", "PostgresGeometryType"), + "PostgresHalfvecType": (".expression.types", "PostgresHalfvecType"), + "PostgresHstore": (".type_values.hstore", "PostgresHstore"), + "PostgresHstoreAdapter": (".adapters.hstore", "PostgresHstoreAdapter"), + "PostgresHstoreType": (".expression.types", "PostgresHstoreType"), + "PostgresInetType": (".expression.types", "PostgresInetType"), + "PostgresInt4MultirangeType": (".expression.types", "PostgresInt4MultirangeType"), + "PostgresInt4RangeType": (".expression.types", "PostgresInt4RangeType"), + "PostgresInt8MultirangeType": (".expression.types", "PostgresInt8MultirangeType"), + "PostgresInt8RangeType": (".expression.types", "PostgresInt8RangeType"), + "PostgresJsonPath": (".type_values.json", "PostgresJsonPath"), + "PostgresJsonPathAdapter": (".adapters.json", "PostgresJsonPathAdapter"), + "PostgresJsonPathType": (".expression.types", "PostgresJsonPathType"), + "PostgresLineSegmentType": (".expression.types", "PostgresLineSegmentType"), + "PostgresLineType": (".expression.types", "PostgresLineType"), + "PostgresLsn": (".type_values.pg_lsn", "PostgresLsn"), + "PostgresLsnAdapter": (".adapters.pg_lsn", "PostgresLsnAdapter"), + "PostgresMacAddr8Type": (".expression.types", "PostgresMacAddr8Type"), + "PostgresMacAddrType": (".expression.types", "PostgresMacAddrType"), + "PostgresMacaddr": (".type_values.network_address", "PostgresMacaddr"), + "PostgresMacaddr8": (".type_values.network_address", "PostgresMacaddr8"), + "PostgresMacaddr8Adapter": (".adapters.network_address", "PostgresMacaddr8Adapter"), + "PostgresMacaddrAdapter": (".adapters.network_address", "PostgresMacaddrAdapter"), + "PostgresMoney": (".type_values.monetary", "PostgresMoney"), + "PostgresMoneyAdapter": (".adapters.monetary", "PostgresMoneyAdapter"), + "PostgresMoneyType": (".expression.types", "PostgresMoneyType"), + "PostgresMultirange": (".type_values.range", "PostgresMultirange"), + "PostgresMultirangeAdapter": (".adapters", "PostgresMultirangeAdapter"), + "PostgresNumMultirangeType": (".expression.types", "PostgresNumMultirangeType"), + "PostgresNumRangeType": (".expression.types", "PostgresNumRangeType"), + "PostgresOIDType": (".expression.types", "PostgresOIDType"), + "PostgresOidAdapter": (".adapters.object_identifier", "PostgresOidAdapter"), + "PostgresPathType": (".expression.types", "PostgresPathType"), + "PostgresPgLSNType": (".expression.types", "PostgresPgLSNType"), + "PostgresPointType": (".expression.types", "PostgresPointType"), + "PostgresPolygonType": (".expression.types", "PostgresPolygonType"), + "PostgresPostGISAdapter": (".adapters.postgis", "PostgresPostGISAdapter"), + "PostgresRange": (".type_values.range", "PostgresRange"), + "PostgresRangeAdapter": (".adapters", "PostgresRangeAdapter"), + "PostgresRangeTypeDefinition": (".expression.ddl", "PostgresRangeTypeDefinition"), + "PostgresRegClassType": (".expression.types", "PostgresRegClassType"), + "PostgresRegTypeType": (".expression.types", "PostgresRegTypeType"), + "PostgresRenameDomainConstraintAction": (".expression.ddl", "PostgresRenameDomainConstraintAction"), + "PostgresRenameEnumValueAction": (".expression.ddl", "PostgresRenameEnumValueAction"), + "PostgresRenameTypeAction": (".expression.ddl", "PostgresRenameTypeAction"), + "PostgresRenameTypeAttributeAction": (".expression.ddl", "PostgresRenameTypeAttributeAction"), + "PostgresRepackExpression": (".expression", "PostgresRepackExpression"), + "PostgresSerialType": (".expression.types", "PostgresSerialType"), + "PostgresSetDomainSchemaAction": (".expression.ddl", "PostgresSetDomainSchemaAction"), + "PostgresSetTypePropertiesAction": (".expression.ddl", "PostgresSetTypePropertiesAction"), + "PostgresSetTypeSchemaAction": (".expression.ddl", "PostgresSetTypeSchemaAction"), + "PostgresShellTypeDefinition": (".expression.ddl", "PostgresShellTypeDefinition"), + "PostgresSmallSerialType": (".expression.types", "PostgresSmallSerialType"), + "PostgresSparsevecType": (".expression.types", "PostgresSparsevecType"), + "PostgresStorageParameter": (".storage_parameters", "PostgresStorageParameter"), + "PostgresStorageParameterValueType": (".storage_parameters", "PostgresStorageParameterValueType"), + "PostgresTIDType": (".expression.types", "PostgresTIDType"), + "PostgresTSQueryType": (".expression.types", "PostgresTSQueryType"), + "PostgresTSVectorType": (".expression.types", "PostgresTSVectorType"), + "PostgresTidAdapter": (".adapters.object_identifier", "PostgresTidAdapter"), + "PostgresTransactionManager": (".transaction", "PostgresTransactionManager"), + "PostgresTsMultirangeType": (".expression.types", "PostgresTsMultirangeType"), + "PostgresTsQuery": (".type_values.text_search", "PostgresTsQuery"), + "PostgresTsQueryAdapter": (".adapters.text_search", "PostgresTsQueryAdapter"), + "PostgresTsRangeType": (".expression.types", "PostgresTsRangeType"), + "PostgresTsTzMultirangeType": (".expression.types", "PostgresTsTzMultirangeType"), + "PostgresTsTzRangeType": (".expression.types", "PostgresTsTzRangeType"), + "PostgresTsVector": (".type_values.text_search", "PostgresTsVector"), + "PostgresTsVectorAdapter": (".adapters.text_search", "PostgresTsVectorAdapter"), + "PostgresUUIDAdapter": (".adapters.uuid", "PostgresUUIDAdapter"), + "PostgresUUIDType": (".expression.types", "PostgresUUIDType"), + "PostgresValidateDomainConstraintAction": (".expression.ddl", "PostgresValidateDomainConstraintAction"), + "PostgresVarBitType": (".expression.types", "PostgresVarBitType"), + "PostgresVector": (".type_values.pgvector", "PostgresVector"), + "PostgresVectorAdapter": (".adapters.pgvector", "PostgresVectorAdapter"), + "PostgresVectorType": (".expression.types", "PostgresVectorType"), + "PostgresXID8Type": (".expression.types", "PostgresXID8Type"), + "PostgresXIDType": (".expression.types", "PostgresXIDType"), + "PostgresXML": (".type_values.xml", "PostgresXML"), + "PostgresXMLAdapter": (".adapters.xml", "PostgresXMLAdapter"), + "PostgresXMLType": (".expression.types", "PostgresXMLType"), + "PostgresXidAdapter": (".adapters.object_identifier", "PostgresXidAdapter"), + "RegClass": (".type_values.object_identifier", "RegClass"), + "RegCollation": (".type_values.object_identifier", "RegCollation"), + "RegConfig": (".type_values.object_identifier", "RegConfig"), + "RegDictionary": (".type_values.object_identifier", "RegDictionary"), + "RegNamespace": (".type_values.object_identifier", "RegNamespace"), + "RegOper": (".type_values.object_identifier", "RegOper"), + "RegOperator": (".type_values.object_identifier", "RegOperator"), + "RegProc": (".type_values.object_identifier", "RegProc"), + "RegProcedure": (".type_values.object_identifier", "RegProcedure"), + "RegRole": (".type_values.object_identifier", "RegRole"), + "RegType": (".type_values.object_identifier", "RegType"), + "TID": (".type_values.object_identifier", "TID"), + "XID": (".type_values.object_identifier", "XID"), + "XID8": (".type_values.object_identifier", "XID8"), + "bit_and": (".functions", "bit_and"), + "bit_concat": (".functions", "bit_concat"), + "bit_count": (".functions", "bit_count"), + "bit_get_bit": (".functions", "bit_get_bit"), + "bit_length": (".functions", "bit_length"), + "bit_length_func": (".functions", "bit_length_func"), + "bit_not": (".functions", "bit_not"), + "bit_octet_length": (".functions", "bit_octet_length"), + "bit_or": (".functions", "bit_or"), + "bit_set_bit": (".functions", "bit_set_bit"), + "bit_shift_left": (".functions", "bit_shift_left"), + "bit_shift_right": (".functions", "bit_shift_right"), + "bit_xor": (".functions", "bit_xor"), + "enum_first": (".functions", "enum_first"), + "enum_ge": (".functions", "enum_ge"), + "enum_gt": (".functions", "enum_gt"), + "enum_last": (".functions", "enum_last"), + "enum_le": (".functions", "enum_le"), + "enum_lt": (".functions", "enum_lt"), + "enum_range": (".functions", "enum_range"), + "gen_random_uuid": (".functions", "gen_random_uuid"), + "geometry_area": (".functions", "geometry_area"), + "geometry_center": (".functions", "geometry_center"), + "geometry_contained_by": (".functions", "geometry_contained_by"), + "geometry_contains": (".functions", "geometry_contains"), + "geometry_distance": (".functions", "geometry_distance"), + "geometry_height": (".functions", "geometry_height"), + "geometry_length": (".functions", "geometry_length"), + "geometry_not_extend_left": (".functions", "geometry_not_extend_left"), + "geometry_not_extend_right": (".functions", "geometry_not_extend_right"), + "geometry_npoints": (".functions", "geometry_npoints"), + "geometry_overlaps": (".functions", "geometry_overlaps"), + "geometry_strictly_left": (".functions", "geometry_strictly_left"), + "geometry_strictly_right": (".functions", "geometry_strictly_right"), + "geometry_width": (".functions", "geometry_width"), + "hstore_akeys": (".functions", "hstore_akeys"), + "hstore_all_keys_exist": (".functions", "hstore_all_keys_exist"), + "hstore_any_key_exists": (".functions", "hstore_any_key_exists"), + "hstore_avals": (".functions", "hstore_avals"), + "hstore_concat": (".functions", "hstore_concat"), + "hstore_contained_by": (".functions", "hstore_contained_by"), + "hstore_contains": (".functions", "hstore_contains"), + "hstore_defined": (".functions", "hstore_defined"), + "hstore_delete": (".functions", "hstore_delete"), + "hstore_delete_keys": (".functions", "hstore_delete_keys"), + "hstore_delete_pairs": (".functions", "hstore_delete_pairs"), + "hstore_each": (".functions", "hstore_each"), + "hstore_exist": (".functions", "hstore_exist"), + "hstore_from_key_value": (".functions", "hstore_from_key_value"), + "hstore_from_record": (".functions", "hstore_from_record"), + "hstore_get_value": (".functions", "hstore_get_value"), + "hstore_get_value_as_text": (".functions", "hstore_get_value_as_text"), + "hstore_get_values": (".functions", "hstore_get_values"), + "hstore_key_exists": (".functions", "hstore_key_exists"), + "hstore_populate_record": (".functions", "hstore_populate_record"), + "hstore_record_update": (".functions", "hstore_record_update"), + "hstore_skeys": (".functions", "hstore_skeys"), + "hstore_slice": (".functions", "hstore_slice"), + "hstore_subscript_get": (".functions", "hstore_subscript_get"), + "hstore_subscript_set": (".functions", "hstore_subscript_set"), + "hstore_subtract_key": (".functions", "hstore_subtract_key"), + "hstore_subtract_keys": (".functions", "hstore_subtract_keys"), + "hstore_subtract_pairs": (".functions", "hstore_subtract_pairs"), + "hstore_svals": (".functions", "hstore_svals"), + "hstore_to_array": (".functions", "hstore_to_array"), + "hstore_to_array_operator": (".functions", "hstore_to_array_operator"), + "hstore_to_json": (".functions", "hstore_to_json"), + "hstore_to_json_loose": (".functions", "hstore_to_json_loose"), + "hstore_to_jsonb": (".functions", "hstore_to_jsonb"), + "hstore_to_jsonb_loose": (".functions", "hstore_to_jsonb_loose"), + "hstore_to_matrix": (".functions", "hstore_to_matrix"), + "hstore_to_matrix_operator": (".functions", "hstore_to_matrix_operator"), + "json_path_filter": (".functions", "json_path_filter"), + "json_path_index": (".functions", "json_path_index"), + "json_path_key": (".functions", "json_path_key"), + "json_path_root": (".functions", "json_path_root"), + "json_path_wildcard": (".functions", "json_path_wildcard"), + "jsonb_path_exists": (".functions", "jsonb_path_exists"), + "jsonb_path_match": (".functions", "jsonb_path_match"), + "jsonb_path_query": (".functions", "jsonb_path_query"), + "jsonb_path_query_first": (".functions", "jsonb_path_query_first"), + "phraseto_tsquery": (".functions.text_search", "phraseto_tsquery"), + "plainto_tsquery": (".functions.text_search", "plainto_tsquery"), + "range_adjacent": (".functions", "range_adjacent"), + "range_contained_by": (".functions", "range_contained_by"), + "range_contains": (".functions", "range_contains"), + "range_contains_range": (".functions", "range_contains_range"), + "range_difference": (".functions", "range_difference"), + "range_intersection": (".functions", "range_intersection"), + "range_is_empty": (".functions", "range_is_empty"), + "range_lower": (".functions", "range_lower"), + "range_lower_inc": (".functions", "range_lower_inc"), + "range_lower_inf": (".functions", "range_lower_inf"), + "range_not_extend_left": (".functions", "range_not_extend_left"), + "range_not_extend_right": (".functions", "range_not_extend_right"), + "range_overlaps": (".functions", "range_overlaps"), + "range_strictly_left_of": (".functions", "range_strictly_left_of"), + "range_strictly_right_of": (".functions", "range_strictly_right_of"), + "range_union": (".functions", "range_union"), + "range_upper": (".functions", "range_upper"), + "range_upper_inc": (".functions", "range_upper_inc"), + "range_upper_inf": (".functions", "range_upper_inf"), + "st_area": (".functions", "st_area"), + "st_as_geojson": (".functions", "st_as_geojson"), + "st_as_text": (".functions", "st_as_text"), + "st_buffer": (".functions", "st_buffer"), + "st_centroid": (".functions", "st_centroid"), + "st_contains": (".functions", "st_contains"), + "st_crosses": (".functions", "st_crosses"), + "st_distance": (".functions", "st_distance"), + "st_dwithin": (".functions", "st_dwithin"), + "st_envelope": (".functions", "st_envelope"), + "st_geog_from_text": (".functions", "st_geog_from_text"), + "st_geom_from_text": (".functions", "st_geom_from_text"), + "st_intersects": (".functions", "st_intersects"), + "st_length": (".functions", "st_length"), + "st_make_point": (".functions", "st_make_point"), + "st_overlaps": (".functions", "st_overlaps"), + "st_set_srid": (".functions", "st_set_srid"), + "st_touches": (".functions", "st_touches"), + "st_transform": (".functions", "st_transform"), + "st_within": (".functions", "st_within"), + "to_tsquery": (".functions.text_search", "to_tsquery"), + "to_tsvector": (".functions.text_search", "to_tsvector"), + "ts_headline": (".functions.text_search", "ts_headline"), + "ts_matches": (".functions.text_search", "ts_matches"), + "ts_matches_expr": (".functions.text_search", "ts_matches_expr"), + "ts_rank": (".functions.text_search", "ts_rank"), + "ts_rank_cd": (".functions.text_search", "ts_rank_cd"), + "tsvector_concat": (".functions.text_search", "tsvector_concat"), + "tsvector_length": (".functions.text_search", "tsvector_length"), + "tsvector_setweight": (".functions.text_search", "tsvector_setweight"), + "tsvector_strip": (".functions.text_search", "tsvector_strip"), + "uuid_default_generator": (".functions", "uuid_default_generator"), + "vector_cosine_distance": (".functions", "vector_cosine_distance"), + "vector_cosine_similarity": (".functions", "vector_cosine_similarity"), + "vector_inner_product": (".functions", "vector_inner_product"), + "vector_l2_distance": (".functions", "vector_l2_distance"), + "vector_literal": (".functions", "vector_literal"), + "websearch_to_tsquery": (".functions.text_search", "websearch_to_tsquery"), +} +if TYPE_CHECKING: # pragma: no cover - re-exports for type checkers + from .backend import PostgresBackend, AsyncPostgresBackend + from .collation import PostgresCollation # noqa: F401 + from .config import PostgresConnectionConfig + from .dialect import PostgresDialect + from .storage_parameters import ( # noqa: F401 + PostgresStorageParameter, + PostgresStorageParameterValueType, + ) + 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, + PostgresHalfvecType, + PostgresSparsevecType, + PostgresXID8Type, + PostgresXIDType, + PostgresXMLType, + PostgresCidrType, + ) + from .explain import PostgresExplainResult, PostgresExplainPlanLine + from .expression import ( + PostgresCopyFormat, + PostgresCopyFromExpression, + PostgresCopyLogVerbosity, + PostgresCopyOnError, + PostgresCopyToExpression, + PostgresRepackExpression, + ) + from .expression.ddl import ( + PostgresAddDomainCheckAction, + PostgresAddEnumValueAction, + PostgresAddTypeAttributeAction, + PostgresAlterDomainExpression, + PostgresAlterEnumTypeAddValueExpression, + PostgresAlterEnumTypeRenameValueExpression, + PostgresAlterTypeAttributeAction, + PostgresBaseTypeDefinition, + PostgresChangeDomainOwnerAction, + PostgresChangeTypeOwnerAction, + PostgresCompositeTypeAttribute, + PostgresCompositeTypeDefinition, + PostgresCreateDomainExpression, + PostgresCreateEnumTypeExpression, + PostgresCreateRangeTypeExpression, + PostgresDropDomainCheckAction, + PostgresDropDomainExpression, + PostgresDropEnumTypeExpression, + PostgresDropTypeAttributeAction, + PostgresDropTypeExpression, + PostgresEnumTypeDefinition, + PostgresRangeTypeDefinition, + PostgresRenameDomainConstraintAction, + PostgresRenameEnumValueAction, + PostgresRenameTypeAction, + PostgresRenameTypeAttributeAction, + PostgresSetDomainSchemaAction, + PostgresSetTypePropertiesAction, + PostgresSetTypeSchemaAction, + PostgresShellTypeDefinition, + PostgresValidateDomainConstraintAction, + ) + from .adapters import ( + PostgresEnumAdapter, + PostgresRangeAdapter, + PostgresMultirangeAdapter, + ) + from .types.range import ( + PostgresRange, + PostgresMultirange, + ) + from .types.geometric import ( + Point, + Line, + LineSegment, + Box, + Path, + Polygon, + Circle, + ) + from .adapters.geometric import PostgresGeometryAdapter + from .types.bit_string import PostgresBitString + from .adapters.bit_string import PostgresBitStringAdapter + from .types.monetary import PostgresMoney + from .adapters.monetary import PostgresMoneyAdapter + from .types.xml import PostgresXML + from .adapters.xml import PostgresXMLAdapter + from .types.network_address import PostgresMacaddr, PostgresMacaddr8 + from .adapters.network_address import PostgresMacaddrAdapter, PostgresMacaddr8Adapter + from .adapters.uuid import PostgresUUIDAdapter + from .types.pgvector import PostgresVector + from .adapters.pgvector import PostgresVectorAdapter + from .types.postgis import PostgresGeometry + from .adapters.postgis import PostgresPostGISAdapter + from .types.hstore import PostgresHstore + from .adapters.hstore import PostgresHstoreAdapter + from .types.text_search import ( + PostgresTsVector, + PostgresTsQuery, + ) + from .functions.text_search import ( + to_tsvector, + to_tsquery, + plainto_tsquery, + phraseto_tsquery, + websearch_to_tsquery, + ts_matches, + ts_matches_expr, + ts_rank, + ts_rank_cd, + ts_headline, + tsvector_concat, + tsvector_strip, + tsvector_setweight, + tsvector_length, + ) + from .adapters.text_search import PostgresTsVectorAdapter, PostgresTsQueryAdapter + from .types.pg_lsn import PostgresLsn + from .adapters.pg_lsn import PostgresLsnAdapter + from .types.object_identifier import ( + OID, + RegClass, + RegType, + RegProc, + RegProcedure, + RegOper, + RegOperator, + RegConfig, + RegDictionary, + RegNamespace, + RegRole, + RegCollation, + XID, + XID8, + CID, + TID, + ) + from .adapters.object_identifier import PostgresOidAdapter, PostgresXidAdapter, PostgresTidAdapter + from .types.json import PostgresJsonPath + from .adapters.json import PostgresJsonPathAdapter + from .types.enum import EnumTypeManager + from .functions import ( + # Range functions + range_contains, + range_contained_by, + range_contains_range, + range_overlaps, + range_adjacent, + range_strictly_left_of, + range_strictly_right_of, + range_not_extend_right, + range_not_extend_left, + range_union, + range_intersection, + range_difference, + range_lower, + range_upper, + range_is_empty, + range_lower_inc, + range_upper_inc, + range_lower_inf, + range_upper_inf, + # Geometry functions + geometry_distance, + geometry_contains, + geometry_contained_by, + geometry_overlaps, + geometry_strictly_left, + geometry_strictly_right, + geometry_not_extend_right, + geometry_not_extend_left, + geometry_area, + geometry_center, + geometry_length, + geometry_width, + geometry_height, + geometry_npoints, + # Enum functions + enum_range, + enum_first, + enum_last, + enum_lt, + enum_le, + enum_gt, + enum_ge, + # Bit string functions + bit_concat, + bit_and, + bit_or, + bit_xor, + bit_not, + bit_shift_left, + bit_shift_right, + bit_length, + bit_length_func, + bit_octet_length, + bit_get_bit, + bit_set_bit, + bit_count, + # JSON functions + json_path_root, + json_path_key, + json_path_index, + json_path_wildcard, + json_path_filter, + jsonb_path_query, + jsonb_path_query_first, + jsonb_path_exists, + jsonb_path_match, + # pgvector functions + vector_l2_distance, + vector_cosine_distance, + vector_inner_product, + vector_cosine_similarity, + vector_literal, + # PostGIS functions - Construction + st_make_point, + st_geom_from_text, + st_geog_from_text, + st_set_srid, + st_transform, + # PostGIS functions - Predicates + st_contains, + st_intersects, + st_within, + st_dwithin, + st_crosses, + st_touches, + st_overlaps, + # PostGIS functions - Measurements + st_distance, + st_area, + st_length, + # PostGIS functions - Output + st_as_geojson, + st_as_text, + # PostGIS functions - Operations + st_buffer, + st_envelope, + st_centroid, + # hstore functions - Constructors + hstore_from_record, + hstore_from_key_value, + # hstore functions - Key/Value Extraction + hstore_akeys, + hstore_skeys, + hstore_avals, + hstore_svals, + hstore_each, + # hstore functions - Conversion + hstore_to_array, + hstore_to_matrix, + hstore_to_json, + hstore_to_jsonb, + hstore_to_json_loose, + hstore_to_jsonb_loose, + # hstore functions - Subset + hstore_slice, + # hstore functions - Existence + hstore_exist, + hstore_defined, + # hstore functions - Delete + hstore_delete, + hstore_delete_keys, + hstore_delete_pairs, + # hstore functions - Record + hstore_populate_record, + # hstore functions - Operators + hstore_get_value, + hstore_get_value_as_text, + hstore_get_values, + hstore_concat, + hstore_key_exists, + hstore_all_keys_exist, + hstore_any_key_exists, + hstore_contains, + hstore_contained_by, + hstore_subtract_key, + hstore_subtract_keys, + hstore_subtract_pairs, + hstore_to_array_operator, + hstore_to_matrix_operator, + hstore_record_update, + # hstore functions - Subscript + hstore_subscript_get, + hstore_subscript_set, + ) __all__ = [ - # Synchronous Backend "PostgresBackend", - # Asynchronous Backend "AsyncPostgresBackend", - # Configuration "PostgresConnectionConfig", - # Dialect related "PostgresDialect", - # PostgreSQL Relation Storage Parameters "PostgresStorageParameter", "PostgresStorageParameterValueType", - # Transaction - Sync and Async "PostgresTransactionManager", "AsyncPostgresTransactionManager", - # PostgreSQL-specific Type Helpers "PostgresEnumType", "PostgresEnumAdapter", - # PostgreSQL EXPLAIN Result Types "PostgresExplainResult", "PostgresExplainPlanLine", "PostgresCopyFormat", @@ -376,7 +674,6 @@ "PostgresCopyOnError", "PostgresCopyToExpression", "PostgresRepackExpression", - # PostgreSQL DDL Statements "PostgresCompositeTypeAttribute", "PostgresCompositeTypeDefinition", "PostgresEnumTypeDefinition", @@ -408,7 +705,6 @@ "PostgresCreateDomainExpression", "PostgresAlterDomainExpression", "PostgresDropDomainExpression", - # Range Types "PostgresRange", "PostgresRangeAdapter", "PostgresMultirange", @@ -432,7 +728,6 @@ "range_upper_inc", "range_lower_inf", "range_upper_inf", - # Geometry Types "Point", "Line", "LineSegment", @@ -455,7 +750,6 @@ "geometry_width", "geometry_height", "geometry_npoints", - # Bit String Types "PostgresBitString", "PostgresBitStringAdapter", "bit_concat", @@ -471,7 +765,6 @@ "bit_get_bit", "bit_set_bit", "bit_count", - # Enum Types "EnumTypeManager", "enum_range", "enum_first", @@ -480,18 +773,14 @@ "enum_le", "enum_gt", "enum_ge", - # Money Type "PostgresMoney", "PostgresMoneyAdapter", - # XML Type "PostgresXML", "PostgresXMLAdapter", - # MACADDR Types "PostgresMacaddr", "PostgresMacaddrAdapter", "PostgresMacaddr8", "PostgresMacaddr8Adapter", - # Text Search Types "PostgresTsVector", "PostgresTsVectorAdapter", "PostgresTsQuery", @@ -510,10 +799,8 @@ "tsvector_strip", "tsvector_setweight", "tsvector_length", - # LSN Type "PostgresLsn", "PostgresLsnAdapter", - # OID Types "OID", "RegClass", "RegType", @@ -533,7 +820,6 @@ "PostgresOidAdapter", "PostgresXidAdapter", "PostgresTidAdapter", - # JSON Path Type "PostgresJsonPath", "PostgresJsonPathAdapter", "json_path_root", @@ -545,11 +831,9 @@ "jsonb_path_query_first", "jsonb_path_exists", "jsonb_path_match", - # UUID Type "PostgresUUIDAdapter", "gen_random_uuid", "uuid_default_generator", - # pgvector Types "PostgresVector", "PostgresVectorAdapter", "vector_l2_distance", @@ -557,10 +841,8 @@ "vector_inner_product", "vector_cosine_similarity", "vector_literal", - # PostGIS Types "PostgresGeometry", "PostgresPostGISAdapter", - # hstore Types "PostgresHstore", "PostgresHstoreAdapter", "st_make_point", @@ -583,34 +865,26 @@ "st_buffer", "st_envelope", "st_centroid", - # hstore functions - Constructors "hstore_from_record", "hstore_from_key_value", - # hstore functions - Key/Value Extraction "hstore_akeys", "hstore_skeys", "hstore_avals", "hstore_svals", "hstore_each", - # hstore functions - Conversion "hstore_to_array", "hstore_to_matrix", "hstore_to_json", "hstore_to_jsonb", "hstore_to_json_loose", "hstore_to_jsonb_loose", - # hstore functions - Subset "hstore_slice", - # hstore functions - Existence "hstore_exist", "hstore_defined", - # hstore functions - Delete "hstore_delete", "hstore_delete_keys", "hstore_delete_pairs", - # hstore functions - Record "hstore_populate_record", - # hstore functions - Operators "hstore_get_value", "hstore_get_value_as_text", "hstore_get_values", @@ -626,10 +900,8 @@ "hstore_to_array_operator", "hstore_to_matrix_operator", "hstore_record_update", - # hstore functions - Subscript "hstore_subscript_get", "hstore_subscript_set", - # PostgreSQL DataType subclasses for DDL "PostgresBigSerialType", "PostgresBitType", "PostgresBoxType", @@ -680,3 +952,20 @@ "PostgresXMLType", "PostgresCidrType", ] + + +def __getattr__(name: str): + """Resolve a public name by importing its defining submodule once.""" + target = _EXPORTS.get(name) + if target is not None: + import importlib + + module_name, attr = target + value = getattr(importlib.import_module(module_name, __name__), attr) + globals()[name] = value # cache, so __getattr__ runs at most once per name + return value + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__(): + return sorted(set(globals()) | set(_EXPORTS)) From 635e4cea219fd81e1b108ec1a8b1039700b961c0 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 15:19:59 +0800 Subject: [PATCH 04/19] refactor(backend): make impl. an open package Drop the top-level impl//__init__.py so the directory becomes a PEP 420 namespace portion and no longer re-exports every public name. Each name is now imported from the sub-package that defines it, so a symbol has one public path instead of two. The facade also made "expression is importable on its own" unenforceable: importing impl..expression executed the parent __init__, which pulled in the driver. With the file gone, the sub-packages stand alone by construction rather than by a lazy __getattr__ that each backend had to maintain separately. This is a breaking change for anyone importing `from rhosocial.activerecord.backend.impl. import X`; the replacement is `...impl.. import X`. --- .claude/architecture.md | 4 +- .claude/commands/gen-model.md | 2 +- .claude/commands/validate-model.md | 2 +- README.md | 2 +- docs/en_US/ddl/materialized_view.md | 4 +- .../installation_and_configuration/README.md | 6 +- .../backend_group.md | 3 +- .../configuration.md | 4 +- .../installation.md | 2 +- .../installation_and_configuration/pool.md | 4 +- docs/en_US/introduction/relationship.md | 2 +- docs/en_US/plugins/README.md | 2 +- .../postgres_specific_features/dialect.md | 4 +- .../introspection.md | 10 +- .../property_graph_query.md | 2 +- .../protocol_support.md | 10 +- docs/en_US/scenarios/parallel_workers.md | 32 +- docs/en_US/testing/configuration.md | 28 +- docs/en_US/transaction_support/README.md | 2 +- docs/en_US/transaction_support/deferrable.md | 2 +- .../transaction_support/isolation_level.md | 2 +- docs/en_US/troubleshooting/connection.md | 6 +- docs/en_US/troubleshooting/performance.md | 10 +- docs/en_US/type_adapters/README.md | 34 +- .../connection_pool_stress_test_async.py | 2 +- .../connection_pool_stress_test_sync.py | 2 +- docs/examples/chapter_04_ddl/ddl.py | 4 +- .../chapter_05_worker_isolation/config.py | 2 +- .../exp1_shared_connection.py | 4 +- .../exp2_isolated_connection.py | 4 +- .../exp3_endurance_test.py | 4 +- .../parallel_workers/config_loader.py | 6 +- .../exp1_basic_multiprocess.py | 2 +- .../exp2_postgres_async_advantage.py | 2 +- .../parallel_workers/exp3_deadlock_wrong.py | 2 +- .../exp4_partition_correct.py | 2 +- .../exp5_multithread_warning.py | 2 +- .../exp6_backend_group_pool.py | 3 +- .../parallel_workers/setup_db.py | 2 +- .../chapter_10_fastapi/config_loader.py | 2 +- docs/examples/chapter_10_fastapi/database.py | 2 +- docs/zh_CN/ddl/materialized_view.md | 4 +- .../installation_and_configuration/README.md | 6 +- .../configuration.md | 4 +- .../installation.md | 2 +- .../installation_and_configuration/pool.md | 4 +- docs/zh_CN/introduction/relationship.md | 9 +- docs/zh_CN/plugins/README.md | 2 +- .../postgres_specific_features/dialect.md | 4 +- .../introspection.md | 10 +- .../property_graph_query.md | 2 +- .../protocol_support.md | 6 +- docs/zh_CN/scenarios/parallel_workers.md | 32 +- docs/zh_CN/testing/configuration.md | 28 +- docs/zh_CN/transaction_support/README.md | 2 +- docs/zh_CN/transaction_support/deferrable.md | 2 +- .../transaction_support/isolation_level.md | 2 +- docs/zh_CN/troubleshooting/connection.md | 6 +- docs/zh_CN/troubleshooting/performance.md | 10 +- docs/zh_CN/type_adapters/README.md | 34 +- .../backend/impl/postgres/__init__.py | 971 ------------------ .../backend/impl/postgres/adapters/json.py | 5 +- .../impl/postgres/adapters/monetary.py | 2 +- .../backend/impl/postgres/adapters/xml.py | 5 +- .../impl/postgres/backend/async_backend.py | 2 +- .../backend/impl/postgres/backend/sync.py | 2 +- .../backend/impl/postgres/cli/connection.py | 2 +- .../backend/impl/postgres/cli/info.py | 2 +- .../backend/impl/postgres/cli/introspect.py | 5 +- .../impl/postgres/cli/named_expression.py | 5 +- .../impl/postgres/cli/named_migration.py | 5 +- .../impl/postgres/cli/named_procedure.py | 5 +- .../postgres/cli/named_procedure_graph.py | 4 +- .../backend/impl/postgres/cli/query.py | 5 +- .../backend/impl/postgres/cli/status.py | 5 +- .../examples/connection/quickstart.py | 2 +- .../impl/postgres/examples/ddl/alter_table.py | 2 +- .../postgres/examples/ddl/create_index.py | 2 +- .../postgres/examples/ddl/create_table.py | 2 +- .../impl/postgres/examples/ddl/drop_table.py | 2 +- .../examples/ddl/materialized_view.py | 4 +- .../impl/postgres/examples/ddl/partition.py | 2 +- .../postgres/examples/ddl/unique_index.py | 2 +- .../impl/postgres/examples/ddl/view.py | 2 +- .../impl/postgres/examples/delete/basic.py | 2 +- .../extensions/address_standardizer.py | 2 +- .../postgres/examples/extensions/bloom.py | 2 +- .../postgres/examples/extensions/btree_gin.py | 2 +- .../examples/extensions/btree_gist.py | 2 +- .../postgres/examples/extensions/citext.py | 2 +- .../impl/postgres/examples/extensions/cube.py | 2 +- .../examples/extensions/earthdistance.py | 2 +- .../examples/extensions/fuzzystrmatch.py | 2 +- .../postgres/examples/extensions/hstore.py | 2 +- .../postgres/examples/extensions/hypopg.py | 2 +- .../postgres/examples/extensions/intarray.py | 2 +- .../postgres/examples/extensions/ltree.py | 2 +- .../postgres/examples/extensions/orafce.py | 2 +- .../postgres/examples/extensions/pg_cron.py | 2 +- .../examples/extensions/pg_partman.py | 2 +- .../postgres/examples/extensions/pg_repack.py | 2 +- .../examples/extensions/pg_stat_statements.py | 2 +- .../examples/extensions/pg_surgery.py | 2 +- .../postgres/examples/extensions/pg_trgm.py | 2 +- .../examples/extensions/pg_walinspect.py | 2 +- .../postgres/examples/extensions/pgaudit.py | 2 +- .../postgres/examples/extensions/pgcrypto.py | 2 +- .../postgres/examples/extensions/pglogical.py | 2 +- .../postgres/examples/extensions/pgrouting.py | 2 +- .../postgres/examples/extensions/pgvector.py | 2 +- .../postgres/examples/extensions/postgis.py | 2 +- .../examples/extensions/postgis_raster.py | 2 +- .../postgres/examples/extensions/tablefunc.py | 2 +- .../postgres/examples/extensions/uuid_ossp.py | 2 +- .../impl/postgres/examples/insert/batch.py | 2 +- .../impl/postgres/examples/insert/single.py | 2 +- .../impl/postgres/examples/insert/upsert.py | 2 +- .../examples/insert/with_returning.py | 2 +- .../named_expressions/order_expressions.py | 3 +- .../examples/named_migrations/run_basic.py | 2 +- .../examples/named_migrations/run_chain.py | 2 +- .../named_procedures/order_workflow.py | 3 +- .../pgq/recursive_cte_graph_traversal.py | 2 +- .../impl/postgres/examples/query/aggregate.py | 2 +- .../postgres/examples/query/array_func.py | 2 +- .../impl/postgres/examples/query/basic.py | 2 +- .../impl/postgres/examples/query/constants.py | 2 +- .../impl/postgres/examples/query/cte.py | 2 +- .../impl/postgres/examples/query/distinct.py | 2 +- .../impl/postgres/examples/query/explain.py | 2 +- .../impl/postgres/examples/query/join.py | 2 +- .../postgres/examples/query/pagination.py | 2 +- .../impl/postgres/examples/query/predicate.py | 2 +- .../impl/postgres/examples/query/subquery.py | 2 +- .../postgres/examples/query/text_search.py | 2 +- .../impl/postgres/examples/query/union.py | 2 +- .../impl/postgres/examples/query/window.py | 2 +- .../examples/query/window_functions.py | 2 +- .../examples/schema_diff/array_type_diff.py | 2 +- .../schema_diff/column_order_irrelevant.py | 2 +- .../examples/schema_diff/index_type_diff.py | 2 +- .../schema_diff/postgres10_identity.py | 2 +- .../schema_diff/serialization_roundtrip.py | 2 +- .../examples/schema_diff/table_changes.py | 2 +- .../examples/sqlxml/standard_expressions.py | 8 +- .../postgres/examples/transaction/basic.py | 2 +- .../examples/transaction/exclusive.py | 2 +- .../examples/transaction/for_update.py | 2 +- .../examples/types/array_postgres84.py | 2 +- .../postgres/examples/types/json_basic.py | 2 +- .../impl/postgres/examples/types/uuid.py | 2 +- .../impl/postgres/examples/update/basic.py | 2 +- .../impl/postgres/expression/ddl/__init__.py | 2 +- .../impl/postgres/expression/ddl/cluster.py | 2 +- .../impl/postgres/expression/ddl/comment.py | 2 +- .../impl/postgres/expression/ddl/extension.py | 4 +- .../impl/postgres/expression/ddl/index.py | 6 +- .../impl/postgres/expression/ddl/mv.py | 6 +- .../impl/postgres/expression/ddl/partition.py | 8 +- .../impl/postgres/expression/ddl/policy.py | 2 +- .../postgres/expression/ddl/rls_config.py | 2 +- .../postgres/expression/ddl/statistics.py | 4 +- .../postgres/expression/ddl/table_settings.py | 2 +- .../impl/postgres/expression/ddl/vacuum.py | 4 +- .../backend/impl/postgres/expression/enum_.py | 2 +- .../backend/impl/postgres/functions/citext.py | 2 +- .../backend/impl/postgres/functions/cube.py | 2 +- .../impl/postgres/functions/data_type.py | 4 +- .../impl/postgres/functions/earthdistance.py | 2 +- .../impl/postgres/functions/fuzzystrmatch.py | 2 +- .../impl/postgres/functions/intarray.py | 2 +- .../backend/impl/postgres/functions/ltree.py | 2 +- .../impl/postgres/functions/pg_cron.py | 2 +- .../postgres/functions/pg_stat_statements.py | 2 +- .../impl/postgres/functions/pg_trgm.py | 8 +- .../impl/postgres/functions/pgaudit.py | 2 +- .../impl/postgres/functions/pgcrypto.py | 2 +- .../impl/postgres/functions/pglogical.py | 2 +- .../impl/postgres/functions/pgrouting.py | 2 +- .../impl/postgres/functions/pgvector.py | 2 +- .../impl/postgres/functions/postgis_raster.py | 2 +- .../backend/impl/postgres/functions/range.py | 2 +- .../impl/postgres/functions/tablefunc.py | 2 +- .../introspection/status_introspector.py | 4 +- .../postgres/named_expressions/partition.py | 8 +- tests/benchmark/backend/conftest.py | 4 +- tests/providers/basic.py | 2 +- tests/providers/basic_connection.py | 4 +- tests/providers/crud_benchmark.py | 2 +- tests/providers/events.py | 2 +- tests/providers/fastapi_benchmark.py | 6 +- tests/providers/fixtures/basic.py | 2 +- tests/providers/fixtures/events.py | 4 +- tests/providers/fixtures/mixins.py | 4 +- tests/providers/fixtures/query.py | 4 +- tests/providers/mixins.py | 2 +- tests/providers/partition.py | 2 +- tests/providers/query.py | 8 +- tests/providers/query_connection.py | 4 +- tests/providers/relation.py | 2 +- tests/providers/scenarios.py | 2 +- .../feature/backend/conftest.py | 5 +- .../test_introspection_default_schema.py | 2 +- .../postgres/test_connection_config.py | 4 +- .../backend/postgres/test_ddl_expressions.py | 2 +- .../backend/postgres/test_enum_types.py | 2 +- .../test_materialized_view_integration.py | 2 +- .../backend/postgres/test_pg19_copy_repack.py | 6 +- .../test_pg19_copy_repack_integration.py | 2 +- .../feature/backend/postgres/test_policy.py | 2 +- .../test_postgres_column_definition.py | 2 +- .../test_qualified_reference_context.py | 6 +- .../backend/postgres/test_type_casting.py | 2 +- .../feature/backend/test_backend_explain.py | 2 +- .../backend/test_backend_initialization.py | 5 +- .../backend/test_connection_resilience.py | 5 +- .../feature/backend/test_partition_explain.py | 2 +- ...gres_datetime_interval_explain_examples.py | 2 +- .../feature/backend/test_version.py | 5 +- .../feature/connection/conftest.py | 5 +- .../collation/test_collation_expression.py | 3 +- 221 files changed, 438 insertions(+), 1368 deletions(-) delete mode 100644 src/rhosocial/activerecord/backend/impl/postgres/__init__.py diff --git a/.claude/architecture.md b/.claude/architecture.md index 32b4ebd3..bb797c4c 100644 --- a/.claude/architecture.md +++ b/.claude/architecture.md @@ -546,7 +546,7 @@ StorageBackendBase (ABC) ```python # 1. User configures model from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend +from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend class User(ActiveRecord): __table_name__ = "users" @@ -616,7 +616,7 @@ def discover_backends(): # Check for installed backends (each uses only native drivers) try: - from rhosocial.activerecord.backend.impl.mysql import MySQLBackend + from rhosocial.activerecord.backend.impl.mysql.backend import MySQLBackend backends['mysql'] = MySQLBackend # Uses mysql-connector-python directly except ImportError: pass diff --git a/.claude/commands/gen-model.md b/.claude/commands/gen-model.md index 87631693..25a8b692 100644 --- a/.claude/commands/gen-model.md +++ b/.claude/commands/gen-model.md @@ -58,7 +58,7 @@ CREATE TABLE users ( 2. Show the SQL migration to create the table 3. Remind the user to configure the backend: ```python - from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend + from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend User.configure(SQLiteBackend("sqlite:///app.db")) ``` diff --git a/.claude/commands/validate-model.md b/.claude/commands/validate-model.md index 5e95c977..e7266c89 100644 --- a/.claude/commands/validate-model.md +++ b/.claude/commands/validate-model.md @@ -66,7 +66,7 @@ user = User(name="John") # Will raise "No backend configured" user.save() # CORRECT -from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend +from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend User.configure(SQLiteBackend("sqlite:///app.db")) user = User(name="John") user.save() diff --git a/README.md b/README.md index 5fa4d1e2..5b66df6e 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ pip install rhosocial-activerecord-postgres ```python from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from typing import Optional from uuid import UUID diff --git a/docs/en_US/ddl/materialized_view.md b/docs/en_US/ddl/materialized_view.md index 4da9e612..6ad5253f 100644 --- a/docs/en_US/ddl/materialized_view.md +++ b/docs/en_US/ddl/materialized_view.md @@ -25,7 +25,7 @@ usable; they simply do not carry `schema` / `if_not_exists`. ```python from rhosocial.activerecord.backend.expression import Column, FunctionCall, QueryExpression, TableExpression -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateMaterializedViewExpression, ) @@ -86,7 +86,7 @@ standard heap relation options, enumerated by [`PostgresStorageParameter`](../../api/storage_parameters.md): ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter PostgresStorageParameter.FILLFACTOR.value # 'fillfactor' PostgresStorageParameter.FILLFACTOR.value_type # PostgresStorageParameterValueType.INT diff --git a/docs/en_US/installation_and_configuration/README.md b/docs/en_US/installation_and_configuration/README.md index 47d47b87..a6d4d34a 100644 --- a/docs/en_US/installation_and_configuration/README.md +++ b/docs/en_US/installation_and_configuration/README.md @@ -17,10 +17,8 @@ pip install rhosocial-activerecord-postgres ``` ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresBackend, - PostgresConnectionConfig -) +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host="localhost", diff --git a/docs/en_US/installation_and_configuration/backend_group.md b/docs/en_US/installation_and_configuration/backend_group.md index 5fa0b2f3..4c05b9c0 100644 --- a/docs/en_US/installation_and_configuration/backend_group.md +++ b/docs/en_US/installation_and_configuration/backend_group.md @@ -6,7 +6,8 @@ This document describes how to use `BackendGroup` and `BackendManager` with the ```python from rhosocial.activerecord.connection import BackendGroup -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.model import ActiveRecord diff --git a/docs/en_US/installation_and_configuration/configuration.md b/docs/en_US/installation_and_configuration/configuration.md index cf1c95c7..8404d30c 100644 --- a/docs/en_US/installation_and_configuration/configuration.md +++ b/docs/en_US/installation_and_configuration/configuration.md @@ -3,7 +3,7 @@ ## Basic Configuration ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host="localhost", @@ -49,7 +49,7 @@ For security, use environment variables: ```python import os -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host=os.getenv("PG_HOST", "localhost"), diff --git a/docs/en_US/installation_and_configuration/installation.md b/docs/en_US/installation_and_configuration/installation.md index 2e4cd4d9..5835a6f9 100644 --- a/docs/en_US/installation_and_configuration/installation.md +++ b/docs/en_US/installation_and_configuration/installation.md @@ -27,7 +27,7 @@ pip install rhosocial-activerecord-postgres[test,dev,docs] ## Verifying Installation ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend print("PostgreSQL backend installed successfully!") ``` diff --git a/docs/en_US/installation_and_configuration/pool.md b/docs/en_US/installation_and_configuration/pool.md index e88356db..e0082fa3 100644 --- a/docs/en_US/installation_and_configuration/pool.md +++ b/docs/en_US/installation_and_configuration/pool.md @@ -5,7 +5,7 @@ The PostgreSQL backend uses a "connect-on-use" pattern by default: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend(connection_config=config) # Connection is NOT established yet @@ -55,7 +55,7 @@ pip install rhosocial-activerecord-postgres[pooling] ``` ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend config = PostgresConnectionConfig( # ... basic config ... diff --git a/docs/en_US/introduction/relationship.md b/docs/en_US/introduction/relationship.md index 82795e8b..f4595946 100644 --- a/docs/en_US/introduction/relationship.md +++ b/docs/en_US/introduction/relationship.md @@ -24,7 +24,7 @@ rhosocial-activerecord-postgres (Backend) The PostgreSQL backend registers itself with the core library through the standard backend interface: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend # Configure model with PostgreSQL backend User.configure( diff --git a/docs/en_US/plugins/README.md b/docs/en_US/plugins/README.md index 50d0e137..87f523d3 100644 --- a/docs/en_US/plugins/README.md +++ b/docs/en_US/plugins/README.md @@ -17,7 +17,7 @@ The backend automatically detects server capabilities and installed extensions v ### Basic Usage ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend( host='localhost', diff --git a/docs/en_US/postgres_specific_features/dialect.md b/docs/en_US/postgres_specific_features/dialect.md index ccd64547..68279df6 100644 --- a/docs/en_US/postgres_specific_features/dialect.md +++ b/docs/en_US/postgres_specific_features/dialect.md @@ -8,7 +8,7 @@ PostgreSQL uses the `::` operator for type casting, which is PostgreSQL-specific ```python from rhosocial.activerecord.backend.expression import Column -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.type_values.constants import INTEGER, NUMERIC, MONEY dialect = PostgresDialect() @@ -32,7 +32,7 @@ PostgreSQL supports chained type conversions: ```python from rhosocial.activerecord.backend.expression import Column -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.type_values.constants import MONEY, NUMERIC, FLOAT8 dialect = PostgresDialect() diff --git a/docs/en_US/postgres_specific_features/introspection.md b/docs/en_US/postgres_specific_features/introspection.md index 9ed2bc3b..11c63e57 100644 --- a/docs/en_US/postgres_specific_features/introspection.md +++ b/docs/en_US/postgres_specific_features/introspection.md @@ -19,9 +19,8 @@ The PostgreSQL introspection system is accessible via `backend.introspector` and ### Accessing the Introspector ```python -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend - -backend = PostgreSQLBackend( +rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +backend = PostgresBackend( host="localhost", port=5432, database="mydb", @@ -254,9 +253,8 @@ for idx in indexes: The async backend provides identical introspection methods with the same names as the sync version: ```python -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgreSQLBackend - -backend = AsyncPostgreSQLBackend( +rhosocial.activerecord.backend.impl.postgres.async_backend import AsyncPostgresBackend +backend = AsyncPostgresBackend( host="localhost", port=5432, database="mydb", diff --git a/docs/en_US/postgres_specific_features/property_graph_query.md b/docs/en_US/postgres_specific_features/property_graph_query.md index 4d46e690..37412a3b 100644 --- a/docs/en_US/postgres_specific_features/property_graph_query.md +++ b/docs/en_US/postgres_specific_features/property_graph_query.md @@ -35,7 +35,7 @@ feature. ## Capability Detection ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect dialect = PostgresDialect(version=(19, 0, 4)) assert dialect.supports_graph_match() is False diff --git a/docs/en_US/postgres_specific_features/protocol_support.md b/docs/en_US/postgres_specific_features/protocol_support.md index d1286a35..7798dfdd 100644 --- a/docs/en_US/postgres_specific_features/protocol_support.md +++ b/docs/en_US/postgres_specific_features/protocol_support.md @@ -60,7 +60,7 @@ PostgreSQL provides comprehensive support for SQL set operations: **Example:** ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect dialect = PostgresDialect() assert dialect.supports_union() # True @@ -86,7 +86,7 @@ PostgreSQL supports TRUNCATE TABLE with several database-specific features: **Example:** ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.expression.statements import TruncateExpression # Basic truncate @@ -521,7 +521,7 @@ After calling `introspect_and_adapt()`, the following is cached: The framework provides a convenient method for version-aware feature detection: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend # Connect and introspect (one-time) backend = PostgresBackend(...) @@ -661,7 +661,7 @@ backend.introspect_and_adapt() ### Checking Protocol Support ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.dialect.protocols import ( SetOperationSupport, TruncateSupport, CTESupport ) @@ -683,7 +683,7 @@ assert dialect.supports_merge_statement() # False (requires ≥ 15) ### Version-Specific Feature Detection ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect # Old PostgreSQL version old_dialect = PostgresDialect(version=(8, 3, 0)) diff --git a/docs/en_US/scenarios/parallel_workers.md b/docs/en_US/scenarios/parallel_workers.md index cfe2f873..1400c991 100644 --- a/docs/en_US/scenarios/parallel_workers.md +++ b/docs/en_US/scenarios/parallel_workers.md @@ -22,8 +22,8 @@ In scenarios such as data processing, task queues, and batch imports, developers `rhosocial-activerecord` follows the core design principle of **one ActiveRecord class bound to one connection**: -- **Sync**: `Post.configure(config, PostgreSQLBackend)` → writes to `Post.__backend__` -- **Async**: `await Post.configure(config, AsyncPostgreSQLBackend)` → writes to `Post.__backend__` +- **Sync**: `Post.configure(config, PostgresBackend)` → writes to `Post.__backend__` +- **Async**: `await Post.configure(config, AsyncPostgresBackend)` → writes to `Post.__backend__` The configuration writes to a **class-level attribute**, which is still unsafe for multi-threading. PostgreSQL differs from other databases in the following ways: @@ -59,19 +59,20 @@ Multi-processing is the recommended approach for parallel worker scenarios. Each ```python import multiprocessing -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from models import Comment, Post, User def worker(post_ids: list[int]): # 1. After process starts, configure connection within process - each process establishes independent TCP connection - config = PostgreSQLConnectionConfig( + config = PostgresConnectionConfig( host="localhost", port=5432, database="mydb", username="app", password="secret", ) - User.configure(config, PostgreSQLBackend) + User.configure(config, PostgresBackend) Post.__backend__ = User.backend() Comment.__backend__ = User.backend() @@ -103,16 +104,17 @@ if __name__ == "__main__": ```python import asyncio import multiprocessing -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgreSQLBackend, PostgreSQLConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from models import AsyncComment, AsyncPost, AsyncUser async def async_worker_main(post_ids: list[int]): # 1. Configure async connection within process (psycopg native async, TCP network I/O) - config = PostgreSQLConnectionConfig( + config = PostgresConnectionConfig( host="localhost", port=5432, database="mydb", username="app", password="secret", ) - await AsyncUser.configure(config, AsyncPostgreSQLBackend) + await AsyncUser.configure(config, AsyncPostgresBackend) AsyncPost.__backend__ = AsyncUser.backend() AsyncComment.__backend__ = AsyncUser.backend() @@ -161,7 +163,7 @@ Runnable timing comparison demo at [exp1_basic_multiprocess.py](../../examples/c ### 3.1 Single Connection Model Constraints -`rhosocial-activerecord`'s async PostgreSQL backend (`AsyncPostgreSQLBackend`) is based on `psycopg` version 3's async interface, **each ActiveRecord class binds to one connection**. This differs from connection pool solutions: +`rhosocial-activerecord`'s async PostgreSQL backend (`AsyncPostgresBackend`) is based on `psycopg` version 3's async interface, **each ActiveRecord class binds to one connection**. This differs from connection pool solutions: | Feature | Single Connection ORM (This Project) | Connection Pool Solution | | --- | --- | --- | @@ -397,7 +399,7 @@ When a system contains two distinctly different workloads, it is recommended to | Workload Type | Characteristics | Suitable Deployment | | --- | --- | --- | | Web API Service | Short requests, high concurrency, response time sensitive | FastAPI / Django + asyncio + connection pool | -| Data Analysis Batch Processing | Long running, large data volume, CPU intensive | Standalone script + multiprocessing + PostgreSQLBackend | +| Data Analysis Batch Processing | Long running, large data volume, CPU intensive | Standalone script + multiprocessing + PostgresBackend | | Task Queue Consumer | Regular polling, independent tasks, easy horizontal scaling | Standalone worker process pool | ### 5.2 Recommended Architecture @@ -408,7 +410,7 @@ User requests ──→ Web application (asyncio + connection pool) └──→ Task queue (PostgreSQL database table / Redis) │ └──→ Background worker process pool - (Each process independent PostgreSQLBackend sync connection) + (Each process independent PostgresBackend sync connection) ``` Web application receives requests and writes to task queue; worker process pool executes time-consuming tasks. @@ -518,7 +520,7 @@ async def async_worker_task(user_id, conn_params): async def async_worker_task(user_id, conn_params): # Only pass connection parameters (serializable), create new instance in child process config = conn_params['config_kwargs'] - await Model.configure(config, AsyncPostgreSQLBackend) + await Model.configure(config, AsyncPostgresBackend) user = await Model.find_one(user_id) await Model.backend().disconnect() ``` @@ -550,8 +552,8 @@ Sync test coverage is higher; async test coverage has uncovered code paths due t ### 8.3 Production Recommendations -1. **Sync Workers Preferred**: In multi-process Worker scenarios, sync backend (`PostgreSQLBackend`) is the more stable choice -2. **Async for Single Process**: Async backend (`AsyncPostgreSQLBackend`) works well in single-process sequential execution scenarios +1. **Sync Workers Preferred**: In multi-process Worker scenarios, sync backend (`PostgresBackend`) is the more stable choice +2. **Async for Single Process**: Async backend (`AsyncPostgresBackend`) works well in single-process sequential execution scenarios 3. **Avoid Cross-Process Async Instance Passing**: Only pass serializable connection parameters, create new async backend instances in child processes ### 8.4 FOR UPDATE Capability Detection Experience @@ -751,7 +753,7 @@ In FastAPI + PostgreSQL + async backend scenario: ```python # database.py - Connection pool manager from rhosocial.activerecord.connection.pool import PoolConfig, AsyncBackendPool -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend # Global connection pool (created at application startup) _pool: AsyncBackendPool = None diff --git a/docs/en_US/testing/configuration.md b/docs/en_US/testing/configuration.md index 686f9c17..a5dbd6e8 100644 --- a/docs/en_US/testing/configuration.md +++ b/docs/en_US/testing/configuration.md @@ -10,9 +10,8 @@ The `dummy` backend is recommended for unit tests as it does not require a real ```python from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend.impl.dummy import DummyBackend, DummyConnectionConfig - - +from rhosocial.activerecord.backend.impl.dummy.backend import DummyBackend +rhosocial.activerecord.backend.config import ConnectionConfig class User(ActiveRecord): name: str email: str @@ -25,7 +24,7 @@ class User(ActiveRecord): # Configure Dummy backend -config = DummyConnectionConfig() +config = ConnectionConfig() User.configure(config, DummyBackend) ``` @@ -34,7 +33,8 @@ User.configure(config, DummyBackend) For tests requiring real database behavior, use the SQLite backend: ```python -from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend, SQLiteConnectionConfig +from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend +from rhosocial.activerecord.backend.impl.sqlite.config import SQLiteConnectionConfig class User(ActiveRecord): @@ -59,9 +59,8 @@ For complete PostgreSQL behavior testing, use the PostgreSQL backend: ```python import os -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - - +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig class User(ActiveRecord): name: str email: str @@ -74,26 +73,25 @@ class User(ActiveRecord): # Read configuration from environment variables -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host=os.environ.get('PG_HOST', 'localhost'), port=int(os.environ.get('PG_PORT', 5432)), database=os.environ.get('PG_DATABASE', 'test'), username=os.environ.get('PG_USER', 'postgres'), password=os.environ.get('PG_PASSWORD', ''), ) -User.configure(config, PostgreSQLBackend) +User.configure(config, PostgresBackend) ``` ## Test Fixtures ```python import pytest -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - - +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig @pytest.fixture def postgres_config(): - return PostgreSQLConnectionConfig( + return PostgresConnectionConfig( host='localhost', port=5432, database='test', @@ -104,7 +102,7 @@ def postgres_config(): @pytest.fixture def postgres_backend(postgres_config): - backend = PostgreSQLBackend(connection_config=postgres_config) + backend = PostgresBackend(connection_config=postgres_config) backend.connect() yield backend backend.disconnect() diff --git a/docs/en_US/transaction_support/README.md b/docs/en_US/transaction_support/README.md index 23b9ee02..76e07752 100644 --- a/docs/en_US/transaction_support/README.md +++ b/docs/en_US/transaction_support/README.md @@ -12,7 +12,7 @@ PostgreSQL provides robust transaction support with advanced features like savep ## Quick Start ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend(connection_config=config) tm = backend.transaction_manager diff --git a/docs/en_US/transaction_support/deferrable.md b/docs/en_US/transaction_support/deferrable.md index 6fe60d0b..b31ab365 100644 --- a/docs/en_US/transaction_support/deferrable.md +++ b/docs/en_US/transaction_support/deferrable.md @@ -9,7 +9,7 @@ Deferrable constraints are checked at transaction commit time, not at statement ## Usage ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.transaction import IsolationLevel backend = PostgresBackend(connection_config=config) diff --git a/docs/en_US/transaction_support/isolation_level.md b/docs/en_US/transaction_support/isolation_level.md index e35dda8f..7c95ee41 100644 --- a/docs/en_US/transaction_support/isolation_level.md +++ b/docs/en_US/transaction_support/isolation_level.md @@ -11,7 +11,7 @@ ## Setting Isolation Level ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.transaction import IsolationLevel backend = PostgresBackend(connection_config=config) diff --git a/docs/en_US/troubleshooting/connection.md b/docs/en_US/troubleshooting/connection.md index 7de866e0..2b50058a 100644 --- a/docs/en_US/troubleshooting/connection.md +++ b/docs/en_US/troubleshooting/connection.md @@ -62,7 +62,7 @@ psycopg.OperationalError: connection timeout expired ### Solutions ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='remote.host.com', connect_timeout=30, # Increase timeout ) @@ -82,7 +82,7 @@ psycopg.OperationalError: SSL connection failed ### Solutions ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='remote.host.com', sslmode='require', # Options: disable, prefer, require, verify-ca, verify-full sslrootcert='/path/to/ca.crt', # For verify-ca or verify-full @@ -256,7 +256,7 @@ SET GLOBAL idle_in_transaction_session_timeout = '10min'; For high-concurrency scenarios, configure connection pooling: ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='localhost', database='mydb', # Connection pool settings diff --git a/docs/en_US/troubleshooting/performance.md b/docs/en_US/troubleshooting/performance.md index 23dcba5f..05f65218 100644 --- a/docs/en_US/troubleshooting/performance.md +++ b/docs/en_US/troubleshooting/performance.md @@ -20,10 +20,10 @@ SELECT pg_reload_conf(); ### Using EXPLAIN ANALYZE ```python -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - -backend = PostgreSQLBackend( - connection_config=PostgreSQLConnectionConfig( +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +backend = PostgresBackend( + connection_config=PostgresConnectionConfig( host='localhost', database='myapp', username='user', @@ -87,7 +87,7 @@ VACUUM ANALYZE users; For high-concurrency applications, use connection pooling: ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='localhost', database='myapp', username='user', diff --git a/docs/en_US/type_adapters/README.md b/docs/en_US/type_adapters/README.md index 7a5bf990..e09361ee 100644 --- a/docs/en_US/type_adapters/README.md +++ b/docs/en_US/type_adapters/README.md @@ -107,8 +107,10 @@ class User(ActiveRecord): For PostgreSQL-specific types with registered adapters, use the corresponding wrapper classes: ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresMoney, Point, PostgresTsVector +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresMoney, + Point, + PostgresTsVector, ) class Order(ActiveRecord): @@ -136,9 +138,8 @@ backend.execute( **Method 2: Use PostgresRange wrapper (recommended for ActiveRecord models)** ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresRange, PostgresRangeAdapter -) +from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresRange +from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresRangeAdapter # Manually register the adapter backend.adapter_registry.register(PostgresRangeAdapter(), PostgresRange, str) @@ -152,10 +153,15 @@ class Order(ActiveRecord): For types with str→str mapping conflicts, you must explicitly register the adapter: ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresXML, PostgresXMLAdapter, - PostgresBitString, PostgresBitStringAdapter, - PostgresJsonPath, PostgresJsonPathAdapter +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresXML, + PostgresBitString, + PostgresJsonPath, +) +from rhosocial.activerecord.backend.impl.postgres.adapters import ( + PostgresXMLAdapter, + PostgresBitStringAdapter, + PostgresJsonPathAdapter, ) # Method 1: Global registration on backend @@ -175,9 +181,13 @@ result = backend.execute( ```python from rhosocial.activerecord import ActiveRecord -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresXML, PostgresXMLAdapter, - PostgresBitString, PostgresBitStringAdapter +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresXML, + PostgresBitString, +) +from rhosocial.activerecord.backend.impl.postgres.adapters import ( + PostgresXMLAdapter, + PostgresBitStringAdapter, ) class Document(ActiveRecord): diff --git a/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py b/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py index d228055c..8d8513f3 100644 --- a/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py +++ b/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py @@ -40,7 +40,7 @@ "python-activerecord-postgres", "src")) from rhosocial.activerecord.connection.pool import PoolConfig, AsyncBackendPool -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_sync.py b/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_sync.py index 076f25c0..ea3104ea 100644 --- a/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_sync.py +++ b/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_sync.py @@ -38,7 +38,7 @@ "python-activerecord-postgres", "src")) from rhosocial.activerecord.connection.pool import PoolConfig, BackendPool -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/docs/examples/chapter_04_ddl/ddl.py b/docs/examples/chapter_04_ddl/ddl.py index 7e0d6d2b..c446a1f8 100644 --- a/docs/examples/chapter_04_ddl/ddl.py +++ b/docs/examples/chapter_04_ddl/ddl.py @@ -10,7 +10,7 @@ from typing import ClassVar from rhosocial.activerecord.model import ActiveRecord from rhosocial.activerecord.base import FieldProxy -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.expression import ( ColumnDefinition, CreateTableExpression, @@ -39,7 +39,7 @@ def table_name(cls) -> str: def main(): # Configure with PostgreSQL backend - from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig + from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host="db-dev-1-n.rho.im", diff --git a/docs/examples/chapter_05_worker_isolation/config.py b/docs/examples/chapter_05_worker_isolation/config.py index 29a01103..a3472d42 100644 --- a/docs/examples/chapter_05_worker_isolation/config.py +++ b/docs/examples/chapter_05_worker_isolation/config.py @@ -7,7 +7,7 @@ import yaml from typing import Optional # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig diff --git a/docs/examples/chapter_05_worker_isolation/exp1_shared_connection.py b/docs/examples/chapter_05_worker_isolation/exp1_shared_connection.py index a26a556a..90a11666 100644 --- a/docs/examples/chapter_05_worker_isolation/exp1_shared_connection.py +++ b/docs/examples/chapter_05_worker_isolation/exp1_shared_connection.py @@ -57,7 +57,7 @@ def worker_task_shared_connection(worker_id: int, num_operations: int, config_di WARNING: This will cause problems! """ - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig for model in ALL_MODELS: @@ -161,7 +161,7 @@ def run_experiment(config_dict: Dict[str, Any], num_workers: int, ops_per_worker # Configure globally ONCE (this is the anti-pattern) print("Step 1: Configuring ActiveRecord globally (shared connection)...") - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig(**config_dict) diff --git a/docs/examples/chapter_05_worker_isolation/exp2_isolated_connection.py b/docs/examples/chapter_05_worker_isolation/exp2_isolated_connection.py index a7967284..ef5200b1 100644 --- a/docs/examples/chapter_05_worker_isolation/exp2_isolated_connection.py +++ b/docs/examples/chapter_05_worker_isolation/exp2_isolated_connection.py @@ -79,7 +79,7 @@ def worker_task_isolated_connection( This is the correct approach for process-based parallelism. """ - from rhosocial.activerecord.backend.impl.mysql import MySQLBackend + from rhosocial.activerecord.backend.impl.mysql.backend import MySQLBackend from rhosocial.activerecord.backend.impl.mysql.config import MySQLConnectionConfig from models import User, Order, Post, Comment, ALL_MODELS # noqa: F401 @@ -211,7 +211,7 @@ def run_experiment(config_dict: Dict[str, Any], num_workers: int, ops_per_worker # Setup initial database state (in main process) print("Step 1: Setting up database...") - from rhosocial.activerecord.backend.impl.mysql import MySQLBackend + from rhosocial.activerecord.backend.impl.mysql.backend import MySQLBackend from rhosocial.activerecord.backend.impl.mysql.config import MySQLConnectionConfig from models import User, ALL_MODELS from config import setup_database diff --git a/docs/examples/chapter_05_worker_isolation/exp3_endurance_test.py b/docs/examples/chapter_05_worker_isolation/exp3_endurance_test.py index bb89f5af..a6b0ada6 100644 --- a/docs/examples/chapter_05_worker_isolation/exp3_endurance_test.py +++ b/docs/examples/chapter_05_worker_isolation/exp3_endurance_test.py @@ -93,7 +93,7 @@ def worker_task( use_isolated: If True, use isolated connection (correct pattern) If False, use shared connection (anti-pattern) """ - from rhosocial.activerecord.backend.impl.mysql import MySQLBackend + from rhosocial.activerecord.backend.impl.mysql.backend import MySQLBackend from rhosocial.activerecord.backend.impl.mysql.config import MySQLConnectionConfig from models import User, Order, Post, Comment, ALL_MODELS # noqa: F401 @@ -234,7 +234,7 @@ def run_endurance_test( stats = ExperimentStats() # Setup database - from rhosocial.activerecord.backend.impl.mysql import MySQLBackend + from rhosocial.activerecord.backend.impl.mysql.backend import MySQLBackend from rhosocial.activerecord.backend.impl.mysql.config import MySQLConnectionConfig from models import User, ALL_MODELS from config import setup_database diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/config_loader.py b/docs/examples/chapter_08_scenarios/parallel_workers/config_loader.py index 5748eeee..7f7d53ba 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/config_loader.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/config_loader.py @@ -8,7 +8,7 @@ 3. Hardcoded defaults (localhost:5432/test_db/postgres/) Public interface: - load_config(scenario=None) → PostgreSQLConnectionConfig + load_config(scenario=None) → PostgresConnectionConfig list_scenarios() → list[str] """ @@ -24,7 +24,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig # noqa: E402 +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig # ─── Default preferred scenarios (by priority) ───────────────────────────────────── @@ -52,7 +52,7 @@ def load_config(scenario: Optional[str] = None) -> PostgresConnectionConfig: scenario: Scenario name in YAML (e.g., "postgres_16"). If None, auto-select by priority. Returns: - PostgreSQLConnectionConfig instance. + PostgresConnectionConfig instance. """ yaml_path = _find_scenarios_yaml() if yaml_path: diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py index a89b3e52..94c40c69 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py @@ -38,7 +38,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import ( # noqa: E402 +from rhosocial.activerecord.backend.impl.postgres.backend import ( AsyncPostgresBackend, PostgresBackend, ) diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py index c5beca4b..45176458 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py @@ -39,7 +39,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import ( # noqa: E402 +from rhosocial.activerecord.backend.impl.postgres.backend import ( AsyncPostgresBackend, PostgresBackend, ) diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp3_deadlock_wrong.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp3_deadlock_wrong.py index bcdb6660..341f0ad9 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp3_deadlock_wrong.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp3_deadlock_wrong.py @@ -36,7 +36,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend # noqa: E402 +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config # noqa: E402 from models import Post # noqa: E402 diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py index 872e4afc..3dead448 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py @@ -38,7 +38,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import ( # noqa: E402 +from rhosocial.activerecord.backend.impl.postgres.backend import ( AsyncPostgresBackend, PostgresBackend, ) diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp5_multithread_warning.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp5_multithread_warning.py index 25f82b2a..d30ac302 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp5_multithread_warning.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp5_multithread_warning.py @@ -41,7 +41,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend # noqa: E402 +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config # noqa: E402 from models import Post # noqa: E402 diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp6_backend_group_pool.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp6_backend_group_pool.py index 3b19ffb7..f6bf471f 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp6_backend_group_pool.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp6_backend_group_pool.py @@ -32,7 +32,8 @@ from rhosocial.activerecord.connection import BackendGroup from rhosocial.activerecord.connection.pool import PoolConfig, BackendPool -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, PostgresConnectionConfig # noqa: F401 +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from config_loader import load_config from models import Comment, Post, User diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py b/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py index 52b02048..61b0f469 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py @@ -24,7 +24,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import ( # noqa: E402 +from rhosocial.activerecord.backend.impl.postgres.backend import ( AsyncPostgresBackend, PostgresBackend, ) diff --git a/docs/examples/chapter_10_fastapi/config_loader.py b/docs/examples/chapter_10_fastapi/config_loader.py index 8b2b3ada..4a8b2f66 100644 --- a/docs/examples/chapter_10_fastapi/config_loader.py +++ b/docs/examples/chapter_10_fastapi/config_loader.py @@ -10,7 +10,7 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig def load_config() -> PostgresConnectionConfig: diff --git a/docs/examples/chapter_10_fastapi/database.py b/docs/examples/chapter_10_fastapi/database.py index bec2a2a4..4eeb153d 100644 --- a/docs/examples/chapter_10_fastapi/database.py +++ b/docs/examples/chapter_10_fastapi/database.py @@ -28,7 +28,7 @@ from fastapi import Request # noqa: F401 from rhosocial.activerecord.connection.pool import PoolConfig, AsyncBackendPool -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend from config_loader import load_config from models import AsyncUser, AsyncPost, AsyncComment diff --git a/docs/zh_CN/ddl/materialized_view.md b/docs/zh_CN/ddl/materialized_view.md index a8ef20ce..100fd1c1 100644 --- a/docs/zh_CN/ddl/materialized_view.md +++ b/docs/zh_CN/ddl/materialized_view.md @@ -24,7 +24,7 @@ ```python from rhosocial.activerecord.backend.expression import Column, FunctionCall, QueryExpression, TableExpression -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateMaterializedViewExpression, ) @@ -83,7 +83,7 @@ create = PostgresCreateMaterializedViewExpression( 关系选项,已由 [`PostgresStorageParameter`](../../api/storage_parameters.md) 枚举: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter PostgresStorageParameter.FILLFACTOR.value # 'fillfactor' PostgresStorageParameter.FILLFACTOR.value_type # PostgresStorageParameterValueType.INT diff --git a/docs/zh_CN/installation_and_configuration/README.md b/docs/zh_CN/installation_and_configuration/README.md index 26d57ec6..10089e56 100644 --- a/docs/zh_CN/installation_and_configuration/README.md +++ b/docs/zh_CN/installation_and_configuration/README.md @@ -17,10 +17,8 @@ pip install rhosocial-activerecord-postgres ``` ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresBackend, - PostgresConnectionConfig -) +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host="localhost", diff --git a/docs/zh_CN/installation_and_configuration/configuration.md b/docs/zh_CN/installation_and_configuration/configuration.md index 1fa02337..ad411bee 100644 --- a/docs/zh_CN/installation_and_configuration/configuration.md +++ b/docs/zh_CN/installation_and_configuration/configuration.md @@ -3,7 +3,7 @@ ## 基础配置 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host="localhost", @@ -49,7 +49,7 @@ config = PostgresConnectionConfig( ```python import os -from rhosocial.activerecord.backend.impl.postgres import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host=os.getenv("PG_HOST", "localhost"), diff --git a/docs/zh_CN/installation_and_configuration/installation.md b/docs/zh_CN/installation_and_configuration/installation.md index ee416540..30937f08 100644 --- a/docs/zh_CN/installation_and_configuration/installation.md +++ b/docs/zh_CN/installation_and_configuration/installation.md @@ -27,7 +27,7 @@ pip install rhosocial-activerecord-postgres[test,dev,docs] ## 验证安装 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend print("PostgreSQL 后端安装成功!") ``` diff --git a/docs/zh_CN/installation_and_configuration/pool.md b/docs/zh_CN/installation_and_configuration/pool.md index 09987579..1915033a 100644 --- a/docs/zh_CN/installation_and_configuration/pool.md +++ b/docs/zh_CN/installation_and_configuration/pool.md @@ -5,7 +5,7 @@ PostgreSQL 后端默认使用"随用随连"模式: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend(connection_config=config) # 此时连接尚未建立 @@ -55,7 +55,7 @@ pip install rhosocial-activerecord-postgres[pooling] ``` ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend config = PostgresConnectionConfig( # ... 基础配置 ... diff --git a/docs/zh_CN/introduction/relationship.md b/docs/zh_CN/introduction/relationship.md index 84c1d667..6fdcee91 100644 --- a/docs/zh_CN/introduction/relationship.md +++ b/docs/zh_CN/introduction/relationship.md @@ -93,13 +93,10 @@ class User(UUIDMixin, TimestampMixin, ActiveRecord): ### 3. 配置后端 ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresBackend, - PostgreSQLConnectionConfig, -) - +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig # 配置 PostgreSQL 连接 -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='localhost', port=5432, database='myapp', diff --git a/docs/zh_CN/plugins/README.md b/docs/zh_CN/plugins/README.md index bd1b9c3d..902e2b71 100644 --- a/docs/zh_CN/plugins/README.md +++ b/docs/zh_CN/plugins/README.md @@ -17,7 +17,7 @@ PostgreSQL 扩展系统允许安装额外的功能模块,如: ### 基本用法 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend( host='localhost', diff --git a/docs/zh_CN/postgres_specific_features/dialect.md b/docs/zh_CN/postgres_specific_features/dialect.md index 4a1c8d92..297e41e0 100644 --- a/docs/zh_CN/postgres_specific_features/dialect.md +++ b/docs/zh_CN/postgres_specific_features/dialect.md @@ -8,7 +8,7 @@ PostgreSQL 使用 `::` 操作符进行类型转换,这是 PostgreSQL 特有的 ```python from rhosocial.activerecord.backend.expression import Column -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.type_values.constants import INTEGER, NUMERIC, MONEY dialect = PostgresDialect() @@ -32,7 +32,7 @@ PostgreSQL 支持链式类型转换: ```python from rhosocial.activerecord.backend.expression import Column -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.type_values.constants import MONEY, NUMERIC, FLOAT8 dialect = PostgresDialect() diff --git a/docs/zh_CN/postgres_specific_features/introspection.md b/docs/zh_CN/postgres_specific_features/introspection.md index 8b630fef..5ebf771b 100644 --- a/docs/zh_CN/postgres_specific_features/introspection.md +++ b/docs/zh_CN/postgres_specific_features/introspection.md @@ -19,9 +19,8 @@ PostgreSQL 内省系统位于 `backend.introspector` 属性中,提供: ### 访问内省器 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend - -backend = PostgreSQLBackend( +rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +backend = PostgresBackend( host="localhost", port=5432, database="mydb", @@ -254,9 +253,8 @@ for idx in indexes: 异步后端提供相同的内省方法,方法名与同步版本相同: ```python -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgreSQLBackend - -backend = AsyncPostgreSQLBackend( +rhosocial.activerecord.backend.impl.postgres.async_backend import AsyncPostgresBackend +backend = AsyncPostgresBackend( host="localhost", port=5432, database="mydb", diff --git a/docs/zh_CN/postgres_specific_features/property_graph_query.md b/docs/zh_CN/postgres_specific_features/property_graph_query.md index d88c5e8a..8165079c 100644 --- a/docs/zh_CN/postgres_specific_features/property_graph_query.md +++ b/docs/zh_CN/postgres_specific_features/property_graph_query.md @@ -30,7 +30,7 @@ PostgreSQL 19 Beta 4 在 19.0 正式发布前撤回了 SQL/PGQ,更早的版本 ## 能力检测 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect dialect = PostgresDialect(version=(19, 0, 4)) assert dialect.supports_graph_match() is False diff --git a/docs/zh_CN/postgres_specific_features/protocol_support.md b/docs/zh_CN/postgres_specific_features/protocol_support.md index 2aa95804..8d24edc5 100644 --- a/docs/zh_CN/postgres_specific_features/protocol_support.md +++ b/docs/zh_CN/postgres_specific_features/protocol_support.md @@ -377,7 +377,7 @@ dialect.supports_postgis_spatial_functions() # 使用缓存的扩展状态 框架提供了便捷的方法进行版本感知的特性检测: ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend # 连接并内省(一次性) backend = PostgresBackend(...) @@ -517,7 +517,7 @@ backend.introspect_and_adapt() ### 检查协议支持 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.dialect.protocols import ( SetOperationSupport, TruncateSupport, CTESupport ) @@ -539,7 +539,7 @@ assert dialect.supports_merge_statement() # False(需要 ≥ 15) ### 版本特定特性检测 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect # 旧版本 PostgreSQL old_dialect = PostgresDialect(version=(8, 3, 0)) diff --git a/docs/zh_CN/scenarios/parallel_workers.md b/docs/zh_CN/scenarios/parallel_workers.md index 2301406f..8862b3cf 100644 --- a/docs/zh_CN/scenarios/parallel_workers.md +++ b/docs/zh_CN/scenarios/parallel_workers.md @@ -22,8 +22,8 @@ `rhosocial-activerecord` 同样遵循**一个 ActiveRecord 类,绑定一条连接**的核心设计原则: -- **同步**:`Post.configure(config, PostgreSQLBackend)` → 写入 `Post.__backend__` -- **异步**:`await Post.configure(config, AsyncPostgreSQLBackend)` → 写入 `Post.__backend__` +- **同步**:`Post.configure(config, PostgresBackend)` → 写入 `Post.__backend__` +- **异步**:`await Post.configure(config, AsyncPostgresBackend)` → 写入 `Post.__backend__` 配置写入的是**类级别属性**,多线程间仍然不安全。PostgreSQL 在以下方面与其他数据库有差异: @@ -59,19 +59,20 @@ ```python import multiprocessing -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from models import Comment, Post, User def worker(post_ids: list[int]): # 1. 进程启动后,在进程内配置连接——每个进程建立独立 TCP 连接 - config = PostgreSQLConnectionConfig( + config = PostgresConnectionConfig( host="localhost", port=5432, database="mydb", username="app", password="secret", ) - User.configure(config, PostgreSQLBackend) + User.configure(config, PostgresBackend) Post.__backend__ = User.backend() Comment.__backend__ = User.backend() @@ -103,16 +104,17 @@ if __name__ == "__main__": ```python import asyncio import multiprocessing -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgreSQLBackend, PostgreSQLConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from models import AsyncComment, AsyncPost, AsyncUser async def async_worker_main(post_ids: list[int]): # 1. 在进程内配置异步连接(psycopg 原生 async,TCP 网络 I/O) - config = PostgreSQLConnectionConfig( + config = PostgresConnectionConfig( host="localhost", port=5432, database="mydb", username="app", password="secret", ) - await AsyncUser.configure(config, AsyncPostgreSQLBackend) + await AsyncUser.configure(config, AsyncPostgresBackend) AsyncPost.__backend__ = AsyncUser.backend() AsyncComment.__backend__ = AsyncUser.backend() @@ -161,7 +163,7 @@ if __name__ == "__main__": ### 3.1 单连接模型的约束 -`rhosocial-activerecord` 的异步 PostgreSQL 后端(`AsyncPostgreSQLBackend`)基于 `psycopg` 版本 3 的异步接口,**每个 ActiveRecord 类绑定一条连接**。这与连接池方案不同: +`rhosocial-activerecord` 的异步 PostgreSQL 后端(`AsyncPostgresBackend`)基于 `psycopg` 版本 3 的异步接口,**每个 ActiveRecord 类绑定一条连接**。这与连接池方案不同: | 特性 | 单连接 ORM(本项目) | 连接池方案 | | --- | --- | --- | @@ -414,7 +416,7 @@ def claim_posts_with_retry(batch_size: int = 5, max_retry: int = 3) -> list: | 工作负载类型 | 特征 | 合适部署 | | --- | --- | --- | | Web API 服务 | 短请求、高并发、响应时间敏感 | FastAPI / Django + asyncio + 连接池 | -| 数据分析批处理 | 长时间运行、大数据量、CPU 密集 | 独立脚本 + multiprocessing + PostgreSQLBackend | +| 数据分析批处理 | 长时间运行、大数据量、CPU 密集 | 独立脚本 + multiprocessing + PostgresBackend | | 任务队列消费 | 定期轮询、任务间独立、易于水平扩展 | 独立 Worker 进程池 | ### 5.2 推荐架构 @@ -425,7 +427,7 @@ def claim_posts_with_retry(batch_size: int = 5, max_retry: int = 3) -> list: └──→ 任务队列(PostgreSQL 数据库表 / Redis) │ └──→ 后台 Worker 进程池 - (每进程独立 PostgreSQLBackend 同步连接) + (每进程独立 PostgresBackend 同步连接) ``` Web 应用负责接收请求、写入任务队列;Worker 进程池负责执行耗时任务。 @@ -536,7 +538,7 @@ async def async_worker_task(user_id, conn_params): async def async_worker_task(user_id, conn_params): # 只传递连接参数(可序列化),在子进程内创建新实例 config = conn_params['config_kwargs'] - await Model.configure(config, AsyncPostgreSQLBackend) + await Model.configure(config, AsyncPostgresBackend) user = await Model.find_one(user_id) await Model.backend().disconnect() ``` @@ -568,8 +570,8 @@ Worker 测试对 PostgreSQL 后端的覆盖率贡献: ### 8.3 生产环境建议 -1. **同步 Worker 是首选**:在多进程 Worker 场景下,同步后端(`PostgreSQLBackend`)是更稳定的选择 -2. **异步适用于单进程**:异步后端(`AsyncPostgreSQLBackend`)在单进程内的顺序执行场景下表现良好 +1. **同步 Worker 是首选**:在多进程 Worker 场景下,同步后端(`PostgresBackend`)是更稳定的选择 +2. **异步适用于单进程**:异步后端(`AsyncPostgresBackend`)在单进程内的顺序执行场景下表现良好 3. **避免跨进程传递异步实例**:只传递可序列化的连接参数,在子进程内创建新的异步后端实例 ### 8.4 FOR UPDATE 能力检测经验 @@ -845,7 +847,7 @@ def thread_worker(group, thread_id): ```python # database.py - 连接池管理器 from rhosocial.activerecord.connection.pool import PoolConfig, AsyncBackendPool -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend # 全局连接池(应用启动时创建) _pool: AsyncBackendPool = None diff --git a/docs/zh_CN/testing/configuration.md b/docs/zh_CN/testing/configuration.md index e8b6d825..d3cdb972 100644 --- a/docs/zh_CN/testing/configuration.md +++ b/docs/zh_CN/testing/configuration.md @@ -10,9 +10,8 @@ ```python from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend.impl.dummy import DummyBackend, DummyConnectionConfig - - +from rhosocial.activerecord.backend.impl.dummy.backend import DummyBackend +rhosocial.activerecord.backend.config import ConnectionConfig class User(ActiveRecord): name: str email: str @@ -25,7 +24,7 @@ class User(ActiveRecord): # 配置 Dummy 后端 -config = DummyConnectionConfig() +config = ConnectionConfig() User.configure(config, DummyBackend) ``` @@ -34,7 +33,8 @@ User.configure(config, DummyBackend) 对于需要真实数据库行为的测试,使用 SQLite 后端: ```python -from rhosocial.activerecord.backend.impl.sqlite import SQLiteBackend, SQLiteConnectionConfig +from rhosocial.activerecord.backend.impl.sqlite.backend import SQLiteBackend +from rhosocial.activerecord.backend.impl.sqlite.config import SQLiteConnectionConfig class User(ActiveRecord): @@ -59,9 +59,8 @@ User.configure(config, SQLiteBackend) ```python import os -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - - +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig class User(ActiveRecord): name: str email: str @@ -74,26 +73,25 @@ class User(ActiveRecord): # 从环境变量读取配置 -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host=os.environ.get('PG_HOST', 'localhost'), port=int(os.environ.get('PG_PORT', 5432)), database=os.environ.get('PG_DATABASE', 'test'), username=os.environ.get('PG_USER', 'postgres'), password=os.environ.get('PG_PASSWORD', ''), ) -User.configure(config, PostgreSQLBackend) +User.configure(config, PostgresBackend) ``` ## 测试 Fixtures ```python import pytest -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - - +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig @pytest.fixture def postgres_config(): - return PostgreSQLConnectionConfig( + return PostgresConnectionConfig( host='localhost', port=5432, database='test', @@ -104,7 +102,7 @@ def postgres_config(): @pytest.fixture def postgres_backend(postgres_config): - backend = PostgreSQLBackend(connection_config=postgres_config) + backend = PostgresBackend(connection_config=postgres_config) backend.connect() yield backend backend.disconnect() diff --git a/docs/zh_CN/transaction_support/README.md b/docs/zh_CN/transaction_support/README.md index a95bfd8e..a3b95d2b 100644 --- a/docs/zh_CN/transaction_support/README.md +++ b/docs/zh_CN/transaction_support/README.md @@ -12,7 +12,7 @@ PostgreSQL 提供强大的事务支持,包括保存点、隔离级别和可延 ## 快速开始 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend backend = PostgresBackend(connection_config=config) tm = backend.transaction_manager diff --git a/docs/zh_CN/transaction_support/deferrable.md b/docs/zh_CN/transaction_support/deferrable.md index e30af2bb..bb5f7c5b 100644 --- a/docs/zh_CN/transaction_support/deferrable.md +++ b/docs/zh_CN/transaction_support/deferrable.md @@ -9,7 +9,7 @@ PostgreSQL 支持 SERIALIZABLE 事务的可延迟约束。 ## 使用方法 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.transaction import IsolationLevel backend = PostgresBackend(connection_config=config) diff --git a/docs/zh_CN/transaction_support/isolation_level.md b/docs/zh_CN/transaction_support/isolation_level.md index 4770c619..d384150a 100644 --- a/docs/zh_CN/transaction_support/isolation_level.md +++ b/docs/zh_CN/transaction_support/isolation_level.md @@ -11,7 +11,7 @@ ## 设置隔离级别 ```python -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.transaction import IsolationLevel backend = PostgresBackend(connection_config=config) diff --git a/docs/zh_CN/troubleshooting/connection.md b/docs/zh_CN/troubleshooting/connection.md index 64dc25c3..722d35cc 100644 --- a/docs/zh_CN/troubleshooting/connection.md +++ b/docs/zh_CN/troubleshooting/connection.md @@ -62,7 +62,7 @@ psycopg.OperationalError: connection timeout expired ### 解决方案 ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='remote.host.com', connect_timeout=30, # 增加超时时间 ) @@ -82,7 +82,7 @@ psycopg.OperationalError: SSL connection failed ### 解决方案 ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='remote.host.com', sslmode='require', # 选项:disable, prefer, require, verify-ca, verify-full sslrootcert='/path/to/ca.crt', # 用于 verify-ca 或 verify-full @@ -256,7 +256,7 @@ SET GLOBAL idle_in_transaction_session_timeout = '10min'; 对于高并发场景,配置连接池: ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='localhost', database='mydb', # 连接池设置 diff --git a/docs/zh_CN/troubleshooting/performance.md b/docs/zh_CN/troubleshooting/performance.md index 2c98997b..11f46121 100644 --- a/docs/zh_CN/troubleshooting/performance.md +++ b/docs/zh_CN/troubleshooting/performance.md @@ -20,10 +20,10 @@ SELECT pg_reload_conf(); ### 使用 EXPLAIN ANALYZE ```python -from rhosocial.activerecord.backend.impl.postgres import PostgreSQLBackend, PostgreSQLConnectionConfig - -backend = PostgreSQLBackend( - connection_config=PostgreSQLConnectionConfig( +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +backend = PostgresBackend( + connection_config=PostgresConnectionConfig( host='localhost', database='myapp', username='user', @@ -87,7 +87,7 @@ VACUUM ANALYZE users; 对于高并发应用,使用连接池: ```python -config = PostgreSQLConnectionConfig( +config = PostgresConnectionConfig( host='localhost', database='myapp', username='user', diff --git a/docs/zh_CN/type_adapters/README.md b/docs/zh_CN/type_adapters/README.md index 00dd59ae..bc1ee57f 100644 --- a/docs/zh_CN/type_adapters/README.md +++ b/docs/zh_CN/type_adapters/README.md @@ -107,8 +107,10 @@ class User(ActiveRecord): 对于已注册适配器的 PostgreSQL 特有类型,使用对应的包装类: ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresMoney, Point, PostgresTsVector +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresMoney, + Point, + PostgresTsVector, ) class Order(ActiveRecord): @@ -136,9 +138,8 @@ backend.execute( **方式二:使用 PostgresRange 包装器(推荐 ActiveRecord 模型)** ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresRange, PostgresRangeAdapter -) +from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresRange +from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresRangeAdapter # 手动注册适配器 backend.adapter_registry.register(PostgresRangeAdapter(), PostgresRange, str) @@ -152,10 +153,15 @@ class Order(ActiveRecord): 对于存在 str→str 映射冲突的类型,必须显式注册适配器: ```python -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresXML, PostgresXMLAdapter, - PostgresBitString, PostgresBitStringAdapter, - PostgresJsonPath, PostgresJsonPathAdapter +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresXML, + PostgresBitString, + PostgresJsonPath, +) +from rhosocial.activerecord.backend.impl.postgres.adapters import ( + PostgresXMLAdapter, + PostgresBitStringAdapter, + PostgresJsonPathAdapter, ) # 方式一:在 backend 上全局注册 @@ -175,9 +181,13 @@ result = backend.execute( ```python from rhosocial.activerecord import ActiveRecord -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresXML, PostgresXMLAdapter, - PostgresBitString, PostgresBitStringAdapter +from rhosocial.activerecord.backend.impl.postgres.type_values import ( + PostgresXML, + PostgresBitString, +) +from rhosocial.activerecord.backend.impl.postgres.adapters import ( + PostgresXMLAdapter, + PostgresBitStringAdapter, ) class Document(ActiveRecord): diff --git a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/__init__.py deleted file mode 100644 index 1aa861dd..00000000 --- a/src/rhosocial/activerecord/backend/impl/postgres/__init__.py +++ /dev/null @@ -1,971 +0,0 @@ -# src/rhosocial/activerecord/backend/impl/postgres/__init__.py -"""PostgreSQL backend implementation for the Python ORM. - -This module provides: -- PostgreSQL synchronous backend with connection management and query execution -- PostgreSQL asynchronous backend with async/await support -- PostgreSQL-specific connection configuration -- Type mapping and value conversion -- Transaction management with savepoint support (sync and async) -- PostgreSQL dialect and expression handling -- PostgreSQL-specific type definitions and mappings - -Architecture: -- PostgreSQLBackend: Synchronous implementation using psycopg -- AsyncPostgreSQLBackend: Asynchronous implementation using psycopg -- Independent from ORM frameworks - uses only native drivers""" - -# Public names are resolved on first access rather than at package-execution -# time. Importing this package used to import every submodule eagerly, which -# pulled in the DB driver through ``.backend`` and made the expression classes -# unreachable without it: Python executes a parent package's ``__init__`` before -# any submodule, so ``impl..expression`` could not be imported on its own. -# The exports themselves are unchanged -- attribute access, ``__all__``, ``dir()`` -# and ``from ... import *`` all behave as before. -from typing import TYPE_CHECKING - -#: Public name -> (submodule that defines it, attribute name within it). -#: The two differ when the original import was aliased. -_EXPORTS = { - "AsyncPostgresBackend": (".backend", "AsyncPostgresBackend"), - "AsyncPostgresTransactionManager": (".transaction", "AsyncPostgresTransactionManager"), - "Box": (".type_values.geometric", "Box"), - "CID": (".type_values.object_identifier", "CID"), - "Circle": (".type_values.geometric", "Circle"), - "EnumTypeManager": (".type_values.enum", "EnumTypeManager"), - "Line": (".type_values.geometric", "Line"), - "LineSegment": (".type_values.geometric", "LineSegment"), - "OID": (".type_values.object_identifier", "OID"), - "Path": (".type_values.geometric", "Path"), - "Point": (".type_values.geometric", "Point"), - "Polygon": (".type_values.geometric", "Polygon"), - "PostgresAddDomainCheckAction": (".expression.ddl", "PostgresAddDomainCheckAction"), - "PostgresAddEnumValueAction": (".expression.ddl", "PostgresAddEnumValueAction"), - "PostgresAddTypeAttributeAction": (".expression.ddl", "PostgresAddTypeAttributeAction"), - "PostgresAlterDomainExpression": (".expression.ddl", "PostgresAlterDomainExpression"), - "PostgresAlterEnumTypeAddValueExpression": (".expression.ddl", "PostgresAlterEnumTypeAddValueExpression"), - "PostgresAlterEnumTypeRenameValueExpression": (".expression.ddl", "PostgresAlterEnumTypeRenameValueExpression"), - "PostgresAlterTypeAttributeAction": (".expression.ddl", "PostgresAlterTypeAttributeAction"), - "PostgresBackend": (".backend", "PostgresBackend"), - "PostgresBaseTypeDefinition": (".expression.ddl", "PostgresBaseTypeDefinition"), - "PostgresBigSerialType": (".expression.types", "PostgresBigSerialType"), - "PostgresBitString": (".type_values.bit_string", "PostgresBitString"), - "PostgresBitStringAdapter": (".adapters.bit_string", "PostgresBitStringAdapter"), - "PostgresBitType": (".expression.types", "PostgresBitType"), - "PostgresBoxType": (".expression.types", "PostgresBoxType"), - "PostgresByteaType": (".expression.types", "PostgresByteaType"), - "PostgresCIDType": (".expression.types", "PostgresCIDType"), - "PostgresChangeDomainOwnerAction": (".expression.ddl", "PostgresChangeDomainOwnerAction"), - "PostgresChangeTypeOwnerAction": (".expression.ddl", "PostgresChangeTypeOwnerAction"), - "PostgresCidrType": (".expression.types", "PostgresCidrType"), - "PostgresCircleType": (".expression.types", "PostgresCircleType"), - "PostgresCollation": (".collation", "PostgresCollation"), - "PostgresCompositeTypeAttribute": (".expression.ddl", "PostgresCompositeTypeAttribute"), - "PostgresCompositeTypeDefinition": (".expression.ddl", "PostgresCompositeTypeDefinition"), - "PostgresConnectionConfig": (".config", "PostgresConnectionConfig"), - "PostgresCopyFormat": (".expression", "PostgresCopyFormat"), - "PostgresCopyFromExpression": (".expression", "PostgresCopyFromExpression"), - "PostgresCopyLogVerbosity": (".expression", "PostgresCopyLogVerbosity"), - "PostgresCopyOnError": (".expression", "PostgresCopyOnError"), - "PostgresCopyToExpression": (".expression", "PostgresCopyToExpression"), - "PostgresCreateDomainExpression": (".expression.ddl", "PostgresCreateDomainExpression"), - "PostgresCreateEnumTypeExpression": (".expression.ddl", "PostgresCreateEnumTypeExpression"), - "PostgresCreateRangeTypeExpression": (".expression.ddl", "PostgresCreateRangeTypeExpression"), - "PostgresDateMultirangeType": (".expression.types", "PostgresDateMultirangeType"), - "PostgresDateRangeType": (".expression.types", "PostgresDateRangeType"), - "PostgresDialect": (".dialect", "PostgresDialect"), - "PostgresDropDomainCheckAction": (".expression.ddl", "PostgresDropDomainCheckAction"), - "PostgresDropDomainExpression": (".expression.ddl", "PostgresDropDomainExpression"), - "PostgresDropEnumTypeExpression": (".expression.ddl", "PostgresDropEnumTypeExpression"), - "PostgresDropTypeAttributeAction": (".expression.ddl", "PostgresDropTypeAttributeAction"), - "PostgresDropTypeExpression": (".expression.ddl", "PostgresDropTypeExpression"), - "PostgresEnumAdapter": (".adapters", "PostgresEnumAdapter"), - "PostgresEnumType": (".type_values", "PostgresEnumType"), - "PostgresEnumTypeDefinition": (".expression.ddl", "PostgresEnumTypeDefinition"), - "PostgresExplainPlanLine": (".explain", "PostgresExplainPlanLine"), - "PostgresExplainResult": (".explain", "PostgresExplainResult"), - "PostgresGeographyType": (".expression.types", "PostgresGeographyType"), - "PostgresGeometry": (".type_values.postgis", "PostgresGeometry"), - "PostgresGeometryAdapter": (".adapters.geometric", "PostgresGeometryAdapter"), - "PostgresGeometryType": (".expression.types", "PostgresGeometryType"), - "PostgresHalfvecType": (".expression.types", "PostgresHalfvecType"), - "PostgresHstore": (".type_values.hstore", "PostgresHstore"), - "PostgresHstoreAdapter": (".adapters.hstore", "PostgresHstoreAdapter"), - "PostgresHstoreType": (".expression.types", "PostgresHstoreType"), - "PostgresInetType": (".expression.types", "PostgresInetType"), - "PostgresInt4MultirangeType": (".expression.types", "PostgresInt4MultirangeType"), - "PostgresInt4RangeType": (".expression.types", "PostgresInt4RangeType"), - "PostgresInt8MultirangeType": (".expression.types", "PostgresInt8MultirangeType"), - "PostgresInt8RangeType": (".expression.types", "PostgresInt8RangeType"), - "PostgresJsonPath": (".type_values.json", "PostgresJsonPath"), - "PostgresJsonPathAdapter": (".adapters.json", "PostgresJsonPathAdapter"), - "PostgresJsonPathType": (".expression.types", "PostgresJsonPathType"), - "PostgresLineSegmentType": (".expression.types", "PostgresLineSegmentType"), - "PostgresLineType": (".expression.types", "PostgresLineType"), - "PostgresLsn": (".type_values.pg_lsn", "PostgresLsn"), - "PostgresLsnAdapter": (".adapters.pg_lsn", "PostgresLsnAdapter"), - "PostgresMacAddr8Type": (".expression.types", "PostgresMacAddr8Type"), - "PostgresMacAddrType": (".expression.types", "PostgresMacAddrType"), - "PostgresMacaddr": (".type_values.network_address", "PostgresMacaddr"), - "PostgresMacaddr8": (".type_values.network_address", "PostgresMacaddr8"), - "PostgresMacaddr8Adapter": (".adapters.network_address", "PostgresMacaddr8Adapter"), - "PostgresMacaddrAdapter": (".adapters.network_address", "PostgresMacaddrAdapter"), - "PostgresMoney": (".type_values.monetary", "PostgresMoney"), - "PostgresMoneyAdapter": (".adapters.monetary", "PostgresMoneyAdapter"), - "PostgresMoneyType": (".expression.types", "PostgresMoneyType"), - "PostgresMultirange": (".type_values.range", "PostgresMultirange"), - "PostgresMultirangeAdapter": (".adapters", "PostgresMultirangeAdapter"), - "PostgresNumMultirangeType": (".expression.types", "PostgresNumMultirangeType"), - "PostgresNumRangeType": (".expression.types", "PostgresNumRangeType"), - "PostgresOIDType": (".expression.types", "PostgresOIDType"), - "PostgresOidAdapter": (".adapters.object_identifier", "PostgresOidAdapter"), - "PostgresPathType": (".expression.types", "PostgresPathType"), - "PostgresPgLSNType": (".expression.types", "PostgresPgLSNType"), - "PostgresPointType": (".expression.types", "PostgresPointType"), - "PostgresPolygonType": (".expression.types", "PostgresPolygonType"), - "PostgresPostGISAdapter": (".adapters.postgis", "PostgresPostGISAdapter"), - "PostgresRange": (".type_values.range", "PostgresRange"), - "PostgresRangeAdapter": (".adapters", "PostgresRangeAdapter"), - "PostgresRangeTypeDefinition": (".expression.ddl", "PostgresRangeTypeDefinition"), - "PostgresRegClassType": (".expression.types", "PostgresRegClassType"), - "PostgresRegTypeType": (".expression.types", "PostgresRegTypeType"), - "PostgresRenameDomainConstraintAction": (".expression.ddl", "PostgresRenameDomainConstraintAction"), - "PostgresRenameEnumValueAction": (".expression.ddl", "PostgresRenameEnumValueAction"), - "PostgresRenameTypeAction": (".expression.ddl", "PostgresRenameTypeAction"), - "PostgresRenameTypeAttributeAction": (".expression.ddl", "PostgresRenameTypeAttributeAction"), - "PostgresRepackExpression": (".expression", "PostgresRepackExpression"), - "PostgresSerialType": (".expression.types", "PostgresSerialType"), - "PostgresSetDomainSchemaAction": (".expression.ddl", "PostgresSetDomainSchemaAction"), - "PostgresSetTypePropertiesAction": (".expression.ddl", "PostgresSetTypePropertiesAction"), - "PostgresSetTypeSchemaAction": (".expression.ddl", "PostgresSetTypeSchemaAction"), - "PostgresShellTypeDefinition": (".expression.ddl", "PostgresShellTypeDefinition"), - "PostgresSmallSerialType": (".expression.types", "PostgresSmallSerialType"), - "PostgresSparsevecType": (".expression.types", "PostgresSparsevecType"), - "PostgresStorageParameter": (".storage_parameters", "PostgresStorageParameter"), - "PostgresStorageParameterValueType": (".storage_parameters", "PostgresStorageParameterValueType"), - "PostgresTIDType": (".expression.types", "PostgresTIDType"), - "PostgresTSQueryType": (".expression.types", "PostgresTSQueryType"), - "PostgresTSVectorType": (".expression.types", "PostgresTSVectorType"), - "PostgresTidAdapter": (".adapters.object_identifier", "PostgresTidAdapter"), - "PostgresTransactionManager": (".transaction", "PostgresTransactionManager"), - "PostgresTsMultirangeType": (".expression.types", "PostgresTsMultirangeType"), - "PostgresTsQuery": (".type_values.text_search", "PostgresTsQuery"), - "PostgresTsQueryAdapter": (".adapters.text_search", "PostgresTsQueryAdapter"), - "PostgresTsRangeType": (".expression.types", "PostgresTsRangeType"), - "PostgresTsTzMultirangeType": (".expression.types", "PostgresTsTzMultirangeType"), - "PostgresTsTzRangeType": (".expression.types", "PostgresTsTzRangeType"), - "PostgresTsVector": (".type_values.text_search", "PostgresTsVector"), - "PostgresTsVectorAdapter": (".adapters.text_search", "PostgresTsVectorAdapter"), - "PostgresUUIDAdapter": (".adapters.uuid", "PostgresUUIDAdapter"), - "PostgresUUIDType": (".expression.types", "PostgresUUIDType"), - "PostgresValidateDomainConstraintAction": (".expression.ddl", "PostgresValidateDomainConstraintAction"), - "PostgresVarBitType": (".expression.types", "PostgresVarBitType"), - "PostgresVector": (".type_values.pgvector", "PostgresVector"), - "PostgresVectorAdapter": (".adapters.pgvector", "PostgresVectorAdapter"), - "PostgresVectorType": (".expression.types", "PostgresVectorType"), - "PostgresXID8Type": (".expression.types", "PostgresXID8Type"), - "PostgresXIDType": (".expression.types", "PostgresXIDType"), - "PostgresXML": (".type_values.xml", "PostgresXML"), - "PostgresXMLAdapter": (".adapters.xml", "PostgresXMLAdapter"), - "PostgresXMLType": (".expression.types", "PostgresXMLType"), - "PostgresXidAdapter": (".adapters.object_identifier", "PostgresXidAdapter"), - "RegClass": (".type_values.object_identifier", "RegClass"), - "RegCollation": (".type_values.object_identifier", "RegCollation"), - "RegConfig": (".type_values.object_identifier", "RegConfig"), - "RegDictionary": (".type_values.object_identifier", "RegDictionary"), - "RegNamespace": (".type_values.object_identifier", "RegNamespace"), - "RegOper": (".type_values.object_identifier", "RegOper"), - "RegOperator": (".type_values.object_identifier", "RegOperator"), - "RegProc": (".type_values.object_identifier", "RegProc"), - "RegProcedure": (".type_values.object_identifier", "RegProcedure"), - "RegRole": (".type_values.object_identifier", "RegRole"), - "RegType": (".type_values.object_identifier", "RegType"), - "TID": (".type_values.object_identifier", "TID"), - "XID": (".type_values.object_identifier", "XID"), - "XID8": (".type_values.object_identifier", "XID8"), - "bit_and": (".functions", "bit_and"), - "bit_concat": (".functions", "bit_concat"), - "bit_count": (".functions", "bit_count"), - "bit_get_bit": (".functions", "bit_get_bit"), - "bit_length": (".functions", "bit_length"), - "bit_length_func": (".functions", "bit_length_func"), - "bit_not": (".functions", "bit_not"), - "bit_octet_length": (".functions", "bit_octet_length"), - "bit_or": (".functions", "bit_or"), - "bit_set_bit": (".functions", "bit_set_bit"), - "bit_shift_left": (".functions", "bit_shift_left"), - "bit_shift_right": (".functions", "bit_shift_right"), - "bit_xor": (".functions", "bit_xor"), - "enum_first": (".functions", "enum_first"), - "enum_ge": (".functions", "enum_ge"), - "enum_gt": (".functions", "enum_gt"), - "enum_last": (".functions", "enum_last"), - "enum_le": (".functions", "enum_le"), - "enum_lt": (".functions", "enum_lt"), - "enum_range": (".functions", "enum_range"), - "gen_random_uuid": (".functions", "gen_random_uuid"), - "geometry_area": (".functions", "geometry_area"), - "geometry_center": (".functions", "geometry_center"), - "geometry_contained_by": (".functions", "geometry_contained_by"), - "geometry_contains": (".functions", "geometry_contains"), - "geometry_distance": (".functions", "geometry_distance"), - "geometry_height": (".functions", "geometry_height"), - "geometry_length": (".functions", "geometry_length"), - "geometry_not_extend_left": (".functions", "geometry_not_extend_left"), - "geometry_not_extend_right": (".functions", "geometry_not_extend_right"), - "geometry_npoints": (".functions", "geometry_npoints"), - "geometry_overlaps": (".functions", "geometry_overlaps"), - "geometry_strictly_left": (".functions", "geometry_strictly_left"), - "geometry_strictly_right": (".functions", "geometry_strictly_right"), - "geometry_width": (".functions", "geometry_width"), - "hstore_akeys": (".functions", "hstore_akeys"), - "hstore_all_keys_exist": (".functions", "hstore_all_keys_exist"), - "hstore_any_key_exists": (".functions", "hstore_any_key_exists"), - "hstore_avals": (".functions", "hstore_avals"), - "hstore_concat": (".functions", "hstore_concat"), - "hstore_contained_by": (".functions", "hstore_contained_by"), - "hstore_contains": (".functions", "hstore_contains"), - "hstore_defined": (".functions", "hstore_defined"), - "hstore_delete": (".functions", "hstore_delete"), - "hstore_delete_keys": (".functions", "hstore_delete_keys"), - "hstore_delete_pairs": (".functions", "hstore_delete_pairs"), - "hstore_each": (".functions", "hstore_each"), - "hstore_exist": (".functions", "hstore_exist"), - "hstore_from_key_value": (".functions", "hstore_from_key_value"), - "hstore_from_record": (".functions", "hstore_from_record"), - "hstore_get_value": (".functions", "hstore_get_value"), - "hstore_get_value_as_text": (".functions", "hstore_get_value_as_text"), - "hstore_get_values": (".functions", "hstore_get_values"), - "hstore_key_exists": (".functions", "hstore_key_exists"), - "hstore_populate_record": (".functions", "hstore_populate_record"), - "hstore_record_update": (".functions", "hstore_record_update"), - "hstore_skeys": (".functions", "hstore_skeys"), - "hstore_slice": (".functions", "hstore_slice"), - "hstore_subscript_get": (".functions", "hstore_subscript_get"), - "hstore_subscript_set": (".functions", "hstore_subscript_set"), - "hstore_subtract_key": (".functions", "hstore_subtract_key"), - "hstore_subtract_keys": (".functions", "hstore_subtract_keys"), - "hstore_subtract_pairs": (".functions", "hstore_subtract_pairs"), - "hstore_svals": (".functions", "hstore_svals"), - "hstore_to_array": (".functions", "hstore_to_array"), - "hstore_to_array_operator": (".functions", "hstore_to_array_operator"), - "hstore_to_json": (".functions", "hstore_to_json"), - "hstore_to_json_loose": (".functions", "hstore_to_json_loose"), - "hstore_to_jsonb": (".functions", "hstore_to_jsonb"), - "hstore_to_jsonb_loose": (".functions", "hstore_to_jsonb_loose"), - "hstore_to_matrix": (".functions", "hstore_to_matrix"), - "hstore_to_matrix_operator": (".functions", "hstore_to_matrix_operator"), - "json_path_filter": (".functions", "json_path_filter"), - "json_path_index": (".functions", "json_path_index"), - "json_path_key": (".functions", "json_path_key"), - "json_path_root": (".functions", "json_path_root"), - "json_path_wildcard": (".functions", "json_path_wildcard"), - "jsonb_path_exists": (".functions", "jsonb_path_exists"), - "jsonb_path_match": (".functions", "jsonb_path_match"), - "jsonb_path_query": (".functions", "jsonb_path_query"), - "jsonb_path_query_first": (".functions", "jsonb_path_query_first"), - "phraseto_tsquery": (".functions.text_search", "phraseto_tsquery"), - "plainto_tsquery": (".functions.text_search", "plainto_tsquery"), - "range_adjacent": (".functions", "range_adjacent"), - "range_contained_by": (".functions", "range_contained_by"), - "range_contains": (".functions", "range_contains"), - "range_contains_range": (".functions", "range_contains_range"), - "range_difference": (".functions", "range_difference"), - "range_intersection": (".functions", "range_intersection"), - "range_is_empty": (".functions", "range_is_empty"), - "range_lower": (".functions", "range_lower"), - "range_lower_inc": (".functions", "range_lower_inc"), - "range_lower_inf": (".functions", "range_lower_inf"), - "range_not_extend_left": (".functions", "range_not_extend_left"), - "range_not_extend_right": (".functions", "range_not_extend_right"), - "range_overlaps": (".functions", "range_overlaps"), - "range_strictly_left_of": (".functions", "range_strictly_left_of"), - "range_strictly_right_of": (".functions", "range_strictly_right_of"), - "range_union": (".functions", "range_union"), - "range_upper": (".functions", "range_upper"), - "range_upper_inc": (".functions", "range_upper_inc"), - "range_upper_inf": (".functions", "range_upper_inf"), - "st_area": (".functions", "st_area"), - "st_as_geojson": (".functions", "st_as_geojson"), - "st_as_text": (".functions", "st_as_text"), - "st_buffer": (".functions", "st_buffer"), - "st_centroid": (".functions", "st_centroid"), - "st_contains": (".functions", "st_contains"), - "st_crosses": (".functions", "st_crosses"), - "st_distance": (".functions", "st_distance"), - "st_dwithin": (".functions", "st_dwithin"), - "st_envelope": (".functions", "st_envelope"), - "st_geog_from_text": (".functions", "st_geog_from_text"), - "st_geom_from_text": (".functions", "st_geom_from_text"), - "st_intersects": (".functions", "st_intersects"), - "st_length": (".functions", "st_length"), - "st_make_point": (".functions", "st_make_point"), - "st_overlaps": (".functions", "st_overlaps"), - "st_set_srid": (".functions", "st_set_srid"), - "st_touches": (".functions", "st_touches"), - "st_transform": (".functions", "st_transform"), - "st_within": (".functions", "st_within"), - "to_tsquery": (".functions.text_search", "to_tsquery"), - "to_tsvector": (".functions.text_search", "to_tsvector"), - "ts_headline": (".functions.text_search", "ts_headline"), - "ts_matches": (".functions.text_search", "ts_matches"), - "ts_matches_expr": (".functions.text_search", "ts_matches_expr"), - "ts_rank": (".functions.text_search", "ts_rank"), - "ts_rank_cd": (".functions.text_search", "ts_rank_cd"), - "tsvector_concat": (".functions.text_search", "tsvector_concat"), - "tsvector_length": (".functions.text_search", "tsvector_length"), - "tsvector_setweight": (".functions.text_search", "tsvector_setweight"), - "tsvector_strip": (".functions.text_search", "tsvector_strip"), - "uuid_default_generator": (".functions", "uuid_default_generator"), - "vector_cosine_distance": (".functions", "vector_cosine_distance"), - "vector_cosine_similarity": (".functions", "vector_cosine_similarity"), - "vector_inner_product": (".functions", "vector_inner_product"), - "vector_l2_distance": (".functions", "vector_l2_distance"), - "vector_literal": (".functions", "vector_literal"), - "websearch_to_tsquery": (".functions.text_search", "websearch_to_tsquery"), -} - -if TYPE_CHECKING: # pragma: no cover - re-exports for type checkers - from .backend import PostgresBackend, AsyncPostgresBackend - from .collation import PostgresCollation # noqa: F401 - from .config import PostgresConnectionConfig - from .dialect import PostgresDialect - from .storage_parameters import ( # noqa: F401 - PostgresStorageParameter, - PostgresStorageParameterValueType, - ) - 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, - PostgresHalfvecType, - PostgresSparsevecType, - PostgresXID8Type, - PostgresXIDType, - PostgresXMLType, - PostgresCidrType, - ) - from .explain import PostgresExplainResult, PostgresExplainPlanLine - from .expression import ( - PostgresCopyFormat, - PostgresCopyFromExpression, - PostgresCopyLogVerbosity, - PostgresCopyOnError, - PostgresCopyToExpression, - PostgresRepackExpression, - ) - from .expression.ddl import ( - PostgresAddDomainCheckAction, - PostgresAddEnumValueAction, - PostgresAddTypeAttributeAction, - PostgresAlterDomainExpression, - PostgresAlterEnumTypeAddValueExpression, - PostgresAlterEnumTypeRenameValueExpression, - PostgresAlterTypeAttributeAction, - PostgresBaseTypeDefinition, - PostgresChangeDomainOwnerAction, - PostgresChangeTypeOwnerAction, - PostgresCompositeTypeAttribute, - PostgresCompositeTypeDefinition, - PostgresCreateDomainExpression, - PostgresCreateEnumTypeExpression, - PostgresCreateRangeTypeExpression, - PostgresDropDomainCheckAction, - PostgresDropDomainExpression, - PostgresDropEnumTypeExpression, - PostgresDropTypeAttributeAction, - PostgresDropTypeExpression, - PostgresEnumTypeDefinition, - PostgresRangeTypeDefinition, - PostgresRenameDomainConstraintAction, - PostgresRenameEnumValueAction, - PostgresRenameTypeAction, - PostgresRenameTypeAttributeAction, - PostgresSetDomainSchemaAction, - PostgresSetTypePropertiesAction, - PostgresSetTypeSchemaAction, - PostgresShellTypeDefinition, - PostgresValidateDomainConstraintAction, - ) - from .adapters import ( - PostgresEnumAdapter, - PostgresRangeAdapter, - PostgresMultirangeAdapter, - ) - from .types.range import ( - PostgresRange, - PostgresMultirange, - ) - from .types.geometric import ( - Point, - Line, - LineSegment, - Box, - Path, - Polygon, - Circle, - ) - from .adapters.geometric import PostgresGeometryAdapter - from .types.bit_string import PostgresBitString - from .adapters.bit_string import PostgresBitStringAdapter - from .types.monetary import PostgresMoney - from .adapters.monetary import PostgresMoneyAdapter - from .types.xml import PostgresXML - from .adapters.xml import PostgresXMLAdapter - from .types.network_address import PostgresMacaddr, PostgresMacaddr8 - from .adapters.network_address import PostgresMacaddrAdapter, PostgresMacaddr8Adapter - from .adapters.uuid import PostgresUUIDAdapter - from .types.pgvector import PostgresVector - from .adapters.pgvector import PostgresVectorAdapter - from .types.postgis import PostgresGeometry - from .adapters.postgis import PostgresPostGISAdapter - from .types.hstore import PostgresHstore - from .adapters.hstore import PostgresHstoreAdapter - from .types.text_search import ( - PostgresTsVector, - PostgresTsQuery, - ) - from .functions.text_search import ( - to_tsvector, - to_tsquery, - plainto_tsquery, - phraseto_tsquery, - websearch_to_tsquery, - ts_matches, - ts_matches_expr, - ts_rank, - ts_rank_cd, - ts_headline, - tsvector_concat, - tsvector_strip, - tsvector_setweight, - tsvector_length, - ) - from .adapters.text_search import PostgresTsVectorAdapter, PostgresTsQueryAdapter - from .types.pg_lsn import PostgresLsn - from .adapters.pg_lsn import PostgresLsnAdapter - from .types.object_identifier import ( - OID, - RegClass, - RegType, - RegProc, - RegProcedure, - RegOper, - RegOperator, - RegConfig, - RegDictionary, - RegNamespace, - RegRole, - RegCollation, - XID, - XID8, - CID, - TID, - ) - from .adapters.object_identifier import PostgresOidAdapter, PostgresXidAdapter, PostgresTidAdapter - from .types.json import PostgresJsonPath - from .adapters.json import PostgresJsonPathAdapter - from .types.enum import EnumTypeManager - from .functions import ( - # Range functions - range_contains, - range_contained_by, - range_contains_range, - range_overlaps, - range_adjacent, - range_strictly_left_of, - range_strictly_right_of, - range_not_extend_right, - range_not_extend_left, - range_union, - range_intersection, - range_difference, - range_lower, - range_upper, - range_is_empty, - range_lower_inc, - range_upper_inc, - range_lower_inf, - range_upper_inf, - # Geometry functions - geometry_distance, - geometry_contains, - geometry_contained_by, - geometry_overlaps, - geometry_strictly_left, - geometry_strictly_right, - geometry_not_extend_right, - geometry_not_extend_left, - geometry_area, - geometry_center, - geometry_length, - geometry_width, - geometry_height, - geometry_npoints, - # Enum functions - enum_range, - enum_first, - enum_last, - enum_lt, - enum_le, - enum_gt, - enum_ge, - # Bit string functions - bit_concat, - bit_and, - bit_or, - bit_xor, - bit_not, - bit_shift_left, - bit_shift_right, - bit_length, - bit_length_func, - bit_octet_length, - bit_get_bit, - bit_set_bit, - bit_count, - # JSON functions - json_path_root, - json_path_key, - json_path_index, - json_path_wildcard, - json_path_filter, - jsonb_path_query, - jsonb_path_query_first, - jsonb_path_exists, - jsonb_path_match, - # pgvector functions - vector_l2_distance, - vector_cosine_distance, - vector_inner_product, - vector_cosine_similarity, - vector_literal, - # PostGIS functions - Construction - st_make_point, - st_geom_from_text, - st_geog_from_text, - st_set_srid, - st_transform, - # PostGIS functions - Predicates - st_contains, - st_intersects, - st_within, - st_dwithin, - st_crosses, - st_touches, - st_overlaps, - # PostGIS functions - Measurements - st_distance, - st_area, - st_length, - # PostGIS functions - Output - st_as_geojson, - st_as_text, - # PostGIS functions - Operations - st_buffer, - st_envelope, - st_centroid, - # hstore functions - Constructors - hstore_from_record, - hstore_from_key_value, - # hstore functions - Key/Value Extraction - hstore_akeys, - hstore_skeys, - hstore_avals, - hstore_svals, - hstore_each, - # hstore functions - Conversion - hstore_to_array, - hstore_to_matrix, - hstore_to_json, - hstore_to_jsonb, - hstore_to_json_loose, - hstore_to_jsonb_loose, - # hstore functions - Subset - hstore_slice, - # hstore functions - Existence - hstore_exist, - hstore_defined, - # hstore functions - Delete - hstore_delete, - hstore_delete_keys, - hstore_delete_pairs, - # hstore functions - Record - hstore_populate_record, - # hstore functions - Operators - hstore_get_value, - hstore_get_value_as_text, - hstore_get_values, - hstore_concat, - hstore_key_exists, - hstore_all_keys_exist, - hstore_any_key_exists, - hstore_contains, - hstore_contained_by, - hstore_subtract_key, - hstore_subtract_keys, - hstore_subtract_pairs, - hstore_to_array_operator, - hstore_to_matrix_operator, - hstore_record_update, - # hstore functions - Subscript - hstore_subscript_get, - hstore_subscript_set, - ) - -__all__ = [ - "PostgresBackend", - "AsyncPostgresBackend", - "PostgresConnectionConfig", - "PostgresDialect", - "PostgresStorageParameter", - "PostgresStorageParameterValueType", - "PostgresTransactionManager", - "AsyncPostgresTransactionManager", - "PostgresEnumType", - "PostgresEnumAdapter", - "PostgresExplainResult", - "PostgresExplainPlanLine", - "PostgresCopyFormat", - "PostgresCopyFromExpression", - "PostgresCopyLogVerbosity", - "PostgresCopyOnError", - "PostgresCopyToExpression", - "PostgresRepackExpression", - "PostgresCompositeTypeAttribute", - "PostgresCompositeTypeDefinition", - "PostgresEnumTypeDefinition", - "PostgresRangeTypeDefinition", - "PostgresBaseTypeDefinition", - "PostgresShellTypeDefinition", - "PostgresRenameTypeAction", - "PostgresSetTypeSchemaAction", - "PostgresChangeTypeOwnerAction", - "PostgresRenameTypeAttributeAction", - "PostgresAddTypeAttributeAction", - "PostgresDropTypeAttributeAction", - "PostgresAlterTypeAttributeAction", - "PostgresAddEnumValueAction", - "PostgresRenameEnumValueAction", - "PostgresSetTypePropertiesAction", - "PostgresDropTypeExpression", - "PostgresCreateEnumTypeExpression", - "PostgresDropEnumTypeExpression", - "PostgresAlterEnumTypeAddValueExpression", - "PostgresAlterEnumTypeRenameValueExpression", - "PostgresCreateRangeTypeExpression", - "PostgresAddDomainCheckAction", - "PostgresDropDomainCheckAction", - "PostgresRenameDomainConstraintAction", - "PostgresValidateDomainConstraintAction", - "PostgresChangeDomainOwnerAction", - "PostgresSetDomainSchemaAction", - "PostgresCreateDomainExpression", - "PostgresAlterDomainExpression", - "PostgresDropDomainExpression", - "PostgresRange", - "PostgresRangeAdapter", - "PostgresMultirange", - "PostgresMultirangeAdapter", - "range_contains", - "range_contained_by", - "range_contains_range", - "range_overlaps", - "range_adjacent", - "range_strictly_left_of", - "range_strictly_right_of", - "range_not_extend_right", - "range_not_extend_left", - "range_union", - "range_intersection", - "range_difference", - "range_lower", - "range_upper", - "range_is_empty", - "range_lower_inc", - "range_upper_inc", - "range_lower_inf", - "range_upper_inf", - "Point", - "Line", - "LineSegment", - "Box", - "Path", - "Polygon", - "Circle", - "PostgresGeometryAdapter", - "geometry_distance", - "geometry_contains", - "geometry_contained_by", - "geometry_overlaps", - "geometry_strictly_left", - "geometry_strictly_right", - "geometry_not_extend_right", - "geometry_not_extend_left", - "geometry_area", - "geometry_center", - "geometry_length", - "geometry_width", - "geometry_height", - "geometry_npoints", - "PostgresBitString", - "PostgresBitStringAdapter", - "bit_concat", - "bit_and", - "bit_or", - "bit_xor", - "bit_not", - "bit_shift_left", - "bit_shift_right", - "bit_length", - "bit_length_func", - "bit_octet_length", - "bit_get_bit", - "bit_set_bit", - "bit_count", - "EnumTypeManager", - "enum_range", - "enum_first", - "enum_last", - "enum_lt", - "enum_le", - "enum_gt", - "enum_ge", - "PostgresMoney", - "PostgresMoneyAdapter", - "PostgresXML", - "PostgresXMLAdapter", - "PostgresMacaddr", - "PostgresMacaddrAdapter", - "PostgresMacaddr8", - "PostgresMacaddr8Adapter", - "PostgresTsVector", - "PostgresTsVectorAdapter", - "PostgresTsQuery", - "PostgresTsQueryAdapter", - "to_tsvector", - "to_tsquery", - "plainto_tsquery", - "phraseto_tsquery", - "websearch_to_tsquery", - "ts_matches", - "ts_matches_expr", - "ts_rank", - "ts_rank_cd", - "ts_headline", - "tsvector_concat", - "tsvector_strip", - "tsvector_setweight", - "tsvector_length", - "PostgresLsn", - "PostgresLsnAdapter", - "OID", - "RegClass", - "RegType", - "RegProc", - "RegProcedure", - "RegOper", - "RegOperator", - "RegConfig", - "RegDictionary", - "RegNamespace", - "RegRole", - "RegCollation", - "XID", - "XID8", - "CID", - "TID", - "PostgresOidAdapter", - "PostgresXidAdapter", - "PostgresTidAdapter", - "PostgresJsonPath", - "PostgresJsonPathAdapter", - "json_path_root", - "json_path_key", - "json_path_index", - "json_path_wildcard", - "json_path_filter", - "jsonb_path_query", - "jsonb_path_query_first", - "jsonb_path_exists", - "jsonb_path_match", - "PostgresUUIDAdapter", - "gen_random_uuid", - "uuid_default_generator", - "PostgresVector", - "PostgresVectorAdapter", - "vector_l2_distance", - "vector_cosine_distance", - "vector_inner_product", - "vector_cosine_similarity", - "vector_literal", - "PostgresGeometry", - "PostgresPostGISAdapter", - "PostgresHstore", - "PostgresHstoreAdapter", - "st_make_point", - "st_geom_from_text", - "st_geog_from_text", - "st_set_srid", - "st_transform", - "st_contains", - "st_intersects", - "st_within", - "st_dwithin", - "st_crosses", - "st_touches", - "st_overlaps", - "st_distance", - "st_area", - "st_length", - "st_as_geojson", - "st_as_text", - "st_buffer", - "st_envelope", - "st_centroid", - "hstore_from_record", - "hstore_from_key_value", - "hstore_akeys", - "hstore_skeys", - "hstore_avals", - "hstore_svals", - "hstore_each", - "hstore_to_array", - "hstore_to_matrix", - "hstore_to_json", - "hstore_to_jsonb", - "hstore_to_json_loose", - "hstore_to_jsonb_loose", - "hstore_slice", - "hstore_exist", - "hstore_defined", - "hstore_delete", - "hstore_delete_keys", - "hstore_delete_pairs", - "hstore_populate_record", - "hstore_get_value", - "hstore_get_value_as_text", - "hstore_get_values", - "hstore_concat", - "hstore_key_exists", - "hstore_all_keys_exist", - "hstore_any_key_exists", - "hstore_contains", - "hstore_contained_by", - "hstore_subtract_key", - "hstore_subtract_keys", - "hstore_subtract_pairs", - "hstore_to_array_operator", - "hstore_to_matrix_operator", - "hstore_record_update", - "hstore_subscript_get", - "hstore_subscript_set", - "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", - "PostgresHalfvecType", - "PostgresSparsevecType", - "PostgresXID8Type", - "PostgresXIDType", - "PostgresXMLType", - "PostgresCidrType", -] - - -def __getattr__(name: str): - """Resolve a public name by importing its defining submodule once.""" - target = _EXPORTS.get(name) - if target is not None: - import importlib - - module_name, attr = target - value = getattr(importlib.import_module(module_name, __name__), attr) - globals()[name] = value # cache, so __getattr__ runs at most once per name - return value - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -def __dir__(): - return sorted(set(globals()) | set(_EXPORTS)) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py index 69dce651..16301cbf 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/json.py @@ -81,7 +81,7 @@ class PostgresJsonPathAdapter: 1. Register it explicitly on your backend instance: ```python - from rhosocial.activerecord.backend.impl.postgres import PostgresJsonPathAdapter + from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresJsonPathAdapter adapter = PostgresJsonPathAdapter() backend.adapter_registry.register(adapter, PostgresJsonPath, str) ``` @@ -89,7 +89,8 @@ class PostgresJsonPathAdapter: 2. Specify it directly when executing queries: ```python - from rhosocial.activerecord.backend.impl.postgres import PostgresJsonPath, PostgresJsonPathAdapter + from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresJsonPath + from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresJsonPathAdapter path_adapter = PostgresJsonPathAdapter() result = backend.execute( "SELECT * FROM data WHERE content @? %s", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py index b41404e0..3044b67f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/monetary.py @@ -35,7 +35,7 @@ class PostgresMoneyAdapter: To use with Decimal values: ```python - from rhosocial.activerecord.backend.impl.postgres import PostgresMoney + from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresMoney # Wrap Decimal in PostgresMoney money = PostgresMoney(Decimal('100.50')) ``` diff --git a/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py b/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py index f1662f53..8b05fee6 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/adapters/xml.py @@ -35,14 +35,15 @@ class PostgresXMLAdapter: 1. Register it explicitly on your backend instance: ```python - from rhosocial.activerecord.backend.impl.postgres import PostgresXMLAdapter + from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresXMLAdapter adapter = PostgresXMLAdapter() backend.adapter_registry.register(adapter, PostgresXML, str) ``` 2. Specify it directly when executing queries: ```python - from rhosocial.activerecord.backend.impl.postgres import PostgresXML, PostgresXMLAdapter + from rhosocial.activerecord.backend.impl.postgres.type_values import PostgresXML + from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresXMLAdapter xml_adapter = PostgresXMLAdapter() result = backend.execute( "INSERT INTO docs (content) VALUES (%s)", diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py index 1541c237..d4b84fae 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py @@ -175,7 +175,7 @@ def __init__(self, **kwargs: Any) -> None: # Initialize transaction manager (will use backend.execute()) self._transaction_manager = AsyncPostgresTransactionManager(self, self.logger) - self.log(logging.INFO, "AsyncPostgreSQLBackend initialized") + self.log(logging.INFO, "AsyncPostgresBackend initialized") def _create_introspector(self) -> Any: """Create and return an AsyncPostgreSQLIntrospector with an async executor.""" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py index bcba3f5f..25238c2b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py @@ -161,7 +161,7 @@ def __init__(self, **kwargs: Any) -> None: # Initialize transaction manager (will use backend.execute()) self._transaction_manager = PostgresTransactionManager(self, self.logger) - self.log(logging.INFO, "PostgreSQLBackend initialized") + self.log(logging.INFO, "PostgresBackend initialized") def _create_introspector(self) -> Any: """Create and return a SyncPostgreSQLIntrospector with a sync executor.""" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py index e3d3518d..c30a1e69 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/connection.py @@ -148,7 +148,7 @@ def resolve_connection_config_from_args(args): def create_backend(args): """Create, connect, and introspect a PostgreSQL backend from parsed args.""" - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend config = resolve_connection_config_from_args(args) backend = PostgresBackend(connection_config=config) backend.connect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py index b1a2122d..7a8e2c81 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/info.py @@ -206,7 +206,7 @@ def handle(args): named_conn = getattr(args, "named_connection", None) if named_conn or args.database: try: - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend config = resolve_connection_config_from_args(args) backend = PostgresBackend(connection_config=config) backend.connect() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py index d5d42a91..a64ccdcd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py @@ -12,7 +12,10 @@ from enum import Enum from typing import Any -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import ( + PostgresBackend, + AsyncPostgresBackend, +) from rhosocial.activerecord.backend.errors import ConnectionError, QueryError from .connection import add_connection_args, resolve_connection_config_from_args diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py index 93f3d166..eb436c9a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py @@ -4,7 +4,10 @@ named-expression requires connection arguments and output arguments. """ -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import ( + PostgresBackend, + AsyncPostgresBackend, +) from rhosocial.activerecord.backend.options import ExecutionOptions from .connection import create_connection_parent_parser, resolve_connection_config_from_args diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py index 3d89eb89..3c2ca7ff 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py @@ -3,7 +3,10 @@ from __future__ import annotations -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import ( + PostgresBackend, + AsyncPostgresBackend, +) from .connection import create_connection_parent_parser, resolve_connection_config_from_args from .output import create_provider diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py index 2df72470..fdf95c34 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py @@ -4,7 +4,10 @@ named-procedure requires connection arguments, output arguments, and --rich-ascii. """ -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import ( + PostgresBackend, + AsyncPostgresBackend, +) from .connection import create_connection_parent_parser, resolve_connection_config_from_args from .output import create_provider diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py index c3686f7e..91b10589 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py @@ -15,7 +15,7 @@ myapp.npg.monthly_report """ -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from .connection import create_connection_parent_parser, resolve_connection_config_from_args from .output import create_provider @@ -64,7 +64,7 @@ def disconnect(): def backend_async_factory(): nonlocal async_backend - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend config = resolve_connection_config_from_args(args) async_backend = AsyncPostgresBackend(connection_config=config) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py index 42e4aab3..5e46f062 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py @@ -9,7 +9,10 @@ import logging import sys -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import ( + PostgresBackend, + AsyncPostgresBackend, +) from rhosocial.activerecord.backend.errors import ConnectionError, QueryError from .connection import add_connection_args, resolve_connection_config_from_args diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py index 40bf8de0..fc9d8347 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py @@ -13,7 +13,10 @@ from enum import Enum from typing import Any -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import ( + PostgresBackend, + AsyncPostgresBackend, +) from rhosocial.activerecord.backend.errors import ConnectionError, QueryError from .connection import add_connection_args, resolve_connection_config_from_args diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py index ea715be8..fb4b1dcd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/connection/quickstart.py @@ -29,7 +29,7 @@ ColumnConstraintType, ColumnDefinition, ) -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType 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 ca5cb766..2cf85a23 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 @@ -8,7 +8,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, 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 5e0c1ea3..ac10af4b 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 @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py index 5fd1e5a0..2256fe52 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/create_table.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py index 759c709c..bc611ee0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/drop_table.py @@ -10,7 +10,7 @@ # SECTION: Setup # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( 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 6057a364..cbd91864 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 @@ -22,7 +22,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, @@ -45,7 +45,7 @@ ColumnConstraint, ColumnConstraintType, ) -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresAlterMaterializedViewExpression, PostgresChangeMaterializedViewOwnerAction, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py index 3a8fc5f4..2944e802 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/partition.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( DropTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py index cd371781..e342af32 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/unique_index.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py index 29ec005e..fc75c2d4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/ddl/view.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py index 896b65aa..8d5b9ccd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/delete/basic.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/address_standardizer.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/address_standardizer.py index dbcc571b..e1c1a8aa 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/address_standardizer.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/address_standardizer.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py index f8a8efdb..6b980e2d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/bloom.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gin.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gin.py index 2e1333fe..3d7a38d4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gin.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gin.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gist.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gist.py index b2201ee0..bec5324b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gist.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/btree_gist.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/citext.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/citext.py index 4d819c0d..93c9fcaa 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/citext.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/citext.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/cube.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/cube.py index 1a474e88..8ee6f04f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/cube.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/cube.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/earthdistance.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/earthdistance.py index 67b91a56..db9d472e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/earthdistance.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/earthdistance.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/fuzzystrmatch.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/fuzzystrmatch.py index 6adf5266..19de3671 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/fuzzystrmatch.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/fuzzystrmatch.py @@ -17,7 +17,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hstore.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hstore.py index 0b1a6d77..8115c7a7 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hstore.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hstore.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hypopg.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hypopg.py index 5c233e3a..829f6301 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hypopg.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/hypopg.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/intarray.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/intarray.py index c3fb44f6..63dadc7c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/intarray.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/intarray.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/ltree.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/ltree.py index 4cdd848d..d3bace83 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/ltree.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/ltree.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/orafce.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/orafce.py index 823fc37c..1b823337 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/orafce.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/orafce.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_cron.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_cron.py index 5c8d9b7d..848ad0f1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_cron.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_cron.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_partman.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_partman.py index 96f65570..65ab4f7e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_partman.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_partman.py @@ -19,7 +19,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_repack.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_repack.py index 68b6e1da..b3c81ebd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_repack.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_repack.py @@ -20,7 +20,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_stat_statements.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_stat_statements.py index ce19f1c4..f7817f42 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_stat_statements.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_stat_statements.py @@ -16,7 +16,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_surgery.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_surgery.py index 801456fe..5c107760 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_surgery.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_surgery.py @@ -18,7 +18,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py index 612284f6..06c9eb2a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_trgm.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_walinspect.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_walinspect.py index ab748414..91f6cc9a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_walinspect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pg_walinspect.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgaudit.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgaudit.py index 0452d6d7..920a72d9 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgaudit.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgaudit.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgcrypto.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgcrypto.py index 19ab5de4..7e55f039 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgcrypto.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgcrypto.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pglogical.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pglogical.py index 7840a516..1668005f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pglogical.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pglogical.py @@ -13,7 +13,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py index 10e92200..af6f2d19 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgrouting.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py index e20bffb5..fc0f88c1 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/pgvector.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis.py index 8b5543cc..ece4456b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis_raster.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis_raster.py index 9ce28c39..3026e3e0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis_raster.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/postgis_raster.py @@ -14,7 +14,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/tablefunc.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/tablefunc.py index 375c2029..c02d94e3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/tablefunc.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/tablefunc.py @@ -15,7 +15,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/uuid_ossp.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/uuid_ossp.py index 7b27544d..3ed5e65e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/uuid_ossp.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/extensions/uuid_ossp.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import ( PostgresConnectionConfig, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py index 9ccacaa9..42eaac9e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/batch.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py index fd3bfeb2..09c25c26 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/single.py @@ -10,7 +10,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py index 9d1c98f2..9e3378e8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/upsert.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py index 89c056ac..92240083 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/insert/with_returning.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py index f597f210..ea29824e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_expressions/order_expressions.py @@ -162,7 +162,8 @@ def prepare_orders_demo(backend) -> None: if __name__ == "__main__": import os - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, PostgresConnectionConfig + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( host=os.getenv("POSTGRES_HOST", "127.0.0.1"), diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_basic.py index 8effaa83..c2322992 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_basic.py @@ -19,7 +19,7 @@ import tempfile import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.migration import ( MigrationRunner, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_chain.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_chain.py index f2c4f07e..b09f2852 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_chain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_migrations/run_chain.py @@ -17,7 +17,7 @@ import tempfile import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.migration import ( MigrationRunner, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/order_workflow.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/order_workflow.py index 20db15c2..c2e6e9a4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/order_workflow.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/named_procedures/order_workflow.py @@ -135,7 +135,8 @@ def run(self, ctx: ProcedureContext) -> None: if __name__ == "__main__": import os - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, PostgresConnectionConfig + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.impl.postgres.examples.named_expressions.order_expressions import ( prepare_orders_demo, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py index dccd0fb3..236dc523 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/pgq/recursive_cte_graph_traversal.py @@ -19,7 +19,7 @@ # SECTION: Setup (reference only — creates tables and data) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py index e62a3234..abcd4eac 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/aggregate.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py index 06431c89..30f7c1a2 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/array_func.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py index d68b866e..de626b8b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/basic.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py index 691982b1..13812756 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/constants.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py index 0790ff44..31505509 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/cte.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py index 3b666510..7565f68c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/distinct.py @@ -10,7 +10,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py index e733712d..a945175a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/explain.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py index a8d68e30..947fb974 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/join.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py index 6f087cbf..c6159fab 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/pagination.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py index cc2f6f33..f9cc7b2a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/predicate.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py index 56231777..262a0a35 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/subquery.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py index a9b76948..8e59959a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/text_search.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py index d89b00cf..24a234f7 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/union.py @@ -10,7 +10,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py index 5df3faf4..1f97712c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py index 7a1ab16b..9b1731cb 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/query/window_functions.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig config = PostgresConnectionConfig( 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 index 3ee9ea7d..b2ba8fb5 100644 --- 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 @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, 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 index 86b9f629..48e98a9b 100644 --- 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 @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, 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 index 1c8598eb..55b64319 100644 --- 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 @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, 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 index 5e0e63de..25654c6a 100644 --- 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 @@ -17,7 +17,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression.statements import IdentityClause from rhosocial.activerecord.backend.expression import ( 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 c138e480..86413613 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 @@ -12,7 +12,7 @@ # ============================================================ import json import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, 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 index e6cc2703..55f3dcb3 100644 --- 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 @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, DropTableExpression, ColumnDefinition, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py index dba2aac5..157695db 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/sqlxml/standard_expressions.py @@ -41,11 +41,9 @@ xmlserialize, xmltable, ) -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresBackend, - PostgresConnectionConfig, - PostgresDialect, -) +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect SQLParam = Optional[Union[str, int, float, bool]] SQLParams = Tuple[SQLParam, ...] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py index 6eb5c9e3..7dd699ee 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/basic.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py index f5bda6ef..1e720f7a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/exclusive.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py index fc8cc9fa..33b46164 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/transaction/for_update.py @@ -12,7 +12,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py index 36ef2ff2..7aefe90a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/array_postgres84.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py index da478356..9479fad4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/json_basic.py @@ -6,7 +6,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py index 21c54946..7ce054a5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/types/uuid.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.expression import ( CreateTableExpression, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py b/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py index c2b32db9..73f56aef 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/examples/update/basic.py @@ -11,7 +11,7 @@ # SECTION: Setup (necessary for execution, reference only) # ============================================================ import os -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/__init__.py index e5de03c5..3b822a3d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/__init__.py @@ -42,7 +42,7 @@ Example:: - from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateIndexExpression, ) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/cluster.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/cluster.py index 3b0999e3..0e9c250a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/cluster.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/cluster.py @@ -36,7 +36,7 @@ class PostgresClusterExpression(BaseExpression): verbose: When True, report the table being clustered (``VERBOSE``). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = PostgresClusterExpression( ... dialect, table_name="orders", using_index="orders_pkey" diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py index 59ba7d03..b8a029bf 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/comment.py @@ -35,7 +35,7 @@ class PostgresCommentExpression(BaseExpression): schema: Schema name for the object. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Comment on a table >>> comment = PostgresCommentExpression( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/extension.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/extension.py index 2a1b99b1..05c53bd5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/extension.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/extension.py @@ -38,7 +38,7 @@ class PostgresCreateExtensionExpression(BaseExpression): cascade: Cascade to dependent extensions. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> create_ext = PostgresCreateExtensionExpression( ... dialect=dialect, @@ -94,7 +94,7 @@ class PostgresDropExtensionExpression(BaseExpression): restrict: Drop only if no dependent objects (RESTRICT). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> drop_ext = PostgresDropExtensionExpression( ... dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index.py index 5d5eb035..287c6265 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/index.py @@ -36,7 +36,7 @@ Example:: - from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresCreateIndexExpression, ) @@ -89,7 +89,7 @@ class PostgresAlterIndexExpression(BaseExpression): - Bulk tablespace move with ALL IN TABLESPACE Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Rename index >>> expr = PostgresAlterIndexExpression( @@ -153,7 +153,7 @@ class PostgresReindexExpression(BaseExpression): verbose: Print progress messages. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Reindex a specific index >>> reindex = PostgresReindexExpression( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py index 7145123d..5aae391d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/mv.py @@ -169,7 +169,7 @@ class PostgresRefreshMaterializedViewExpression(RefreshMaterializedViewExpressio concurrently: Alias for concurrent (backward compatibility). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Regular refresh >>> refresh = PostgresRefreshMaterializedViewExpression( @@ -277,7 +277,7 @@ class PostgresSetMaterializedViewPropertiesAction(MaterializedViewAlterAction): :class:`~....storage_parameters.PostgresStorageParameter`. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter + >>> from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter >>> action = PostgresSetMaterializedViewPropertiesAction( ... dialect, ... {PostgresStorageParameter.FILLFACTOR: 90, "autovacuum_enabled": "true"}, @@ -346,7 +346,7 @@ class PostgresAlterMaterializedViewExpression(BaseExpression): """Expression for ``ALTER MATERIALIZED VIEW``. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( ... PostgresAlterMaterializedViewExpression, ... PostgresRenameMaterializedViewAction, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/partition.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/partition.py index e087a908..6dd27f34 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/partition.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/partition.py @@ -74,7 +74,7 @@ class PartitionValue(BaseExpression): partition_type: Optional RANGE, LIST, or HASH context. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> PartitionValue(dialect=dialect, value=None).to_sql() ('NULL', ()) @@ -169,7 +169,7 @@ class PostgresCreatePartitionExpression(BaseExpression): if_not_exists: Add IF NOT EXISTS clause. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # RANGE partition >>> partition = PostgresCreatePartitionExpression( @@ -243,7 +243,7 @@ class PostgresDetachPartitionExpression(BaseExpression): finalize: Finalize concurrent detach (PG 14+). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> detach = PostgresDetachPartitionExpression( ... dialect=dialect, @@ -299,7 +299,7 @@ class PostgresAttachPartitionExpression(BaseExpression): UnsupportedFeatureError: If ``concurrently=True``. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> attach = PostgresAttachPartitionExpression( ... dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/policy.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/policy.py index 2feabfbd..3a4590ad 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/policy.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/policy.py @@ -91,7 +91,7 @@ class PostgresCreatePolicyExpression(BaseExpression): when ``command`` is SELECT or DELETE. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = PostgresCreatePolicyExpression( ... dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/rls_config.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/rls_config.py index 133fd743..03c75524 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/rls_config.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/rls_config.py @@ -57,7 +57,7 @@ class PostgresAlterTableRlsExpression(BaseExpression): ``ALWAYS`` has no meaning with ``DISABLE`` and raises an error. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = PostgresAlterTableRlsExpression( ... dialect, table_name="orders", mode=RlsConfigurationMode.ENABLE diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/statistics.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/statistics.py index 37f597df..a1ab1327 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/statistics.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/statistics.py @@ -39,7 +39,7 @@ class PostgresCreateStatisticsExpression(BaseExpression): if_not_exists: Add IF NOT EXISTS clause. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> # Create ndistinct statistics >>> stats = PostgresCreateStatisticsExpression( @@ -99,7 +99,7 @@ class PostgresDropStatisticsExpression(BaseExpression): if_exists: Add IF EXISTS clause (prevent error if not exists). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> drop = PostgresDropStatisticsExpression( ... dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/table_settings.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/table_settings.py index e5aaeb10..e15391a3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/table_settings.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/table_settings.py @@ -50,7 +50,7 @@ class PostgresAlterTableSettingsExpression(BaseExpression): ``logging_mode``. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = PostgresAlterTableSettingsExpression( ... dialect, table_name="orders", mode=LoggingMode.UNLOGGED diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/vacuum.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/vacuum.py index 95c3dc7c..5651d7e3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/vacuum.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/vacuum.py @@ -46,7 +46,7 @@ class PostgresVacuumExpression(BaseExpression): columns: Specific columns to analyze (PG 16+). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> vacuum = PostgresVacuumExpression( ... dialect=dialect, @@ -118,7 +118,7 @@ class PostgresAnalyzeExpression(BaseExpression): columns: Specific columns to analyze (PG 16+). Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> analyze = PostgresAnalyzeExpression( ... dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py index 29aa625f..4b0c24b4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/enum_.py @@ -44,7 +44,7 @@ class PostgresEnumType(BaseExpression): schema: Optional schema name Examples: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> status_enum = PostgresEnumType( ... dialect=dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/citext.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/citext.py index b8402df4..7e611f88 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/citext.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/citext.py @@ -79,7 +79,7 @@ def citext_literal( BaseExpression with ::citext type cast applied Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> citext_literal(dialect, 'Hello World') # Produces: 'Hello World'::citext diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/cube.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/cube.py index ab1f5033..6d440e47 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/cube.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/cube.py @@ -92,7 +92,7 @@ def cube_literal( FunctionCall for cube(ARRAY[coordinates]) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> cube_literal(dialect, [1.0, 2.0, 3.0]) # Generates SQL: cube(ARRAY[1.0, 2.0, 3.0]) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/data_type.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/data_type.py index edef188e..cbef72fc 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/data_type.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/data_type.py @@ -36,7 +36,7 @@ def xid8_literal( BaseExpression with ::xid8 type cast applied Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> xid8_literal(dialect, 123456789) # Produces: 123456789::xid8 @@ -67,7 +67,7 @@ def array_literal( BaseExpression representing the array literal Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> array_literal(dialect, [1, 2, 3], 'int') # Produces parameterized: '{1,2,3}'::int[] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/earthdistance.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/earthdistance.py index 4ab6af4e..354c1cef 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/earthdistance.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/earthdistance.py @@ -81,7 +81,7 @@ def ll_to_earth( FunctionCall for ll_to_earth(latitude, longitude) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> ll_to_earth(dialect, 40.7128, -74.0060) # Generates: ll_to_earth(40.7128, -74.006) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/fuzzystrmatch.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/fuzzystrmatch.py index 4dab00e9..0ffdd6a5 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/fuzzystrmatch.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/fuzzystrmatch.py @@ -77,7 +77,7 @@ def levenshtein( FunctionCall for levenshtein(source, target) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> levenshtein(dialect, 'name_col', 'search_term') """ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/intarray.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/intarray.py index c64ba3c5..adc54173 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/intarray.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/intarray.py @@ -89,7 +89,7 @@ def intarray_contains( BinaryExpression for column @> values Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> intarray_contains(dialect, 'tags', '{1,2}') # Generates SQL: tags @> '{1,2}' diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/ltree.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/ltree.py index b72279c3..3406cdb8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/ltree.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/ltree.py @@ -93,7 +93,7 @@ def ltree_literal( Literal expression for the ltree path Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> ltree_literal(dialect, 'Top.Science.Astronomy') """ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_cron.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_cron.py index cab407bf..aaede880 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_cron.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_cron.py @@ -90,7 +90,7 @@ def cron_schedule( FunctionCall for cron_schedule(schedule, command[, comment]) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> cron_schedule(dialect, '0 * * * *', 'DELETE FROM logs WHERE created < now() - interval ''7 days''') >>> cron_schedule(dialect, '30 3 * * *', 'VACUUM ANALYZE', 'Nightly vacuum') diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_stat_statements.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_stat_statements.py index f5773b90..2ec5e5fd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_stat_statements.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_stat_statements.py @@ -63,7 +63,7 @@ def pg_stat_statements_reset( FunctionCall for pg_stat_statements_reset() Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> pg_stat_statements_reset(dialect) # Generates: pg_stat_statements_reset() diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_trgm.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_trgm.py index 6df5882e..7138f136 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_trgm.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pg_trgm.py @@ -84,7 +84,7 @@ def similarity( FunctionCall for similarity(text1, text2) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> similarity(dialect, 'name_col', 'search_term') >>> # Generates: similarity(name_col, 'search_term') @@ -119,7 +119,7 @@ def word_similarity( FunctionCall for word_similarity(column, query) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> word_similarity(dialect, 'title_col', 'search') >>> # Generates: word_similarity(title_col, 'search') @@ -151,7 +151,7 @@ def show_trgm( FunctionCall for show_trgm(text) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> show_trgm(dialect, 'hello') >>> # Generates: show_trgm('hello') @@ -189,7 +189,7 @@ def similarity_operator( BinaryExpression for column % text (or column !% text if negate) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> similarity_operator(dialect, 'name_col', 'search') >>> # Generates: name_col % 'search' diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgaudit.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgaudit.py index 2d677e74..a21ad855 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgaudit.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgaudit.py @@ -88,7 +88,7 @@ def pgaudit_set_role( FunctionCall for set_config('pgaudit.role', role, false) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> pgaudit_set_role(dialect, 'audit_role') # Generates: set_config('pgaudit.role', 'audit_role', false) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgcrypto.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgcrypto.py index 0e4a27b3..3462cf4f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgcrypto.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgcrypto.py @@ -82,7 +82,7 @@ def gen_salt( FunctionCall for gen_salt(algorithm) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> gen_salt(dialect, 'bf') >>> gen_salt(dialect) # uses md5 diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pglogical.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pglogical.py index e0a2123e..2c918f19 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pglogical.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pglogical.py @@ -82,7 +82,7 @@ def pglogical_create_node( FunctionCall for pglogical.create_node(node_name, dsn) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> pglogical_create_node(dialect, 'provider_node', 'host=127.0.0.1 dbname=mydb') """ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgrouting.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgrouting.py index a32d08fc..079e9288 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgrouting.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgrouting.py @@ -89,7 +89,7 @@ def pgr_dijkstra( FunctionCall for pgr_dijkstra(edges_sql, start_vid, end_vid[, directed]) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> pgr_dijkstra( ... dialect, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py index 96bab870..30eb0cf8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/pgvector.py @@ -90,7 +90,7 @@ def vector_l2_distance( BinaryArithmeticExpression for the distance calculation Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> d = PostgresDialect() >>> expr = vector_l2_distance(d, "embedding", [1.0, 2.0, 3.0]) """ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis_raster.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis_raster.py index a4b9f9f7..f3587d69 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis_raster.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/postgis_raster.py @@ -92,7 +92,7 @@ def st_rast_from_hexwkb( FunctionCall for ST_SetSRID(ST_RastFromHexWKB(raster_data), srid) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> st_rast_from_hexwkb(dialect, '0100000...', srid=4326) # Generates: ST_SetSRID(ST_RastFromHexWKB('0100000...'), 4326) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py index bd78bc94..e45ce036 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/range.py @@ -69,7 +69,7 @@ def range_contains( BinaryExpression for range @> element Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> range_contains(dialect, 'int4range_col', 5) """ diff --git a/src/rhosocial/activerecord/backend/impl/postgres/functions/tablefunc.py b/src/rhosocial/activerecord/backend/impl/postgres/functions/tablefunc.py index 31452d75..d701431c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/functions/tablefunc.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/functions/tablefunc.py @@ -94,7 +94,7 @@ def crosstab( FunctionCall for crosstab(source_sql[, categories_sql]) Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect() >>> crosstab(dialect, 'SELECT row_name, category, value FROM data') >>> crosstab( diff --git a/src/rhosocial/activerecord/backend/impl/postgres/introspection/status_introspector.py b/src/rhosocial/activerecord/backend/impl/postgres/introspection/status_introspector.py index 5964c3f7..454e69a8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/introspection/status_introspector.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/introspection/status_introspector.py @@ -245,7 +245,7 @@ class SyncPostgreSQLStatusIntrospector( Usage:: - backend = PostgreSQLBackend(connection_config=config) + backend = PostgresBackend(connection_config=config) backend.connect() status = backend.introspector.status.get_overview() print(status.server_version) @@ -839,7 +839,7 @@ class AsyncPostgreSQLStatusIntrospector( Usage:: - backend = AsyncPostgreSQLBackend(connection_config=config) + backend = AsyncPostgresBackend(connection_config=config) await backend.connect() status = await backend.introspector.status.get_overview() print(status.server_version) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/named_expressions/partition.py b/src/rhosocial/activerecord/backend/impl/postgres/named_expressions/partition.py index 2da88248..9cc51fa3 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/named_expressions/partition.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/named_expressions/partition.py @@ -55,7 +55,7 @@ def create_next_monthly_partition( ValueError: If reference_date is not a valid date string. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = create_next_monthly_partition( ... dialect, "orders", reference_date="2026-06-01") @@ -128,7 +128,7 @@ def create_range_partition_for_month( ValueError: If month is not in 1..12. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = create_range_partition_for_month(dialect, "orders", 2026, 6) >>> sql, _ = expr.to_sql() @@ -186,7 +186,7 @@ def create_range_partition_for_quarter( ValueError: If quarter is not in 1..4. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> expr = create_range_partition_for_quarter(dialect, "orders", 2026, 1) >>> sql, _ = expr.to_sql() @@ -253,7 +253,7 @@ def create_range_partitions_for_interval( if date range is invalid. Example: - >>> from rhosocial.activerecord.backend.impl.postgres import PostgresDialect + >>> from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect >>> dialect = PostgresDialect((14, 0, 0)) >>> exprs = create_range_partitions_for_interval( ... dialect, "orders", "2026-01-01", "2026-03-01") diff --git a/tests/benchmark/backend/conftest.py b/tests/benchmark/backend/conftest.py index 58825d1c..012a7ae4 100644 --- a/tests/benchmark/backend/conftest.py +++ b/tests/benchmark/backend/conftest.py @@ -70,7 +70,7 @@ def pytest_configure(config): @pytest.fixture(scope="function", params=SCENARIO_PARAMS) def postgres_backend_sync_context(request, benchmark_size): - from rhosocial.activerecord.backend.impl.postgres import PostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend scenario = request.param _, config = get_scenario(scenario) @@ -96,7 +96,7 @@ def postgres_backend_sync_context(request, benchmark_size): @pytest.fixture(scope="function", params=SCENARIO_PARAMS) def postgres_backend_async_context(request, benchmark_size): - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend scenario = request.param _, config = get_scenario(scenario) diff --git a/tests/providers/basic.py b/tests/providers/basic.py index bcd7be81..913ef60a 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -395,7 +395,7 @@ async def get_dialect(self, scenario_name: str = "default"): return super().get_dialect(scenario_name) async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend _, config = get_scenario(scenario_name) await model_class.configure(config, AsyncPostgresBackend) backend_instance = model_class.__backend__ diff --git a/tests/providers/basic_connection.py b/tests/providers/basic_connection.py index 36aefaf8..c81dc941 100644 --- a/tests/providers/basic_connection.py +++ b/tests/providers/basic_connection.py @@ -8,7 +8,7 @@ from typing import Type, Tuple, Optional, List from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig # noqa: F401 from rhosocial.activerecord.connection.pool import BackendPool, AsyncBackendPool, PoolConfig from rhosocial.activerecord.backend.options import ExecutionOptions @@ -103,7 +103,7 @@ def setup_sync_pool_and_model(self, scenario_name: str) -> Tuple[BackendPool, Ty async def setup_async_pool_and_model(self, scenario_name: str) -> Tuple[AsyncBackendPool, Type[AsyncActiveRecord]]: """Setup async connection pool and model for context tests.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend _, config = get_scenario(scenario_name) diff --git a/tests/providers/crud_benchmark.py b/tests/providers/crud_benchmark.py index 675a8d14..874b39c7 100644 --- a/tests/providers/crud_benchmark.py +++ b/tests/providers/crud_benchmark.py @@ -128,7 +128,7 @@ def _setup_model(self, model_class: Type[ActiveRecord], scenario: str) -> Type[A async def _setup_async_model( self, model_class: Type[ActiveRecord], scenario: str ) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend _, config = get_scenario(scenario) await model_class.configure(config, AsyncPostgresBackend) diff --git a/tests/providers/events.py b/tests/providers/events.py index b53f7499..8f570c27 100644 --- a/tests/providers/events.py +++ b/tests/providers/events.py @@ -159,7 +159,7 @@ def __init__(self): self._active_async_backends = [] async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend _, config = get_scenario(scenario_name) await model_class.configure(config, AsyncPostgresBackend) backend_instance = model_class.__backend__ diff --git a/tests/providers/fastapi_benchmark.py b/tests/providers/fastapi_benchmark.py index db0ae072..9be27347 100644 --- a/tests/providers/fastapi_benchmark.py +++ b/tests/providers/fastapi_benchmark.py @@ -143,7 +143,7 @@ async def teardown_benchmark_async( async def _setup_async_model( self, model_class: Type[ActiveRecord], scenario: str ) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend _, config = get_scenario(scenario) await model_class.configure(config, AsyncPostgresBackend) @@ -158,7 +158,7 @@ async def _initialize_schema_async(self, backend): await backend.execute(self._schema_sql(), options=options) def _create_async_context_factory(self, config): - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend @asynccontextmanager async def context_factory(): @@ -177,7 +177,7 @@ def _fastapi_runtime_config(self, connection_strategy): ) from exc async def _create_async_pool(self, config, runtime_config): - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend from rhosocial.activerecord.connection.pool import AsyncBackendPool, PoolConfig pool_config = PoolConfig( diff --git a/tests/providers/fixtures/basic.py b/tests/providers/fixtures/basic.py index fec5d58f..1443a91f 100644 --- a/tests/providers/fixtures/basic.py +++ b/tests/providers/fixtures/basic.py @@ -38,7 +38,7 @@ TimestampTzType, VarCharType, ) -from rhosocial.activerecord.backend.impl.postgres import ( +from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresSerialType, PostgresUUIDType, PostgresByteaType, diff --git a/tests/providers/fixtures/events.py b/tests/providers/fixtures/events.py index 3e942487..a31e189c 100644 --- a/tests/providers/fixtures/events.py +++ b/tests/providers/fixtures/events.py @@ -20,9 +20,7 @@ TimestampTzType, VarCharType, ) -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresSerialType, -) +from rhosocial.activerecord.backend.impl.postgres.expression import PostgresSerialType # --------------------------------------------------------------------------- diff --git a/tests/providers/fixtures/mixins.py b/tests/providers/fixtures/mixins.py index 9b706da8..a70e53a7 100644 --- a/tests/providers/fixtures/mixins.py +++ b/tests/providers/fixtures/mixins.py @@ -22,9 +22,7 @@ TimestampTzType, VarCharType, ) -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresSerialType, -) +from rhosocial.activerecord.backend.impl.postgres.expression import PostgresSerialType # --------------------------------------------------------------------------- diff --git a/tests/providers/fixtures/query.py b/tests/providers/fixtures/query.py index 2f0fbdec..61c423ab 100644 --- a/tests/providers/fixtures/query.py +++ b/tests/providers/fixtures/query.py @@ -25,9 +25,7 @@ TimestampTzType, VarCharType, ) -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresSerialType, -) +from rhosocial.activerecord.backend.impl.postgres.expression import PostgresSerialType _CASCADE = ReferentialAction.CASCADE diff --git a/tests/providers/mixins.py b/tests/providers/mixins.py index 72afb884..9c49f9c4 100644 --- a/tests/providers/mixins.py +++ b/tests/providers/mixins.py @@ -183,7 +183,7 @@ def __init__(self): self._active_async_backends = [] async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend _, config = get_scenario(scenario_name) await model_class.configure(config, AsyncPostgresBackend) backend_instance = model_class.__backend__ diff --git a/tests/providers/partition.py b/tests/providers/partition.py index c6d082f6..dce8ed1d 100644 --- a/tests/providers/partition.py +++ b/tests/providers/partition.py @@ -16,7 +16,7 @@ PartitionStrategy, TruncateExpression, ) -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresAttachPartitionExpression, PostgresColumnDefinition, diff --git a/tests/providers/query.py b/tests/providers/query.py index 1e18c2a2..e7d87b95 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -468,7 +468,7 @@ def __init__(self): self._active_async_backends = [] async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str, shared_backend=None) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend _, config = get_scenario(scenario_name) if shared_backend is None: await model_class.configure(config, AsyncPostgresBackend) @@ -580,7 +580,7 @@ async def setup_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveR async def setup_order_item_model(self, scenario_name: str) -> Type[AsyncActiveRecord]: """Set up the composite-PK AsyncOrderItem model for the query feature tests.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType from providers.fixtures._common import drop_table from providers.fixtures.basic import TABLE_EXPRESSIONS as BASIC_EXPRS @@ -609,7 +609,7 @@ async def setup_schema_fixtures(self, scenario_name: str): AsyncSchemaOrder, ) - from rhosocial.activerecord.backend.impl.postgres import ( + from rhosocial.activerecord.backend.impl.postgres.backend import ( AsyncPostgresBackend, PostgresBackend, ) @@ -665,7 +665,7 @@ async def cleanup_after_test(self, scenario_name: str): async def setup_mixed_schema_fixtures(self, scenario_name: str): """(AsyncUser, AsyncOrder, AsyncMixedSchemaOrder) with orders also in SCHEMA_A.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend from rhosocial.activerecord.testsuite.feature.query.cross_schema.mixed_schema_models import ( AsyncMixedSchemaOrder, ) diff --git a/tests/providers/query_connection.py b/tests/providers/query_connection.py index 7fab8e23..dc86d651 100644 --- a/tests/providers/query_connection.py +++ b/tests/providers/query_connection.py @@ -8,7 +8,7 @@ from typing import Type, Tuple, Optional, List from rhosocial.activerecord.model import ActiveRecord, AsyncActiveRecord -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig # noqa: F401 from rhosocial.activerecord.connection.pool import BackendPool, AsyncBackendPool, PoolConfig from rhosocial.activerecord.backend.options import ExecutionOptions @@ -103,7 +103,7 @@ def setup_sync_pool_and_model(self, scenario_name: str) -> Tuple[BackendPool, Ty async def setup_async_pool_and_model(self, scenario_name: str) -> Tuple[AsyncBackendPool, Type[AsyncActiveRecord]]: """Setup async connection pool and model for query context tests.""" - from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend _, config = get_scenario(scenario_name) diff --git a/tests/providers/relation.py b/tests/providers/relation.py index f58fb93d..036e2d90 100644 --- a/tests/providers/relation.py +++ b/tests/providers/relation.py @@ -2,7 +2,7 @@ from typing import Dict, List, Tuple, Type, Set from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend from rhosocial.activerecord.backend.impl.postgres.adapters.json import PostgresJSONBAdapter from rhosocial.activerecord.testsuite.feature.relation.interfaces import IRelationSyncProvider, IRelationAsyncProvider from rhosocial.activerecord.testsuite.feature.relation.fixtures.models import ( diff --git a/tests/providers/scenarios.py b/tests/providers/scenarios.py index 2d1a04c4..621b6677 100644 --- a/tests/providers/scenarios.py +++ b/tests/providers/scenarios.py @@ -4,7 +4,7 @@ import os from dataclasses import replace from typing import Dict, Any, Tuple, Type -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.testsuite.core.pool import pooled_database_name diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py index 4d13f625..7b3bef06 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py @@ -7,7 +7,10 @@ import os from typing import Dict, Any, Tuple, Type # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import ( + PostgresBackend, + AsyncPostgresBackend, +) from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_default_schema.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_default_schema.py index 54e20116..ef0c49cb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_default_schema.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_default_schema.py @@ -10,7 +10,7 @@ Priority: config.default_schema > first entry of config.search_path > 'public'. """ -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.introspection import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py index 335b57e9..e0c6ea4a 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py @@ -14,11 +14,11 @@ import pytest -from rhosocial.activerecord.backend.impl.postgres import ( +from rhosocial.activerecord.backend.impl.postgres.backend import ( AsyncPostgresBackend, PostgresBackend, - PostgresConnectionConfig, ) +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig class FakeConnection: 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 9b285096..28b909bb 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 @@ -22,7 +22,7 @@ PartitionClause, PartitionStrategy, ) -from rhosocial.activerecord.backend.impl.dummy import DummyDialect +from rhosocial.activerecord.backend.impl.dummy.backend import DummyDialect from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PartitionValue, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py index 54aedf22..cdf0c31d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_enum_types.py @@ -11,7 +11,7 @@ from typing import Tuple # noqa: F401 from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression.enum_ import PostgresEnumType from rhosocial.activerecord.backend.impl.postgres.adapters import PostgresEnumAdapter from rhosocial.activerecord.backend.impl.postgres.protocols import PostgresEnumTypeSupport # noqa: F401 diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py index c4ae17bb..f41e6286 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_materialized_view_integration.py @@ -10,7 +10,7 @@ import pytest from rhosocial.activerecord.backend.errors import DatabaseError -from rhosocial.activerecord.backend.impl.postgres import PostgresStorageParameter +from rhosocial.activerecord.backend.impl.postgres.storage_parameters import PostgresStorageParameter from rhosocial.activerecord.backend.impl.postgres.expression.ddl import ( PostgresAlterMaterializedViewExpression, PostgresChangeMaterializedViewOwnerAction, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py index 5b781c50..31178d0d 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py @@ -16,16 +16,18 @@ TableExpression, ) from rhosocial.activerecord.backend.expression.serialization import deserialize, serialize -from rhosocial.activerecord.backend.impl.postgres import ( +from rhosocial.activerecord.backend.impl.postgres.backend import ( AsyncPostgresBackend, PostgresBackend, +) +from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCopyFromExpression, PostgresCopyLogVerbosity, PostgresCopyOnError, PostgresCopyToExpression, - PostgresDialect, PostgresRepackExpression, ) +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.mixins import ( PostgresCopyMixin, PostgresRepackMixin, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py index ab599fb5..80ff0cc9 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack_integration.py @@ -4,7 +4,7 @@ import pytest from rhosocial.activerecord.backend.expression import Column, Literal, QueryExpression, TableExpression -from rhosocial.activerecord.backend.impl.postgres import ( +from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCopyFromExpression, PostgresCopyToExpression, PostgresRepackExpression, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_policy.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_policy.py index d464e4dd..00439023 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_policy.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_policy.py @@ -18,7 +18,7 @@ from rhosocial.activerecord.backend.expression import Column, Literal from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression.ddl.policy import ( AlterPolicyMode, PostgresAlterPolicyExpression, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_column_definition.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_column_definition.py index 8cd07bf0..b0bff327 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_column_definition.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_postgres_column_definition.py @@ -5,7 +5,7 @@ from rhosocial.activerecord.backend.expression import ColumnCommentClause, ColumnDefinition from rhosocial.activerecord.backend.expression.types import TextType -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresColumnDefinition, PostgresColumnOptions, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_qualified_reference_context.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_qualified_reference_context.py index e2632a58..cc999f14 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_qualified_reference_context.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_qualified_reference_context.py @@ -25,10 +25,8 @@ import pytest -from rhosocial.activerecord.backend.impl.postgres import ( - PostgresBackend, - PostgresConnectionConfig, -) +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType from rhosocial.activerecord.base.field_proxy import FieldProxy diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py index 60a28b51..f232a2c7 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_type_casting.py @@ -11,7 +11,7 @@ import warnings from rhosocial.activerecord.backend.expression.core import Column, Literal, CastExpression -from rhosocial.activerecord.backend.impl.postgres import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.type_values.constants import ( MONEY, NUMERIC, FLOAT8, INTEGER, VARCHAR, TEXT, JSONB, TIMESTAMP, TIMESTAMPTZ, BOOLEAN, BIGINT, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_explain.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_explain.py index d35c6621..c494d6dd 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_explain.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_explain.py @@ -14,7 +14,7 @@ ) from rhosocial.activerecord.backend.expression import RawSQLExpression from rhosocial.activerecord.backend.expression.statements import ExplainOptions -from rhosocial.activerecord.backend.impl.postgres import ( +from rhosocial.activerecord.backend.impl.postgres.explain import ( PostgresExplainResult, PostgresExplainPlanLine, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py index a9e8b113..16cae9b8 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py @@ -1,6 +1,9 @@ import pytest -from rhosocial.activerecord.backend.impl.postgres import AsyncPostgresBackend, PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import ( + AsyncPostgresBackend, + PostgresBackend, +) from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.transaction import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py index 6f757709..87d126b3 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py @@ -23,7 +23,10 @@ import time import logging -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import ( + PostgresBackend, + AsyncPostgresBackend, +) logger = logging.getLogger(__name__) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_explain.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_explain.py index cc4480b7..7e34d2ec 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_explain.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_partition_explain.py @@ -33,7 +33,7 @@ TableConstraintType, ) from rhosocial.activerecord.backend.expression.types import BigIntType, TextType, TimestampType -from rhosocial.activerecord.backend.impl.postgres import PostgresExplainResult +from rhosocial.activerecord.backend.impl.postgres.explain import PostgresExplainResult from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCreatePartitionExpression, ) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py index 9f16564c..eb689c87 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_postgres_datetime_interval_explain_examples.py @@ -23,7 +23,7 @@ extract, ) from rhosocial.activerecord.backend.expression.query_parts import OrderByClause -from rhosocial.activerecord.backend.impl.postgres import PostgresExplainResult +from rhosocial.activerecord.backend.impl.postgres.explain import PostgresExplainResult from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py index 87b9d09a..dfb87254 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py @@ -2,7 +2,10 @@ import pytest import pytest_asyncio # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import ( + PostgresBackend, + AsyncPostgresBackend, +) class TestPostgresVersion: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py index 7935cb2a..4ef41f93 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py @@ -14,7 +14,10 @@ import pytest_asyncio import yaml -from rhosocial.activerecord.backend.impl.postgres import PostgresBackend, AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import ( + PostgresBackend, + AsyncPostgresBackend, +) from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.connection.pool import ( PoolConfig, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py b/tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py index 93b67fbd..20d03525 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/query/collation/test_collation_expression.py @@ -6,7 +6,8 @@ import pytest from rhosocial.activerecord.backend.expression import Column, Literal -from rhosocial.activerecord.backend.impl.postgres import PostgresCollation, PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect +from rhosocial.activerecord.backend.impl.postgres.collation import PostgresCollation @pytest.fixture From 5157d66d8977c64d4802a503687e575e319de26f Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 16:59:37 +0800 Subject: [PATCH 05/19] refactor(backend): keep both backend classes in impl..backend Move the sync and async classes into a backend/ package with the fixed layout: backend/backend.py and backend/async_backend.py. Eight backends kept them as two modules beside backend/, and sqlite and postgres used backend/sync.py, so the class locations differed three ways between backends. With the layout fixed the worker tasks derive the async class from the sync module's own package instead of searching for it, so the _find_async_backend helper is gone. --- .../impl/postgres/{backend => }/async_backend.py | 12 ++++++------ .../backend/impl/postgres/backend/__init__.py | 15 +++++---------- .../backend/impl/postgres/backend/sync.py | 2 +- .../postgres/{backend/base.py => backend_base.py} | 2 +- 4 files changed, 13 insertions(+), 18 deletions(-) rename src/rhosocial/activerecord/backend/impl/postgres/{backend => }/async_backend.py (99%) rename src/rhosocial/activerecord/backend/impl/postgres/{backend/base.py => backend_base.py} (99%) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py b/src/rhosocial/activerecord/backend/impl/postgres/async_backend.py similarity index 99% rename from src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py rename to src/rhosocial/activerecord/backend/impl/postgres/async_backend.py index d4b84fae..795fe2cc 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/async_backend.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py +# src/rhosocial/activerecord/backend/impl/postgres/async_backend.py """PostgreSQL asynchronous backend implementation. This module provides an asynchronous PostgreSQL implementation: @@ -51,11 +51,11 @@ ) from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.result import QueryResult -from ..config import PostgresConnectionConfig -from ..dialect import PostgresDialect -from .base import PostgresBackendMixin, PostgresConcurrencyMixin -from ..protocols import PostgresExtensionInfo -from ..transaction import AsyncPostgresTransactionManager +from .config import PostgresConnectionConfig +from .dialect import PostgresDialect +from .backend_base import PostgresBackendMixin, PostgresConcurrencyMixin +from .protocols import PostgresExtensionInfo +from .transaction import AsyncPostgresTransactionManager if TYPE_CHECKING: from ..expression import PostgresCopyFromExpression, PostgresCopyToExpression diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py index 723e1782..cb97b759 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py @@ -1,22 +1,17 @@ # src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py """PostgreSQL backend implementations. -This module provides PostgreSQL backend implementations for both -synchronous and asynchronous database operations. - -Exports: - - PostgresBackendMixin: Shared functionality mixin for sync/async backends - - PostgresBackend: Synchronous PostgreSQL backend - - AsyncPostgresBackend: Asynchronous PostgreSQL backend +This module provides the synchronous PostgreSQL backend implementation. The +async counterpart lives in ``impl.postgres.async_backend``; every backend +follows that layout, so the sync class is always at ``impl..backend`` +and the async class at ``impl..async_backend``. """ -from .base import PostgresBackendMixin +from ..backend_base import PostgresBackendMixin from .sync import PostgresBackend -from .async_backend import AsyncPostgresBackend __all__ = [ "PostgresBackendMixin", "PostgresBackend", - "AsyncPostgresBackend", ] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py index 25238c2b..0d8134dd 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py @@ -42,7 +42,7 @@ from rhosocial.activerecord.backend.result import QueryResult from ..config import PostgresConnectionConfig from ..dialect import PostgresDialect -from .base import PostgresBackendMixin, PostgresConcurrencyMixin +from ..backend_base import PostgresBackendMixin, PostgresConcurrencyMixin from ..protocols import PostgresExtensionInfo from ..transaction import PostgresTransactionManager from ..introspection import SyncPostgreSQLIntrospector diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py b/src/rhosocial/activerecord/backend/impl/postgres/backend_base.py similarity index 99% rename from src/rhosocial/activerecord/backend/impl/postgres/backend/base.py rename to src/rhosocial/activerecord/backend/impl/postgres/backend_base.py index 7abb647e..e916a345 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend_base.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/backend/base.py +# src/rhosocial/activerecord/backend/impl/postgres/backend_base.py """PostgreSQL backend shared functionality. This module provides the base mixin class for PostgreSQL backend implementations, From eaf96f0aed8021f48a1d7fa57bcabeede7710163 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 17:36:31 +0800 Subject: [PATCH 06/19] fix(backend): correct relative import depth inside backend/ The move into backend/ shifted every relative import by one level, and the function-local ones were missed, so impl.bigquery could not reach its adapters module. Resolve each target against the filesystem instead of counting dots. --- .../connection_pool_stress_test_async.py | 2 +- .../exp1_basic_multiprocess.py | 6 +- .../exp2_postgres_async_advantage.py | 6 +- .../exp4_partition_correct.py | 6 +- .../parallel_workers/setup_db.py | 6 +- docs/examples/chapter_10_fastapi/database.py | 2 +- .../impl/postgres/backend/async_backend.py | 921 ++++++++++++++++++ .../backend/impl/postgres/backend/backend.py | 900 +++++++++++++++++ .../backend/impl/postgres/backend/base.py | 416 ++++++++ .../backend/impl/postgres/cli/introspect.py | 6 +- .../impl/postgres/cli/named_expression.py | 6 +- .../impl/postgres/cli/named_migration.py | 6 +- .../impl/postgres/cli/named_procedure.py | 6 +- .../postgres/cli/named_procedure_graph.py | 4 +- .../backend/impl/postgres/cli/query.py | 6 +- .../backend/impl/postgres/cli/status.py | 6 +- tests/benchmark/backend/conftest.py | 4 +- tests/providers/basic.py | 4 +- tests/providers/basic_connection.py | 4 +- tests/providers/crud_benchmark.py | 4 +- tests/providers/events.py | 4 +- tests/providers/fastapi_benchmark.py | 12 +- tests/providers/mixins.py | 4 +- tests/providers/partition.py | 2 +- tests/providers/query.py | 16 +- tests/providers/query_connection.py | 4 +- tests/providers/relation.py | 2 +- .../feature/backend/conftest.py | 6 +- .../postgres/test_connection_config.py | 6 +- .../backend/postgres/test_pg19_copy_repack.py | 6 +- .../backend/test_backend_initialization.py | 6 +- .../backend/test_connection_resilience.py | 6 +- .../feature/backend/test_version.py | 6 +- .../feature/connection/conftest.py | 6 +- 34 files changed, 2319 insertions(+), 88 deletions(-) create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py create mode 100644 src/rhosocial/activerecord/backend/impl/postgres/backend/base.py diff --git a/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py b/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py index 8d8513f3..dce14a01 100644 --- a/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py +++ b/docs/examples/chapter_02_connection_pool/connection_pool_stress_test_async.py @@ -40,7 +40,7 @@ "python-activerecord-postgres", "src")) from rhosocial.activerecord.connection.pool import PoolConfig, AsyncBackendPool -from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend from rhosocial.activerecord.backend.options import ExecutionOptions from rhosocial.activerecord.backend.schema import StatementType diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py index 94c40c69..693e5ba9 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp1_basic_multiprocess.py @@ -38,10 +38,8 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres.backend import ( - AsyncPostgresBackend, - PostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config # noqa: E402 from models import AsyncComment, AsyncPost, AsyncUser, Comment, Post, User # noqa: E402 diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py index 45176458..4d14e29a 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp2_postgres_async_advantage.py @@ -39,10 +39,8 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres.backend import ( - AsyncPostgresBackend, - PostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config # noqa: E402 from models import AsyncPost, Post # noqa: E402 diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py b/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py index 3dead448..eb162419 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/exp4_partition_correct.py @@ -38,10 +38,8 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres.backend import ( - AsyncPostgresBackend, - PostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config # noqa: E402 from models import AsyncComment, AsyncPost, AsyncUser, Comment, Post, User # noqa: E402 diff --git a/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py b/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py index 61b0f469..4948d838 100644 --- a/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py +++ b/docs/examples/chapter_08_scenarios/parallel_workers/setup_db.py @@ -24,10 +24,8 @@ if _src not in sys.path: sys.path.insert(0, _src) -from rhosocial.activerecord.backend.impl.postgres.backend import ( - AsyncPostgresBackend, - PostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from config_loader import load_config, show_active_config # noqa: E402 from models import ( # noqa: E402 diff --git a/docs/examples/chapter_10_fastapi/database.py b/docs/examples/chapter_10_fastapi/database.py index 4eeb153d..c713ad74 100644 --- a/docs/examples/chapter_10_fastapi/database.py +++ b/docs/examples/chapter_10_fastapi/database.py @@ -28,7 +28,7 @@ from fastapi import Request # noqa: F401 from rhosocial.activerecord.connection.pool import PoolConfig, AsyncBackendPool -from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend from config_loader import load_config from models import AsyncUser, AsyncPost, AsyncComment diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py new file mode 100644 index 00000000..eac2ee4e --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py @@ -0,0 +1,921 @@ +# src/rhosocial/activerecord/backend/impl/postgres/async_backend.py +"""PostgreSQL asynchronous backend implementation. + +This module provides an asynchronous PostgreSQL implementation: +- PostgreSQL asynchronous backend with connection management and query execution +- PostgreSQL-specific connection configuration +- Type mapping and value conversion +- Transaction management with savepoint support +- PostgreSQL dialect and expression handling +- PostgreSQL-specific type definitions and mappings + +Architecture: +- AsyncPostgresBackend: Asynchronous implementation using psycopg +- Independent from ORM frameworks - uses only native drivers +""" + +import datetime +import logging +from typing import ( + Any, + AsyncIterable, + Dict, + Iterable, + List, + Optional, + Tuple, + TYPE_CHECKING, + Union, +) + +import psycopg +from psycopg import AsyncConnection +from psycopg.errors import Error as PsycopgError +from psycopg.errors import ( + IntegrityError as PsycopgIntegrityError, + OperationalError as PsycopgOperationalError, + ProgrammingError as PsycopgProgrammingError, + DeadlockDetected as PsycopgDeadlockError, +) + +from rhosocial.activerecord.backend.base import AsyncStorageBackend +from rhosocial.activerecord.backend.introspection.backend_mixin import IntrospectorBackendMixin +from rhosocial.activerecord.backend.explain import AsyncExplainBackendMixin +from rhosocial.activerecord.backend.errors import ( + ConnectionError, + DatabaseError, + DeadlockError, + IntegrityError, + OperationalError, + QueryError, +) +from rhosocial.activerecord.backend.options import ExecutionOptions +from rhosocial.activerecord.backend.result import QueryResult +from ..config import PostgresConnectionConfig +from ..dialect import PostgresDialect +from .base import PostgresBackendMixin, PostgresConcurrencyMixin +from ..protocols import PostgresExtensionInfo +from ..transaction import AsyncPostgresTransactionManager + +if TYPE_CHECKING: + from ..expression import PostgresCopyFromExpression, PostgresCopyToExpression + + +CopyChunk = Union[str, bytes, bytearray, memoryview] +AsyncCopyInput = Union[ + CopyChunk, + Iterable[CopyChunk], + AsyncIterable[CopyChunk], +] + + +class AsyncPostgresBackend( + IntrospectorBackendMixin, + PostgresBackendMixin, + AsyncExplainBackendMixin, + PostgresConcurrencyMixin, + AsyncStorageBackend, +): + """Asynchronous PostgreSQL-specific backend implementation.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize async PostgreSQL backend with connection configuration. + + Note: + The dialect is initialized with a default version (9.6.0) and no plugins + are enabled. Call introspect_and_adapt() after connecting to detect + the actual server version and installed extensions. + """ + graph_feature_overrides = kwargs.pop("graph_feature_overrides", None) + + # Ensure we have proper PostgreSQL configuration + connection_config = kwargs.get("connection_config") + + if connection_config is None: + # Extract PostgreSQL-specific parameters from kwargs + config_params = {} + pg_specific_params = [ + "host", + "port", + "database", + "username", + "password", + "charset", + "collation", + "timezone", + "version", + "pool_size", + "pool_timeout", + "pool_name", + "pool_reset_session", + "pool_pre_ping", + "ssl_ca", + "ssl_cert", + "ssl_key", + "ssl_verify_cert", + "ssl_verify_identity", + "log_queries", + "log_level", + "application_name", + "fallback_application_name", + "connect_timeout", + "options", + "service", + "target_session_attrs", + "gssencmode", + "channel_binding", + "replication", + "assume_role", + "role", + "search_path", + "row_security", + "datestyle", + "intervalstyle", + "timezone_setting", + "extra_float_digits", + "client_encoding", + "tcp_user_timeout", + "tcp_keepalives_idle", + "tcp_keepalives_interval", + "tcp_keepalives_count", + "load_balance_hosts", + "replication", + "keepalives", + "keepalives_idle", + "keepalives_interval", + "keepalives_count", + ] + + for param in pg_specific_params: + if param in kwargs: + config_params[param] = kwargs[param] + + # Set defaults if not provided + if "host" not in config_params: + config_params["host"] = "localhost" + if "port" not in config_params: + config_params["port"] = 5432 + + kwargs["connection_config"] = PostgresConnectionConfig(**config_params) + + # Initialize the base class first to ensure all base properties are set + super().__init__(**kwargs) + + # Initialize PostgreSQL-specific components + # Version is None until introspect_and_adapt() is called + self._dialect = PostgresDialect( + graph_feature_overrides=graph_feature_overrides, + ) + + # Register PostgreSQL-specific type adapters (same as sync backend) + # Note: XML adapter is NOT registered by default due to potential conflicts. + # See PostgresXMLAdapter documentation for details. + self._register_postgres_adapters() + + # Initialize transaction manager (will use backend.execute()) + self._transaction_manager = AsyncPostgresTransactionManager(self, self.logger) + + self.log(logging.INFO, "AsyncPostgresBackend initialized") + + def _create_introspector(self) -> Any: + """Create and return an AsyncPostgreSQLIntrospector with an async executor.""" + from rhosocial.activerecord.backend.introspection.executor import AsyncIntrospectorExecutor + from ..introspection import AsyncPostgreSQLIntrospector + + return AsyncPostgreSQLIntrospector(self, AsyncIntrospectorExecutor(self)) + + @property + def dialect(self) -> PostgresDialect: + """Get the PostgreSQL dialect instance.""" + return self._dialect + + @property + def transaction_manager(self) -> AsyncPostgresTransactionManager: + """Get the async PostgreSQL transaction manager.""" + return self._transaction_manager + + + async def connect(self) -> None: + """Establish async connection to PostgreSQL database.""" + try: + # Prepare connection parameters from config + conn_params = { + "host": self.config.host, + "port": self.config.port, + "dbname": self.config.database, + "user": self.config.username, + "password": self.config.password, + } + + # Add additional parameters if they exist in config + additional_params = [ + "application_name", + "fallback_application_name", + "connect_timeout", + "options", + "service", + "target_session_attrs", + "gssencmode", + "channel_binding", + "replication", + "assume_role", + "role", + "search_path", + "row_security", + "datestyle", + "intervalstyle", + "timezone", + "extra_float_digits", + "client_encoding", + "tcp_user_timeout", + "tcp_keepalives_idle", + "tcp_keepalives_interval", + "tcp_keepalives_count", + "load_balance_hosts", + "keepalives", + "keepalives_idle", + "keepalives_interval", + "keepalives_count", + ] + + for param in additional_params: + if hasattr(self.config, param): + value = getattr(self.config, param) + if value is not None: # Only add the parameter if it's not None + conn_params[param] = value + + # Handle 'options' parameter specially as it should be a string, not a dict + if hasattr(self.config, "options") and self.config.options is not None: + options_value = self.config.options + if isinstance(options_value, dict): + options_str = " ".join([f"-c {k}={v}" for k, v in options_value.items()]) + conn_params["options"] = options_str + else: + conn_params["options"] = options_value + + # Add SSL parameters if provided + ssl_params = {} + if hasattr(self.config, "ssl_ca"): + ssl_params["sslcert"] = self.config.ssl_ca + if hasattr(self.config, "ssl_cert"): + ssl_params["sslcert"] = self.config.ssl_cert + if hasattr(self.config, "ssl_key"): + ssl_params["sslkey"] = self.config.ssl_key + if hasattr(self.config, "ssl_mode"): + ssl_params["sslmode"] = self.config.ssl_mode + + if ssl_params: + conn_params.update(ssl_params) + + self._connection = await AsyncConnection.connect(**conn_params) + await self._connection.set_autocommit(True) # Disable psycopg auto-transaction management + + self.log( + logging.INFO, + f"Connected to PostgreSQL database: {self.config.host}:{self.config.port}/{self.config.database}", + ) + except PsycopgError as e: + self.log(logging.ERROR, f"Failed to connect to PostgreSQL database: {str(e)}") + raise ConnectionError(f"Failed to connect to PostgreSQL: {str(e)}") from e + + async def disconnect(self) -> None: + """Close async connection to PostgreSQL database.""" + if self._connection: + try: + # Rollback any active transaction + if self.in_transaction: + await self.transaction_manager.rollback() + + await self._connection.close() + self._connection = None + self.log(logging.INFO, "Disconnected from PostgreSQL database") + except PsycopgError as e: + self.log(logging.ERROR, f"Error during disconnection: {str(e)}") + raise OperationalError(f"Error during PostgreSQL disconnection: {str(e)}") from e + + async def _get_cursor(self) -> Any: + """Get a database cursor, ensuring connection is active. + + This method implements automatic connection health checking (Plan A): + - Checks if connection object exists + - Checks if connection is still valid using closed/broken attributes + - Automatically reconnects if connection was lost + """ + if not self._connection: + self.log(logging.DEBUG, "No connection, connecting...") + await self.connect() + elif self._connection.closed or self._connection.broken: + self.log(logging.DEBUG, "Connection lost, reconnecting...") + self._connection = None # Clear the closed/broken connection + await self.connect() + + return self._connection.cursor() + + @staticmethod + def _copy_output_chunk(chunk: Any) -> bytes: + if isinstance(chunk, bytes): + return chunk + if isinstance(chunk, str): + return chunk.encode("utf-8") + if isinstance(chunk, (bytearray, memoryview)): + return bytes(chunk) + raise TypeError("PostgreSQL COPY stream yielded a non-buffer chunk") + + @staticmethod + def _validate_copy_input_chunk( + chunk: Any, + binary: bool, + encoding: Optional[str] = None, + ) -> CopyChunk: + if not isinstance(chunk, (str, bytes, bytearray, memoryview)): + raise TypeError("COPY input chunks must be str or bytes-like objects") + if binary and isinstance(chunk, str): + raise TypeError("COPY FROM BINARY requires bytes-like input chunks") + if encoding is not None and isinstance(chunk, str): + raise TypeError("COPY FROM with ENCODING requires bytes-like input chunks") + return chunk + + async def copy_to(self, expression: "PostgresCopyToExpression") -> bytes: + """Execute COPY TO STDOUT and return the complete byte stream.""" + from ..expression import PostgresCopyToExpression + + if not isinstance(expression, PostgresCopyToExpression): + raise TypeError("expression must be a PostgresCopyToExpression") + sql, params = expression.to_sql() + chunks = [] + try: + async with await self._get_cursor() as cursor: + async with cursor.copy(sql, params) as copy: + async for chunk in copy: + chunks.append(self._copy_output_chunk(chunk)) + except BaseException as error: + if isinstance(error, PsycopgError): + await self._handle_error(error) + else: + await self._try_rollback_transaction() + raise + return b"".join(chunks) + + async def copy_from( + self, + expression: "PostgresCopyFromExpression", + data: AsyncCopyInput, + ) -> int: + """Stream COPY FROM STDIN data and return the server-reported row count.""" + from ..expression import PostgresCopyFromExpression + + if not isinstance(expression, PostgresCopyFromExpression): + raise TypeError("expression must be a PostgresCopyFromExpression") + sql, params = expression.to_sql() + binary = str(expression.format).lower() == "binary" + try: + async with await self._get_cursor() as cursor: + async with cursor.copy(sql, params) as copy: + if isinstance(data, (str, bytes, bytearray, memoryview)): + await copy.write( + self._validate_copy_input_chunk(data, binary, expression.encoding) + ) + elif hasattr(data, "__aiter__"): + async for chunk in data: + await copy.write( + self._validate_copy_input_chunk(chunk, binary, expression.encoding) + ) + else: + try: + chunks = iter(data) + except TypeError as error: + raise TypeError( + "COPY input must be bytes, str, or a sync/async iterable of chunks" + ) from error + for chunk in chunks: + await copy.write( + self._validate_copy_input_chunk(chunk, binary, expression.encoding) + ) + rowcount = int(cursor.rowcount) + if getattr(self, "_transaction_manager", None) is not None: + await self._handle_auto_commit_if_needed() + return rowcount + except BaseException as error: + if isinstance(error, PsycopgError): + await self._handle_error(error) + else: + await self._try_rollback_transaction() + raise + + async def execute_many(self, sql: str, params_list: List[Tuple]) -> QueryResult: + """Execute the same SQL statement multiple times with different parameters asynchronously.""" + if not self._connection: + await self.connect() + + cursor = None + start_time = datetime.datetime.now() + + try: + cursor = await self._get_cursor() + + # Log the batch operation if logging is enabled + if getattr(self.config, "log_queries", False): + self.log(logging.DEBUG, f"Executing batch operation: {sql}") + self.log(logging.DEBUG, f"With {len(params_list)} parameter sets") + + # Execute multiple statements + affected_rows = 0 + for params in params_list: + await cursor.execute(sql, params) + affected_rows += cursor.rowcount + + duration = (datetime.datetime.now() - start_time).total_seconds() + + result = QueryResult(affected_rows=affected_rows, data=None, duration=duration) + + self.log( + logging.INFO, + f"Batch operation completed, affected {affected_rows} rows, duration={duration:.3f}s", + ) + return result + + except PsycopgIntegrityError as e: + self.log(logging.ERROR, f"Integrity error in batch: {str(e)}") + raise IntegrityError(str(e)) from e + except PsycopgError as e: + self.log(logging.ERROR, f"PostgreSQL error in batch: {str(e)}") + raise DatabaseError(str(e)) from e + except Exception as e: + self.log(logging.ERROR, f"Unexpected error during batch execution: {str(e)}") + raise QueryError(str(e)) from e + finally: + if cursor: + await cursor.close() + + async def get_server_version(self) -> Optional[Tuple[int, int, int]]: + """Get PostgreSQL server version asynchronously. + + Returns: + Tuple of (major, minor, patch) version numbers, or None if version + cannot be determined. + """ + if not self._connection: + await self.connect() + + cursor = None + try: + cursor = await self._get_cursor() + await cursor.execute("SELECT version()") + version_row = await cursor.fetchone() + if not version_row or not version_row[0]: + self.log(logging.WARNING, "PostgreSQL version query returned no result") + return None + + version_str = version_row[0] + + # Extract version from string like "PostgreSQL 13.2..." + import re + + match = re.search(r"PostgreSQL (\d+)(?:\.(\d+))?(?:[^\d]*(\d+))?", version_str) + if match: + major = int(match.group(1)) + minor = int(match.group(2)) if match.group(2) else 0 + patch = int(match.group(3)) if match.group(3) else 0 + return (major, minor, patch) + else: + self.log(logging.WARNING, f"Could not parse PostgreSQL version: {version_str}") + return None + except Exception as e: + self.log(logging.WARNING, f"Could not determine PostgreSQL version: {str(e)}") + return None + finally: + if cursor: + await cursor.close() + + async def introspect_and_adapt(self) -> None: + """Introspect backend and adapt backend instance to actual server capabilities. + + This method ensures a connection exists, queries the actual PostgreSQL server version + and installed extensions, and updates the backend's internal state. + + Introspection includes: + 1. Server version: affects version-dependent feature support checks + 2. Installed extensions: queries pg_extension system table to determine plugin availability + + After detection, the dialect instance caches extension information, + which can be queried via is_extension_installed(). + + Note: If version cannot be determined, the default version (9.6.0) is retained. + """ + # Ensure connection exists + if not self._connection: + await self.connect() + + # Get server version + actual_version = await self.get_server_version() + + # If version cannot be determined, keep the default version + if actual_version is None: + self.log(logging.WARNING, "Could not determine server version, retaining default (9.6.0)") + actual_version = (9, 6, 0) + + cached_version = getattr(self, "_server_version_cache", None) + version_changed = cached_version != actual_version + + if version_changed: + self._server_version_cache = actual_version + self._dialect = PostgresDialect( + actual_version, + graph_feature_overrides=getattr( + self._dialect, "_graph_feature_overrides", None + ), + ) + + # Detect installed extensions + extensions = await self._detect_extensions() + self._dialect._extensions = extensions + + # Log detected extensions + installed_exts = [f"{k}={v.version}" for k, v in extensions.items() if v.installed] + ext_info = ", ".join(installed_exts) if installed_exts else "none" + + if version_changed: + self.log(logging.INFO, f"Adapted to PostgreSQL {actual_version}, extensions: {ext_info}") + else: + self.log(logging.DEBUG, f"Extensions detected: {ext_info}") + + async def _detect_extensions(self) -> Dict[str, PostgresExtensionInfo]: + """Detect installed and available extensions.""" + async with self._connection.cursor() as cursor: + await cursor.execute(PostgresDialect.SQL_INSTALLED_EXTENSIONS) + installed_rows = await cursor.fetchall() + + await cursor.execute(PostgresDialect.SQL_AVAILABLE_EXTENSIONS) + available_rows = await cursor.fetchall() + + return PostgresDialect.build_extension_map(installed_rows, available_rows) + + async def ping(self, reconnect: bool = True) -> bool: + """ + Ping the PostgreSQL server to check if the async connection is alive. + + Args: + reconnect: If True, attempt to reconnect if the connection is dead. + If False, just return the current connection status. + + Returns: + True if the connection is alive (or was successfully reconnected), + False if the connection is dead and reconnect is False or reconnection failed. + """ + try: + if not self._connection: + if reconnect: + await self.connect() + return True + else: + return False + + # Check connection status without triggering auto-reconnect + if self._connection.closed or self._connection.broken: + if reconnect: + self._connection = None + await self.connect() + return True + else: + return False + + # Execute a simple query to verify connection is actually working + cursor = await self._get_cursor() + await cursor.execute("SELECT 1") + await cursor.fetchone() + await cursor.close() + + return True + except psycopg.Error as e: + self.log(logging.WARNING, f"PostgreSQL async connection ping failed: {str(e)}") + if reconnect: + try: + self._connection = None + await self.connect() + return True + except Exception as connect_error: + self.log(logging.ERROR, f"Failed to reconnect after ping failure: {str(connect_error)}") + return False + return False + + async def _reconnect(self) -> bool: + """ + Attempt to reconnect to the PostgreSQL server. + + Returns: + True if reconnection was successful, False otherwise + """ + try: + self._connection = None + await self.connect() + return True + except Exception as e: + self.log(logging.ERROR, f"Failed to reconnect: {str(e)}") + return False + + async def execute( + self, + sql: str, + params: Optional[Tuple] = None, + *, + options: Optional[ExecutionOptions] = None, + max_retries: int = 2, + **kwargs: Any, + ) -> QueryResult: + """ + Execute a SQL statement with optional parameters asynchronously. + + This method wraps the base execute method with automatic reconnection + logic for connection errors (Plan B: Error Retry Mechanism). + + Args: + sql: SQL statement to execute + params: Optional parameters for the SQL statement + options: Execution options + max_retries: Maximum number of retry attempts for connection errors + **kwargs: Additional keyword arguments + + Returns: + QueryResult containing the query results + """ + from rhosocial.activerecord.backend.options import StatementType + + # If no options provided, create default options from kwargs + if options is None: + # Determine statement type based on SQL + sql_upper = sql.strip().upper() + if sql_upper.startswith(("SELECT", "WITH", "SHOW", "DESCRIBE", "EXPLAIN")): + stmt_type = StatementType.DQL + elif sql_upper.startswith(("INSERT", "UPDATE", "DELETE", "UPSERT")): + stmt_type = StatementType.DML + else: + stmt_type = StatementType.DDL + + # Extract column_mapping and column_adapters from kwargs if present + column_mapping = kwargs.get("column_mapping") + column_adapters = kwargs.get("column_adapters") + + options = ExecutionOptions( + stmt_type=stmt_type, + process_result_set=None, + column_adapters=column_adapters, + column_mapping=column_mapping, + ) + else: + # If options is provided but column_mapping or column_adapters are explicitly passed in kwargs, + # update the options with these values + if "column_mapping" in kwargs: + options.column_mapping = kwargs["column_mapping"] + if "column_adapters" in kwargs: + options.column_adapters = kwargs["column_adapters"] + + # Retry loop for connection errors (Plan B) + # We catch the framework's OperationalError because the original PsycopgOperationalError + # has already been converted by _handle_error() in the base class. + for attempt in range(max_retries + 1): + try: + return await super().execute(sql, params, options=options) + except OperationalError as e: + error_msg = str(e) + if self._is_connection_error_message(error_msg) and attempt < max_retries: + self.log(logging.WARNING, + f"Connection error on attempt {attempt + 1}, retrying... Error: {error_msg}") + if await self._reconnect(): + continue + # If reconnection fails, break and raise the error + self.log(logging.ERROR, "Failed to reconnect after connection error") + break + raise + + async def _handle_error(self, error: Exception) -> None: + """Handle PostgreSQL-specific errors asynchronously.""" + error_msg = str(error) + + # Try to rollback transaction to recover from aborted state + # PostgreSQL requires explicit ROLLBACK after errors in a transaction + await self._try_rollback_transaction() + + if isinstance(error, PsycopgIntegrityError): + if "duplicate key value violates unique constraint" in error_msg.lower(): + self.log(logging.ERROR, f"Unique constraint violation: {error_msg}") + raise IntegrityError(f"Unique constraint violation: {error_msg}") from error + elif "violates foreign key constraint" in error_msg.lower(): + self.log(logging.ERROR, f"Foreign key constraint violation: {error_msg}") + raise IntegrityError(f"Foreign key constraint violation: {error_msg}") from error + self.log(logging.ERROR, f"Integrity error: {error_msg}") + raise IntegrityError(error_msg) from error + elif isinstance(error, PsycopgOperationalError): + if "deadlock detected" in error_msg.lower(): + self.log(logging.ERROR, f"Deadlock error: {error_msg}") + raise DeadlockError(error_msg) from error + self.log(logging.ERROR, f"Operational error: {error_msg}") + raise OperationalError(error_msg) from error + elif isinstance(error, PsycopgProgrammingError): + self.log(logging.ERROR, f"Programming error: {error_msg}") + raise QueryError(error_msg) from error + elif isinstance(error, PsycopgDeadlockError): + self.log(logging.ERROR, f"Deadlock error: {error_msg}") + raise DeadlockError(error_msg) from error + elif isinstance(error, PsycopgError): + self.log(logging.ERROR, f"PostgreSQL error: {error_msg}") + raise DatabaseError(error_msg) from error + else: + self.log(logging.ERROR, f"Unexpected error: {error_msg}") + raise error + + async def _try_rollback_transaction(self) -> None: + """Attempt to rollback transaction to recover from aborted state asynchronously. + + PostgreSQL requires explicit ROLLBACK after query errors in a transaction. + This method attempts to recover by rolling back the transaction, regardless + of the transaction manager's state. + """ + try: + # First, try the transaction manager if active + if self.in_transaction: + self.log(logging.INFO, "Attempting to rollback transaction after error") + await self._transaction_manager.rollback() + + # Also try direct connection rollback to handle edge cases + # This ensures we recover from aborted transaction state + if self._connection and not self._connection.closed: + try: + await self._connection.rollback() + self.log(logging.INFO, "Direct connection rollback completed") + except Exception as direct_error: + self.log(logging.DEBUG, f"Direct rollback not needed or failed: {direct_error}") + except Exception as rollback_error: + self.log(logging.WARNING, f"Failed to rollback transaction: {rollback_error}") + + async def _handle_auto_commit(self) -> None: + """Handle auto commit based on PostgreSQL connection and transaction state asynchronously. + + This method will commit the current connection if: + 1. The connection exists and is open + 2. There is no active transaction managed by transaction_manager + + It's used by insert/update/delete operations to ensure changes are + persisted immediately when auto_commit=True is specified. + """ + try: + # Check if connection exists + if not self._connection: + return + + # Check if we're not in an active transaction + if not self.in_transaction: + # For PostgreSQL, if autocommit is disabled, we need to commit explicitly + if not self.config.autocommit: + await self._connection.commit() + self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") + except Exception as e: + # Just log the error but don't raise - this is a convenience feature + self.log(logging.WARNING, f"Failed to auto-commit: {str(e)}") + + async def _handle_auto_commit_if_needed(self) -> None: + """ + Handle auto-commit for PostgreSQL asynchronously. + + PostgreSQL respects the autocommit setting, but we also need to handle explicit commits. + """ + if not self.in_transaction and self._connection: + if not self.config.autocommit: + await self._connection.commit() + self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") + + # region Advisory Lock Methods + + async def execute_advisory_lock( + self, + key, + shared: bool = False, + session: bool = True + ) -> None: + """ + Acquire an advisory lock asynchronously. + + This method blocks until the lock is acquired. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, acquire shared lock; otherwise exclusive lock + session: If True (default), session-level lock; otherwise transaction-level + + Raises: + DatabaseError: If the lock operation fails + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryLockExpression + + expr = PostgresAdvisoryLockExpression(self.dialect, key=key, shared=shared, session=session) + sql, params = expr.to_sql() + await self.execute(sql, params) + + async def execute_advisory_unlock(self, key, shared: bool = False) -> bool: + """ + Release an advisory lock asynchronously. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, release shared lock; otherwise exclusive lock + + Returns: + True if the lock was released, False if it was not held + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryUnlockExpression + + expr = PostgresAdvisoryUnlockExpression(self.dialect, key=key, shared=shared) + sql, params = expr.to_sql() + result = await self.execute(sql, params) + # pg_advisory_unlock returns boolean in first column + if result.data and len(result.data) > 0: + row = result.data[0] + # Result is a dict with column name as key + return bool(list(row.values())[0]) + return False + + async def execute_advisory_unlock_all(self) -> None: + """ + Release all advisory locks held by the current session asynchronously. + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryUnlockAllExpression + + expr = PostgresAdvisoryUnlockAllExpression(self.dialect) + sql, params = expr.to_sql() + await self.execute(sql, params) + + async def try_advisory_lock( + self, + key, + shared: bool = False, + session: bool = True + ) -> bool: + """ + Non-blocking advisory lock acquisition asynchronously. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, acquire shared lock; otherwise exclusive lock + session: If True (default), session-level lock; otherwise transaction-level + + Returns: + True if the lock was acquired, False if it was not available + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresTryAdvisoryLockExpression + + expr = PostgresTryAdvisoryLockExpression(self.dialect, key=key, shared=shared, session=session) + sql, params = expr.to_sql() + result = await self.execute(sql, params) + # pg_try_advisory_lock returns boolean in first column + if result.data and len(result.data) > 0: + row = result.data[0] + # Result is a dict with column name as key + return bool(list(row.values())[0]) + return False + + def advisory_lock( + self, + key, + shared: bool = False, + session: bool = True + ): + """ + Async context manager for advisory locks. + + Automatically acquires the lock on entry and releases it on exit. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, acquire shared lock; otherwise exclusive lock + session: If True (default), session-level lock; otherwise transaction-level + + Returns: + Async context manager that holds the lock + + Example: + async with backend.advisory_lock(key=12345): + # Critical section - lock is held + await process_data() + # Lock automatically released + + # Shared lock for read-only access + async with backend.advisory_lock(key=12345, shared=True): + await read_data() + """ + from contextlib import asynccontextmanager + + @asynccontextmanager + async def _advisory_lock_context(): + await self.execute_advisory_lock(key=key, shared=shared, session=session) + try: + yield + finally: + # Only release session-level locks; transaction-level are auto-released + if session: + await self.execute_advisory_unlock(key=key) + + return _advisory_lock_context() + + # endregion + + +__all__ = ["AsyncPostgresBackend"] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py new file mode 100644 index 00000000..25238c2b --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py @@ -0,0 +1,900 @@ +# src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py +"""PostgreSQL synchronous backend implementation. + +This module provides a synchronous PostgreSQL implementation: +- PostgreSQL synchronous backend with connection management and query execution +- PostgreSQL-specific connection configuration +- Type mapping and value conversion +- Transaction management with savepoint support +- PostgreSQL dialect and expression handling +- PostgreSQL-specific type definitions and mappings + +Architecture: +- PostgresBackend: Synchronous implementation using psycopg +- Independent from ORM frameworks - uses only native drivers +""" + +import datetime +import logging +from typing import Any, Dict, Iterable, List, Optional, Tuple, TYPE_CHECKING, Union + +import psycopg +from psycopg.errors import Error as PsycopgError +from psycopg.errors import ( + IntegrityError as PsycopgIntegrityError, + OperationalError as PsycopgOperationalError, + ProgrammingError as PsycopgProgrammingError, + DeadlockDetected as PsycopgDeadlockError, +) + +from rhosocial.activerecord.backend.base import StorageBackend +from rhosocial.activerecord.backend.introspection.backend_mixin import IntrospectorBackendMixin +from rhosocial.activerecord.backend.explain import SyncExplainBackendMixin +from rhosocial.activerecord.backend.errors import ( + ConnectionError, + DatabaseError, + DeadlockError, + IntegrityError, + OperationalError, + QueryError, +) +from rhosocial.activerecord.backend.options import ExecutionOptions +from rhosocial.activerecord.backend.result import QueryResult +from ..config import PostgresConnectionConfig +from ..dialect import PostgresDialect +from .base import PostgresBackendMixin, PostgresConcurrencyMixin +from ..protocols import PostgresExtensionInfo +from ..transaction import PostgresTransactionManager +from ..introspection import SyncPostgreSQLIntrospector + +if TYPE_CHECKING: + from ..expression import PostgresCopyFromExpression, PostgresCopyToExpression + + +CopyChunk = Union[str, bytes, bytearray, memoryview] +CopyInput = Union[CopyChunk, Iterable[CopyChunk]] + + +class PostgresBackend( + IntrospectorBackendMixin, + PostgresBackendMixin, + SyncExplainBackendMixin, + PostgresConcurrencyMixin, + StorageBackend, +): + """PostgreSQL-specific backend implementation.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize PostgreSQL backend with connection configuration. + + Note: + The dialect is initialized with a default version (9.6.0) and no plugins + are enabled. Call introspect_and_adapt() after connecting to detect + the actual server version and installed extensions. + """ + graph_feature_overrides = kwargs.pop("graph_feature_overrides", None) + + # Ensure we have proper PostgreSQL configuration + connection_config = kwargs.get("connection_config") + + if connection_config is None: + # Extract PostgreSQL-specific parameters from kwargs + config_params = {} + pg_specific_params = [ + "host", + "port", + "database", + "username", + "password", + "charset", + "collation", + "timezone", + "version", + "pool_size", + "pool_timeout", + "pool_name", + "pool_reset_session", + "pool_pre_ping", + "ssl_ca", + "ssl_cert", + "ssl_key", + "ssl_verify_cert", + "ssl_verify_identity", + "log_queries", + "log_level", + "application_name", + "fallback_application_name", + "connect_timeout", + "options", + "service", + "target_session_attrs", + "gssencmode", + "channel_binding", + "replication", + "assume_role", + "role", + "search_path", + "row_security", + "datestyle", + "intervalstyle", + "timezone_setting", + "extra_float_digits", + "client_encoding", + "tcp_user_timeout", + "tcp_keepalives_idle", + "tcp_keepalives_interval", + "tcp_keepalives_count", + "load_balance_hosts", + "replication", + "keepalives", + "keepalives_idle", + "keepalives_interval", + "keepalives_count", + ] + + for param in pg_specific_params: + if param in kwargs: + config_params[param] = kwargs[param] + + # Set defaults if not provided + if "host" not in config_params: + config_params["host"] = "localhost" + if "port" not in config_params: + config_params["port"] = 5432 + + kwargs["connection_config"] = PostgresConnectionConfig(**config_params) + + # Initialize the base class first to ensure all base properties are set + super().__init__(**kwargs) + + # Initialize PostgreSQL-specific components + # Version is None until introspect_and_adapt() is called + self._dialect = PostgresDialect( + graph_feature_overrides=graph_feature_overrides, + ) + + # Register PostgreSQL-specific type adapters (same as async backend) + # Note: XML adapter is NOT registered by default due to potential conflicts. + # See PostgresXMLAdapter documentation for details. + self._register_postgres_adapters() + + # Initialize transaction manager (will use backend.execute()) + self._transaction_manager = PostgresTransactionManager(self, self.logger) + + self.log(logging.INFO, "PostgresBackend initialized") + + def _create_introspector(self) -> Any: + """Create and return a SyncPostgreSQLIntrospector with a sync executor.""" + from rhosocial.activerecord.backend.introspection.executor import SyncIntrospectorExecutor + + return SyncPostgreSQLIntrospector(self, SyncIntrospectorExecutor(self)) + + @property + def dialect(self) -> PostgresDialect: + """Get the PostgreSQL dialect instance.""" + return self._dialect + + @property + def transaction_manager(self) -> PostgresTransactionManager: + """Get the PostgreSQL transaction manager.""" + return self._transaction_manager + + + def connect(self) -> None: + """Establish connection to PostgreSQL database.""" + try: + # Prepare connection parameters from config + conn_params = { + "host": self.config.host, + "port": self.config.port, + "dbname": self.config.database, + "user": self.config.username, + "password": self.config.password, + } + + # Add additional parameters if they exist in config + additional_params = [ + "application_name", + "fallback_application_name", + "connect_timeout", + "options", + "service", + "target_session_attrs", + "gssencmode", + "channel_binding", + "replication", + "assume_role", + "role", + "search_path", + "row_security", + "datestyle", + "intervalstyle", + "timezone", + "extra_float_digits", + "client_encoding", + "tcp_user_timeout", + "tcp_keepalives_idle", + "tcp_keepalives_interval", + "tcp_keepalives_count", + "load_balance_hosts", + "keepalives", + "keepalives_idle", + "keepalives_interval", + "keepalives_count", + ] + + for param in additional_params: + if hasattr(self.config, param): + value = getattr(self.config, param) + if value is not None: # Only add the parameter if it's not None + conn_params[param] = value + + # Handle 'options' parameter specially as it should be a string, not a dict + if hasattr(self.config, "options") and self.config.options is not None: + # Convert options dict to string format if needed, or use as-is if it's already a string + options_value = self.config.options + if isinstance(options_value, dict): + # Convert dict to connection string format + options_str = " ".join([f"-c {k}={v}" for k, v in options_value.items()]) + conn_params["options"] = options_str + else: + conn_params["options"] = options_value + + # Add SSL parameters if provided + ssl_params = {} + if hasattr(self.config, "ssl_ca"): + ssl_params["sslcert"] = self.config.ssl_ca + if hasattr(self.config, "ssl_cert"): + ssl_params["sslcert"] = self.config.ssl_cert + if hasattr(self.config, "ssl_key"): + ssl_params["sslkey"] = self.config.ssl_key + if hasattr(self.config, "ssl_mode"): + ssl_params["sslmode"] = self.config.ssl_mode + + if ssl_params: + conn_params.update(ssl_params) + + self._connection = psycopg.connect(**conn_params) + self._connection.autocommit = True # Disable psycopg auto-transaction management + + self.log( + logging.INFO, + f"Connected to PostgreSQL database: {self.config.host}:{self.config.port}/{self.config.database}", + ) + except PsycopgError as e: + self.log(logging.ERROR, f"Failed to connect to PostgreSQL database: {str(e)}") + raise ConnectionError(f"Failed to connect to PostgreSQL: {str(e)}") from e + + def disconnect(self) -> None: + """Close connection to PostgreSQL database.""" + if self._connection: + try: + # Rollback any active transaction + if self.in_transaction: + self.transaction_manager.rollback() + + self._connection.close() + self._connection = None + self.log(logging.INFO, "Disconnected from PostgreSQL database") + except PsycopgError as e: + self.log(logging.ERROR, f"Error during disconnection: {str(e)}") + raise OperationalError(f"Error during PostgreSQL disconnection: {str(e)}") from e + + def _get_cursor(self) -> Any: + """Get a database cursor, ensuring connection is active. + + This method implements automatic connection health checking (Plan A): + - Checks if connection object exists + - Checks if connection is still valid using closed/broken attributes + - Automatically reconnects if connection was lost + """ + if not self._connection: + self.log(logging.DEBUG, "No connection, connecting...") + self.connect() + elif self._connection.closed or self._connection.broken: + self.log(logging.DEBUG, "Connection lost, reconnecting...") + self._connection = None # Clear the closed/broken connection + self.connect() + + return self._connection.cursor() + + @staticmethod + def _copy_output_chunk(chunk: Any) -> bytes: + if isinstance(chunk, bytes): + return chunk + if isinstance(chunk, str): + return chunk.encode("utf-8") + if isinstance(chunk, (bytearray, memoryview)): + return bytes(chunk) + raise TypeError("PostgreSQL COPY stream yielded a non-buffer chunk") + + @staticmethod + def _validate_copy_input_chunk( + chunk: Any, + binary: bool, + encoding: Optional[str] = None, + ) -> CopyChunk: + if not isinstance(chunk, (str, bytes, bytearray, memoryview)): + raise TypeError("COPY input chunks must be str or bytes-like objects") + if binary and isinstance(chunk, str): + raise TypeError("COPY FROM BINARY requires bytes-like input chunks") + if encoding is not None and isinstance(chunk, str): + raise TypeError("COPY FROM with ENCODING requires bytes-like input chunks") + return chunk + + def copy_to(self, expression: "PostgresCopyToExpression") -> bytes: + """Execute COPY TO STDOUT and return the complete byte stream.""" + from ..expression import PostgresCopyToExpression + + if not isinstance(expression, PostgresCopyToExpression): + raise TypeError("expression must be a PostgresCopyToExpression") + sql, params = expression.to_sql() + chunks = [] + try: + with self._get_cursor() as cursor: + with cursor.copy(sql, params) as copy: + for chunk in copy: + chunks.append(self._copy_output_chunk(chunk)) + except BaseException as error: + if isinstance(error, PsycopgError): + self._handle_error(error) + else: + self._try_rollback_transaction() + raise + return b"".join(chunks) + + def copy_from( + self, + expression: "PostgresCopyFromExpression", + data: CopyInput, + ) -> int: + """Stream COPY FROM STDIN data and return the server-reported row count.""" + from ..expression import PostgresCopyFromExpression + + if not isinstance(expression, PostgresCopyFromExpression): + raise TypeError("expression must be a PostgresCopyFromExpression") + sql, params = expression.to_sql() + binary = str(expression.format).lower() == "binary" + chunks: Iterable[CopyChunk] + if isinstance(data, (str, bytes, bytearray, memoryview)): + chunks = (data,) + else: + try: + chunks = iter(data) + except TypeError as error: + raise TypeError("COPY input must be bytes, str, or an iterable of chunks") from error + try: + with self._get_cursor() as cursor: + with cursor.copy(sql, params) as copy: + for chunk in chunks: + copy.write( + self._validate_copy_input_chunk(chunk, binary, expression.encoding) + ) + rowcount = int(cursor.rowcount) + if getattr(self, "_transaction_manager", None) is not None: + self._handle_auto_commit_if_needed() + return rowcount + except BaseException as error: + if isinstance(error, PsycopgError): + self._handle_error(error) + else: + self._try_rollback_transaction() + raise + + def execute_many(self, sql: str, params_list: List[Tuple]) -> QueryResult: + """Execute the same SQL statement multiple times with different parameters.""" + if not self._connection: + self.connect() + + cursor = None + start_time = datetime.datetime.now() + + try: + cursor = self._get_cursor() + + # Log the batch operation if logging is enabled + if getattr(self.config, "log_queries", False): + self.log(logging.DEBUG, f"Executing batch operation: {sql}") + self.log(logging.DEBUG, f"With {len(params_list)} parameter sets") + + # Execute multiple statements + affected_rows = 0 + for params in params_list: + cursor.execute(sql, params) + affected_rows += cursor.rowcount + + duration = (datetime.datetime.now() - start_time).total_seconds() + + result = QueryResult(affected_rows=affected_rows, data=None, duration=duration) + + self.log( + logging.INFO, + f"Batch operation completed, affected {affected_rows} rows, duration={duration:.3f}s", + ) + return result + + except PsycopgIntegrityError as e: + self.log(logging.ERROR, f"Integrity error in batch: {str(e)}") + raise IntegrityError(str(e)) from e + except PsycopgError as e: + self.log(logging.ERROR, f"PostgreSQL error in batch: {str(e)}") + raise DatabaseError(str(e)) from e + except Exception as e: + self.log(logging.ERROR, f"Unexpected error during batch execution: {str(e)}") + raise QueryError(str(e)) from e + finally: + if cursor: + cursor.close() + + def get_server_version(self) -> Optional[Tuple[int, int, int]]: + """Get PostgreSQL server version. + + Returns: + Tuple of (major, minor, patch) version numbers, or None if version + cannot be determined. + """ + if not self._connection: + self.connect() + + cursor = None + try: + cursor = self._get_cursor() + cursor.execute("SELECT version()") + result = cursor.fetchone() + if not result or not result[0]: + self.log(logging.WARNING, "PostgreSQL version query returned no result") + return None + + version_str = result[0] + + # Extract version from string like "PostgreSQL 13.2..." + import re + + match = re.search(r"PostgreSQL (\d+)(?:\.(\d+))?(?:[^\d]*(\d+))?", version_str) + if match: + major = int(match.group(1)) + minor = int(match.group(2)) if match.group(2) else 0 + patch = int(match.group(3)) if match.group(3) else 0 + return (major, minor, patch) + else: + self.log(logging.WARNING, f"Could not parse PostgreSQL version: {version_str}") + return None + except Exception as e: + self.log(logging.WARNING, f"Could not determine PostgreSQL version: {str(e)}") + return None + finally: + if cursor: + cursor.close() + + def introspect_and_adapt(self) -> None: + """Introspect backend and adapt backend instance to actual server capabilities. + + This method ensures a connection exists, queries the actual PostgreSQL server version + and installed extensions, and updates the backend's internal state. + + Introspection includes: + 1. Server version: affects version-dependent feature support checks + 2. Installed extensions: queries pg_extension system table to determine plugin availability + + After detection, the dialect instance caches extension information, + which can be queried via is_extension_installed(). + + Note: If version cannot be determined, the default version (9.6.0) is retained. + """ + # Ensure connection exists + if not self._connection: + self.connect() + + # Get server version + actual_version = self.get_server_version() + + # If version cannot be determined, keep the default version + if actual_version is None: + self.log(logging.WARNING, "Could not determine server version, retaining default (9.6.0)") + actual_version = (9, 6, 0) + + cached_version = getattr(self, "_server_version_cache", None) + version_changed = cached_version != actual_version + + if version_changed: + self._server_version_cache = actual_version + self._dialect = PostgresDialect( + actual_version, + graph_feature_overrides=getattr( + self._dialect, "_graph_feature_overrides", None + ), + ) + + # Detect installed extensions + extensions = self._detect_extensions() + self._dialect._extensions = extensions + + # Log detected extensions + installed_exts = [f"{k}={v.version}" for k, v in extensions.items() if v.installed] + ext_info = ", ".join(installed_exts) if installed_exts else "none" + + if version_changed: + self.log(logging.INFO, f"Adapted to PostgreSQL {actual_version}, extensions: {ext_info}") + else: + self.log(logging.DEBUG, f"Extensions detected: {ext_info}") + + def _detect_extensions(self) -> Dict[str, PostgresExtensionInfo]: + """Detect installed and available extensions.""" + with self._connection.cursor() as cursor: + cursor.execute(PostgresDialect.SQL_INSTALLED_EXTENSIONS) + installed_rows = cursor.fetchall() + + cursor.execute(PostgresDialect.SQL_AVAILABLE_EXTENSIONS) + available_rows = cursor.fetchall() + + return PostgresDialect.build_extension_map(installed_rows, available_rows) + + def ping(self, reconnect: bool = True) -> bool: + """ + Ping the PostgreSQL server to check if the connection is alive. + + Args: + reconnect: If True, attempt to reconnect if the connection is dead. + If False, just return the current connection status. + + Returns: + True if the connection is alive (or was successfully reconnected), + False if the connection is dead and reconnect is False or reconnection failed. + """ + try: + if not self._connection: + if reconnect: + self.connect() + return True + else: + return False + + # Check connection status without triggering auto-reconnect + if self._connection.closed or self._connection.broken: + if reconnect: + self._connection = None + self.connect() + return True + else: + return False + + # Execute a simple query to verify connection is actually working + cursor = self._get_cursor() + cursor.execute("SELECT 1") + cursor.fetchone() + cursor.close() + + return True + except psycopg.Error as e: + self.log(logging.WARNING, f"PostgreSQL connection ping failed: {str(e)}") + if reconnect: + try: + self._connection = None + self.connect() + return True + except Exception as connect_error: + self.log(logging.ERROR, f"Failed to reconnect after ping failure: {str(connect_error)}") + return False + return False + + def _reconnect(self) -> bool: + """ + Attempt to reconnect to the PostgreSQL server. + + Returns: + True if reconnection was successful, False otherwise + """ + try: + self._connection = None + self.connect() + return True + except Exception as e: + self.log(logging.ERROR, f"Failed to reconnect: {str(e)}") + return False + + def execute( + self, + sql: str, + params: Optional[Tuple] = None, + *, + options: Optional[ExecutionOptions] = None, + max_retries: int = 2, + **kwargs: Any, + ) -> QueryResult: + """ + Execute a SQL statement with optional parameters. + + This method wraps the base execute method with automatic reconnection + logic for connection errors (Plan B: Error Retry Mechanism). + + Args: + sql: SQL statement to execute + params: Optional parameters for the SQL statement + options: Execution options + max_retries: Maximum number of retry attempts for connection errors + **kwargs: Additional keyword arguments + + Returns: + QueryResult containing the query results + """ + from rhosocial.activerecord.backend.options import StatementType + + # If no options provided, create default options from kwargs + if options is None: + # Determine statement type based on SQL + sql_upper = sql.strip().upper() + if sql_upper.startswith(("SELECT", "WITH", "SHOW", "DESCRIBE", "EXPLAIN")): + stmt_type = StatementType.DQL + elif sql_upper.startswith(("INSERT", "UPDATE", "DELETE", "UPSERT")): + stmt_type = StatementType.DML + else: + stmt_type = StatementType.DDL + + # Extract column_mapping and column_adapters from kwargs if present + column_mapping = kwargs.get("column_mapping") + column_adapters = kwargs.get("column_adapters") + + options = ExecutionOptions( + stmt_type=stmt_type, + process_result_set=None, + column_adapters=column_adapters, + column_mapping=column_mapping, + ) + else: + # If options is provided but column_mapping or column_adapters are explicitly passed in kwargs, + # update the options with these values + if "column_mapping" in kwargs: + options.column_mapping = kwargs["column_mapping"] + if "column_adapters" in kwargs: + options.column_adapters = kwargs["column_adapters"] + + # Retry loop for connection errors (Plan B) + # We catch the framework's OperationalError because the original PsycopgOperationalError + # has already been converted by _handle_error() in the base class. + for attempt in range(max_retries + 1): + try: + return super().execute(sql, params, options=options) + except OperationalError as e: + error_msg = str(e) + if self._is_connection_error_message(error_msg) and attempt < max_retries: + self.log(logging.WARNING, + f"Connection error on attempt {attempt + 1}, retrying... Error: {error_msg}") + if self._reconnect(): + continue + # If reconnection fails, break and raise the error + self.log(logging.ERROR, "Failed to reconnect after connection error") + break + raise + + def _handle_error(self, error: Exception) -> None: + """Handle PostgreSQL-specific errors.""" + error_msg = str(error) + + # Try to rollback transaction to recover from aborted state + # PostgreSQL requires explicit ROLLBACK after errors in a transaction + self._try_rollback_transaction() + + if isinstance(error, PsycopgIntegrityError): + if "duplicate key value violates unique constraint" in error_msg.lower(): + self.log(logging.ERROR, f"Unique constraint violation: {error_msg}") + raise IntegrityError(f"Unique constraint violation: {error_msg}") from error + elif "violates foreign key constraint" in error_msg.lower(): + self.log(logging.ERROR, f"Foreign key constraint violation: {error_msg}") + raise IntegrityError(f"Foreign key constraint violation: {error_msg}") from error + self.log(logging.ERROR, f"Integrity error: {error_msg}") + raise IntegrityError(error_msg) from error + elif isinstance(error, PsycopgOperationalError): + if "deadlock detected" in error_msg.lower(): + self.log(logging.ERROR, f"Deadlock error: {error_msg}") + raise DeadlockError(error_msg) from error + self.log(logging.ERROR, f"Operational error: {error_msg}") + raise OperationalError(error_msg) from error + elif isinstance(error, PsycopgProgrammingError): + self.log(logging.ERROR, f"Programming error: {error_msg}") + raise QueryError(error_msg) from error + elif isinstance(error, PsycopgDeadlockError): + self.log(logging.ERROR, f"Deadlock error: {error_msg}") + raise DeadlockError(error_msg) from error + elif isinstance(error, PsycopgError): + self.log(logging.ERROR, f"PostgreSQL error: {error_msg}") + raise DatabaseError(error_msg) from error + else: + self.log(logging.ERROR, f"Unexpected error: {error_msg}") + raise error + + def _try_rollback_transaction(self) -> None: + """Attempt to rollback transaction to recover from aborted state. + + PostgreSQL requires explicit ROLLBACK after query errors in a transaction. + This method attempts to recover by rolling back the transaction, regardless + of the transaction manager's state. + """ + try: + # First, try the transaction manager if active + if self.in_transaction: + self.log(logging.INFO, "Attempting to rollback transaction after error") + self._transaction_manager.rollback() + + # Also try direct connection rollback to handle edge cases + # This ensures we recover from aborted transaction state + if self._connection and not self._connection.closed: + try: + self._connection.rollback() + self.log(logging.INFO, "Direct connection rollback completed") + except Exception as direct_error: + self.log(logging.DEBUG, f"Direct rollback not needed or failed: {direct_error}") + except Exception as rollback_error: + self.log(logging.WARNING, f"Failed to rollback transaction: {rollback_error}") + + def _handle_auto_commit(self) -> None: + """Handle auto commit based on PostgreSQL connection and transaction state. + + This method will commit the current connection if: + 1. The connection exists and is open + 2. There is no active transaction managed by transaction_manager + + It's used by insert/update/delete operations to ensure changes are + persisted immediately when auto_commit=True is specified. + """ + try: + # Check if connection exists + if not self._connection: + return + + # Check if we're not in an active transaction + if not self.in_transaction: + # For PostgreSQL, if autocommit is disabled, we need to commit explicitly + if not self.config.autocommit: + self._connection.commit() + self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") + except Exception as e: + # Just log the error but don't raise - this is a convenience feature + self.log(logging.WARNING, f"Failed to auto-commit: {str(e)}") + + def _handle_auto_commit_if_needed(self) -> None: + """ + Handle auto-commit for PostgreSQL. + + PostgreSQL respects the autocommit setting, but we also need to handle explicit commits. + """ + if not self.in_transaction and self._connection: + if not self.config.autocommit: + self._connection.commit() + self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") + + # region Advisory Lock Methods + + def execute_advisory_lock( + self, + key, + shared: bool = False, + session: bool = True + ) -> None: + """ + Acquire an advisory lock. + + This method blocks until the lock is acquired. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, acquire shared lock; otherwise exclusive lock + session: If True (default), session-level lock; otherwise transaction-level + + Raises: + DatabaseError: If the lock operation fails + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryLockExpression + + expr = PostgresAdvisoryLockExpression(self.dialect, key=key, shared=shared, session=session) + sql, params = expr.to_sql() + self.execute(sql, params) + + def execute_advisory_unlock(self, key, shared: bool = False) -> bool: + """ + Release an advisory lock. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, release shared lock; otherwise exclusive lock + + Returns: + True if the lock was released, False if it was not held + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryUnlockExpression + + expr = PostgresAdvisoryUnlockExpression(self.dialect, key=key, shared=shared) + sql, params = expr.to_sql() + result = self.execute(sql, params) + # pg_advisory_unlock returns boolean in first column + if result.data and len(result.data) > 0: + row = result.data[0] + # Result is a dict with column name as key + return bool(list(row.values())[0]) + return False + + def execute_advisory_unlock_all(self) -> None: + """ + Release all advisory locks held by the current session. + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryUnlockAllExpression + + expr = PostgresAdvisoryUnlockAllExpression(self.dialect) + sql, params = expr.to_sql() + self.execute(sql, params) + + def try_advisory_lock( + self, + key, + shared: bool = False, + session: bool = True + ) -> bool: + """ + Non-blocking advisory lock acquisition. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, acquire shared lock; otherwise exclusive lock + session: If True (default), session-level lock; otherwise transaction-level + + Returns: + True if the lock was acquired, False if it was not available + """ + from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresTryAdvisoryLockExpression + + expr = PostgresTryAdvisoryLockExpression(self.dialect, key=key, shared=shared, session=session) + sql, params = expr.to_sql() + result = self.execute(sql, params) + # pg_try_advisory_lock returns boolean in first column + if result.data and len(result.data) > 0: + row = result.data[0] + # Result is a dict with column name as key + return bool(list(row.values())[0]) + return False + + def advisory_lock( + self, + key, + shared: bool = False, + session: bool = True + ): + """ + Context manager for advisory locks. + + Automatically acquires the lock on entry and releases it on exit. + + Args: + key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers + shared: If True, acquire shared lock; otherwise exclusive lock + session: If True (default), session-level lock; otherwise transaction-level + + Returns: + Context manager that holds the lock + + Example: + with backend.advisory_lock(key=12345): + # Critical section - lock is held + process_data() + # Lock automatically released + + # Shared lock for read-only access + with backend.advisory_lock(key=12345, shared=True): + read_data() + """ + from contextlib import contextmanager + + @contextmanager + def _advisory_lock_context(): + self.execute_advisory_lock(key=key, shared=shared, session=session) + try: + yield + finally: + # Only release session-level locks; transaction-level are auto-released + if session: + self.execute_advisory_unlock(key=key) + + return _advisory_lock_context() + + # endregion + + +__all__ = ["PostgresBackend"] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py new file mode 100644 index 00000000..e916a345 --- /dev/null +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py @@ -0,0 +1,416 @@ +# src/rhosocial/activerecord/backend/impl/postgres/backend_base.py +"""PostgreSQL backend shared functionality. + +This module provides the base mixin class for PostgreSQL backend implementations, +containing methods that are shared between sync and async backends. +""" + +from typing import Dict, Tuple, Type, TYPE_CHECKING +import logging + +if TYPE_CHECKING: + from rhosocial.activerecord.backend.type_adapter import SQLTypeAdapter + from rhosocial.activerecord.backend.protocols import ConcurrencyHint + + +class PostgresBackendMixin: + """PostgreSQL backend shared methods mixin. + + This mixin provides methods that are shared between sync and async + PostgreSQL backend implementations. These methods do not involve + I/O operations and have identical implementations in both backends. + + Classes using this mixin must provide: + - self._dialect: PostgresDialect instance + - self.adapter_registry: Type adapter registry + - self.config: Connection configuration + - self._logger: Logger instance + """ + + # PostgreSQL connection error SQLSTATE codes + # Reference: https://www.postgresql.org/docs/current/errcodes-appendix.html + CONNECTION_ERROR_SQLSTATES = { + "08000", # CONNECTION_EXCEPTION + "08001", # SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION + "08003", # CONNECTION_DOES_NOT_EXIST + "08004", # SQLSERVER_REJECTED_ESTABLISHMENT_OF_SQLCONNECTION + "08006", # CONNECTION_FAILURE + "08007", # TRANSACTION_RESOLUTION_UNKNOWN + "08P01", # PROTOCOL_VIOLATION + "57P01", # ADMIN_SHUTDOWN + "57P02", # CRASH_SHUTDOWN + "57P03", # CANNOT_CONNECT_NOW + "57P04", # DATABASE_DROPPED + } + + def log(self, level: int, message: str): + """Log a message with the specified level.""" + if hasattr(self, "_logger") and self._logger: + self._logger.log(level, message) + else: + # Fallback logging + print(f"[{logging.getLevelName(level)}] {message}") + + def get_default_schema(self) -> str: + """Get the default schema name for PostgreSQL. + + Returns the configured default_schema, or 'public' as the + PostgreSQL standard default if not explicitly set. + + Returns: + str: Default schema name + """ + return getattr(self.config, "default_schema", None) or "public" + + def create_expression(self, expression_str: str): + """Create an expression object for raw SQL expressions.""" + from rhosocial.activerecord.backend.expression.operators import RawSQLExpression + + return RawSQLExpression(self.dialect, expression_str) + + @property + def threadsafety(self) -> int: + """Return driver threadsafety level. + + PostgreSQL connections can be safely shared across threads when using + psycopg version 3, which supports thread-safe connection handling. + + Returns: + 2 (connections can be safely shared across threads) + """ + return 2 + + def requires_manual_commit(self) -> bool: + """Check if manual commit is required for this database.""" + return not self.config.autocommit + + def _register_postgres_adapters(self): + """Register PostgreSQL-specific type adapters. + + Range and Multirange adapter registration is controlled by configuration: + - range_adapter_mode: Controls how Range types are handled + - NATIVE (default): Use psycopg's native Range type (pass-through) + - CUSTOM: Use PostgresRange custom type + - BOTH: Register both adapters, with custom taking precedence + - multirange_adapter_mode: Same for Multirange types (PG 14+ only) + + Note: The following adapters are NOT registered by default due to str->str + type mapping conflicts with existing string handling: + - PostgresXMLAdapter: XML type + - PostgresBitStringAdapter: bit/varbit types + - PostgresJsonPathAdapter: jsonpath type + + Users should specify these explicitly when working with such columns. + """ + from ..adapters import ( + PostgresListAdapter, + PostgresJSONBAdapter, + PostgresNetworkAddressAdapter, + PostgresEnumAdapter, + PostgresRangeAdapter, + PostgresMultirangeAdapter, + ) + from ..adapters.geometric import PostgresGeometryAdapter + from ..adapters.monetary import PostgresMoneyAdapter + from ..adapters.network_address import PostgresMacaddrAdapter, PostgresMacaddr8Adapter + from ..adapters.object_identifier import PostgresOidAdapter, PostgresXidAdapter, PostgresTidAdapter + from ..adapters.pg_lsn import PostgresLsnAdapter + from ..adapters.text_search import PostgresTsVectorAdapter, PostgresTsQueryAdapter + from ..config import RangeAdapterMode + + pg_adapters = [ + PostgresListAdapter(), + PostgresJSONBAdapter(), + PostgresNetworkAddressAdapter(), + PostgresGeometryAdapter(), + PostgresEnumAdapter(), + PostgresMoneyAdapter(), + PostgresMacaddrAdapter(), + PostgresMacaddr8Adapter(), + PostgresTsVectorAdapter(), + PostgresTsQueryAdapter(), + PostgresLsnAdapter(), + PostgresOidAdapter(), + PostgresXidAdapter(), + PostgresTidAdapter(), + # PostgresJsonPathAdapter(), # Not registered: str->str conflict + # PostgresXMLAdapter is NOT registered by default + # due to str->str type pair conflict + ] + + for adapter in pg_adapters: + for py_type, db_types in adapter.supported_types.items(): + for db_type in db_types: + self.adapter_registry.register(adapter, py_type, db_type) + + # Register Range adapters based on configuration + range_mode = getattr(self.config, "range_adapter_mode", RangeAdapterMode.NATIVE) + if range_mode in (RangeAdapterMode.CUSTOM, RangeAdapterMode.BOTH): + range_adapter = PostgresRangeAdapter() + for py_type, db_types in range_adapter.supported_types.items(): + for db_type in db_types: + self.adapter_registry.register(range_adapter, py_type, db_type) + self.log(logging.DEBUG, "Registered PostgresRangeAdapter (custom range type)") + + # Register Multirange adapters based on configuration (PG 14+ only) + # Note: Version check happens in dialect, but at this point we haven't + # connected yet, so we register if configured. The adapter itself will + # handle version-specific logic if needed. + multirange_mode = getattr(self.config, "multirange_adapter_mode", RangeAdapterMode.NATIVE) + if multirange_mode in (RangeAdapterMode.CUSTOM, RangeAdapterMode.BOTH): + multirange_adapter = PostgresMultirangeAdapter() + for py_type, db_types in multirange_adapter.supported_types.items(): + for db_type in db_types: + self.adapter_registry.register(multirange_adapter, py_type, db_type) + self.log(logging.DEBUG, "Registered PostgresMultirangeAdapter (custom multirange type)") + + self.log(logging.DEBUG, "Registered PostgreSQL-specific type adapters") + + def _is_connection_error(self, error: Exception) -> bool: + """Check if an error indicates a connection loss. + + This method identifies errors that result from lost or invalid connections, + which should trigger automatic reconnection attempts. + + Args: + error: The exception to check + + Returns: + True if the error indicates a connection problem, False otherwise + """ + from psycopg.errors import OperationalError as PsycopgOperationalError + + if isinstance(error, PsycopgOperationalError): + # Check SQLSTATE code if available + sqlstate = getattr(error, 'sqlstate', None) + if sqlstate and sqlstate in self.CONNECTION_ERROR_SQLSTATES: + return True + + # Check for common connection error patterns in message + error_msg = str(error).lower() + connection_error_patterns = [ + 'connection', + 'connect', + 'closed', + 'terminated', + 'unexpectedly', + 'broken pipe', + 'reset by peer', + 'server closed', + 'server has gone away', + ] + for pattern in connection_error_patterns: + if pattern in error_msg: + return True + + return False + + def _is_connection_error_message(self, error_msg: str) -> bool: + """Check if an error message indicates a connection loss. + + This method identifies error messages that result from lost or invalid connections, + which should trigger automatic reconnection attempts. + + Args: + error_msg: The error message to check + + Returns: + True if the message indicates a connection problem, False otherwise + """ + error_msg_lower = error_msg.lower() + connection_error_patterns = [ + 'connection', + 'connect', + 'closed', + 'terminated', + 'terminating connection due to administrator command', + 'unexpectedly', + 'broken pipe', + 'reset by peer', + 'server closed', + 'server has gone away', + 'ssl connection has been closed', + 'consuming input failed', + ] + for pattern in connection_error_patterns: + if pattern in error_msg_lower: + return True + return False + + def _parse_explain_result(self, raw_rows, sql, duration): + """Return a typed :class:`PostgresExplainResult` for PostgreSQL EXPLAIN output.""" + from ..explain import PostgresExplainResult, PostgresExplainPlanLine + rows = [PostgresExplainPlanLine(line=r.get("QUERY PLAN", "")) for r in raw_rows] + return PostgresExplainResult(raw_rows=raw_rows, sql=sql, duration=duration, rows=rows) + + def get_default_adapter_suggestions(self) -> Dict[Type, Tuple["SQLTypeAdapter", Type]]: + """ + [Backend Implementation] Provides default type adapter suggestions for PostgreSQL. + + This method defines a curated set of type adapter suggestions for common Python + types, mapping them to their typical PostgreSQL-compatible representations as + demonstrated in test fixtures. It explicitly retrieves necessary `SQLTypeAdapter` + instances from the backend's `adapter_registry`. If an adapter for a specific + (Python type, DB driver type) pair is not registered, no suggestion will be + made for that Python type. + + Returns: + Dict[Type, Tuple[SQLTypeAdapter, Type]]: A dictionary where keys are + original Python types (`TypeRegistry`'s `py_type`), and values are + tuples containing a `SQLTypeAdapter` instance and the target + Python type (`TypeRegistry`'s `db_type`) expected by the driver. + + Note: + PostgreSQL XML and BitString types are NOT included here due to str->str + mapping conflicts with the base string handling. Users must explicitly + specify adapters for these types when needed. + """ + suggestions: Dict[Type, Tuple["SQLTypeAdapter", Type]] = {} + + # Define a list of desired Python type to DB driver type mappings. + # This list reflects types seen in test fixtures and common usage, + # along with their preferred database-compatible Python types for the driver. + # Types that are natively compatible with the DB driver (e.g., Python str, int, float) + # and for which no specific conversion logic is needed are omitted from this list. + # The consuming layer should assume pass-through behavior for any Python type + # that does not have an explicit adapter suggestion. + # + # Exception: If a user requires specific processing for a natively compatible type + # (e.g., custom serialization/deserialization for JSON strings beyond basic conversion), + # they would need to implement and register their own specialized adapter. + # This backend's default suggestions do not cater to such advanced processing needs. + from datetime import date, datetime, time + from decimal import Decimal + from uuid import UUID + from enum import Enum + + # Import PostgreSQL-specific types + from ..type_values.range import PostgresRange, PostgresMultirange + from ..type_values.geometric import Point, Line, LineSegment, Box, Path, Polygon, Circle + from ..type_values.text_search import PostgresTsVector, PostgresTsQuery + from ..type_values.monetary import PostgresMoney + from ..type_values.network_address import PostgresMacaddr, PostgresMacaddr8 + from ..type_values.pg_lsn import PostgresLsn + from ..type_values.json import PostgresJsonPath + from ..type_values.object_identifier import ( + OID, + RegClass, + RegType, + RegProc, + RegProcedure, + RegOper, + RegOperator, + RegConfig, + RegDictionary, + RegNamespace, + RegRole, + RegCollation, + XID, + XID8, + CID, + TID, + ) + + type_mappings = [ + # === Standard Python Types === + (bool, bool), # Python bool -> DB driver bool (PostgreSQL BOOLEAN) + # Why str for date/time? + # PostgreSQL has native DATE, TIME, TIMESTAMP types but accepts string representations + (datetime, str), # Python datetime -> DB driver str (PostgreSQL TIMESTAMP) + (date, str), # Python date -> DB driver str (PostgreSQL DATE) + (time, str), # Python time -> DB driver str (PostgreSQL TIME) + (Decimal, Decimal), # Python Decimal -> DB driver Decimal (PostgreSQL NUMERIC/DECIMAL) + (UUID, str), # Python UUID -> DB driver str (PostgreSQL UUID type) + (dict, str), # Python dict -> DB driver str (PostgreSQL JSON/JSONB) + (list, list), # Python list -> DB driver list (PostgreSQL arrays - psycopg handles natively) + (Enum, str), # Python Enum -> DB driver str (PostgreSQL TEXT) + # === PostgreSQL Range Types === + (PostgresRange, str), # PostgreSQL range types (int4range, daterange, etc.) + (PostgresMultirange, str), # PostgreSQL multirange types (PG 14+) + # === PostgreSQL Geometric Types === + # All geometric types map to str representation + (Point, str), # 2D point (x, y) + (Line, str), # Infinite line {A, B, C} + (LineSegment, str), # Line segment [(x1,y1),(x2,y2)] + (Box, str), # Rectangular box (x1,y1),(x2,y2) + (Path, str), # Sequence of points (open or closed) + (Polygon, str), # Closed path + (Circle, str), # Circle <(x,y),r> + # === PostgreSQL Text Search Types === + (PostgresTsVector, str), # Text search vector + (PostgresTsQuery, str), # Text search query + # === PostgreSQL Monetary Type === + (PostgresMoney, str), # MONEY type with locale-aware formatting + # === PostgreSQL Network Address Types === + (PostgresMacaddr, str), # 6-byte MAC address + (PostgresMacaddr8, str), # 8-byte MAC address + # === PostgreSQL pg_lsn Type === + (PostgresLsn, str), # Log Sequence Number + # === PostgreSQL JSON Path Type === + (PostgresJsonPath, str), # JSON path expression + # === PostgreSQL Object Identifier Types === + (OID, int), # Unsigned 4-byte integer for internal object identification + (RegClass, str), # Relation (table/view/sequence) name + (RegType, str), # Data type name + (RegProc, str), # Function name + (RegProcedure, str), # Function name with argument types + (RegOper, str), # Operator name + (RegOperator, str), # Operator name with argument types + (RegConfig, str), # Text search configuration name + (RegDictionary, str), # Text search dictionary name + (RegNamespace, str), # Namespace (schema) name + (RegRole, str), # Role (user/group) name + (RegCollation, str), # Collation name + # === PostgreSQL Transaction/Command Identifiers === + (XID, int), # 32-bit transaction identifier + (XID8, int), # 64-bit transaction identifier (PG 13+) + (CID, int), # Command identifier + # === PostgreSQL Tuple Identifier === + (TID, str), # Tuple identifier (block number, offset) + # Note: PostgresXML and PostgresBitString are NOT included here + # because they both map to str, which conflicts with base string handling. + # Users must explicitly specify adapters for these types. + ] + + # Iterate through the defined mappings and retrieve adapters from the registry. + for py_type, db_type in type_mappings: + adapter = self.adapter_registry.get_adapter(py_type, db_type) + if adapter: + suggestions[py_type] = (adapter, db_type) + else: + # Log a debug message if a specific adapter is expected but not found. + self.log( + logging.DEBUG, + f"No adapter found for ({py_type.__name__}, {db_type.__name__}). " + "Suggestion will not be provided for this type.", + ) + + return suggestions + + +class PostgresConcurrencyMixin: + """Mixin providing PostgreSQL-specific concurrency hint. + + Uses PostgreSQL connection pool for concurrency limit. + Returns pool.size as the concurrency limit. + """ + + def get_concurrency_hint(self) -> "ConcurrencyHint": + """Get concurrency hint based on connection pool size.""" + from rhosocial.activerecord.backend.protocols import ConcurrencyHint + + pool = getattr(self, "_pool", None) + if pool and hasattr(pool, "size"): + max_concurrency = pool.size + return ConcurrencyHint( + max_concurrency=max_concurrency, + reason=f"connection pool size={max_concurrency}", + ) + # No pool - return None to indicate no constraint + return ConcurrencyHint( + max_concurrency=None, + reason="no connection pool configured", + ) + + +__all__ = ["PostgresBackendMixin", "PostgresConcurrencyMixin"] diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py index a64ccdcd..a20b5632 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/introspect.py @@ -12,10 +12,8 @@ from enum import Enum from typing import Any -from rhosocial.activerecord.backend.impl.postgres.backend import ( - PostgresBackend, - AsyncPostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.errors import ConnectionError, QueryError from .connection import add_connection_args, resolve_connection_config_from_args diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py index eb436c9a..40828cc4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_expression.py @@ -4,10 +4,8 @@ named-expression requires connection arguments and output arguments. """ -from rhosocial.activerecord.backend.impl.postgres.backend import ( - PostgresBackend, - AsyncPostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.options import ExecutionOptions from .connection import create_connection_parent_parser, resolve_connection_config_from_args diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py index 3c2ca7ff..ae1d1e54 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_migration.py @@ -3,10 +3,8 @@ from __future__ import annotations -from rhosocial.activerecord.backend.impl.postgres.backend import ( - PostgresBackend, - AsyncPostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from .connection import create_connection_parent_parser, resolve_connection_config_from_args from .output import create_provider diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py index fdf95c34..e8802c79 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure.py @@ -4,10 +4,8 @@ named-procedure requires connection arguments, output arguments, and --rich-ascii. """ -from rhosocial.activerecord.backend.impl.postgres.backend import ( - PostgresBackend, - AsyncPostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from .connection import create_connection_parent_parser, resolve_connection_config_from_args from .output import create_provider diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py index 91b10589..b069c271 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/named_procedure_graph.py @@ -64,7 +64,9 @@ def disconnect(): def backend_async_factory(): nonlocal async_backend - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) config = resolve_connection_config_from_args(args) async_backend = AsyncPostgresBackend(connection_config=config) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py index 5e46f062..a6efb32a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/query.py @@ -9,10 +9,8 @@ import logging import sys -from rhosocial.activerecord.backend.impl.postgres.backend import ( - PostgresBackend, - AsyncPostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.errors import ConnectionError, QueryError from .connection import add_connection_args, resolve_connection_config_from_args diff --git a/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py b/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py index fc9d8347..a7a938a8 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/cli/status.py @@ -13,10 +13,8 @@ from enum import Enum from typing import Any -from rhosocial.activerecord.backend.impl.postgres.backend import ( - PostgresBackend, - AsyncPostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.errors import ConnectionError, QueryError from .connection import add_connection_args, resolve_connection_config_from_args diff --git a/tests/benchmark/backend/conftest.py b/tests/benchmark/backend/conftest.py index 012a7ae4..a8371131 100644 --- a/tests/benchmark/backend/conftest.py +++ b/tests/benchmark/backend/conftest.py @@ -96,7 +96,9 @@ def postgres_backend_sync_context(request, benchmark_size): @pytest.fixture(scope="function", params=SCENARIO_PARAMS) def postgres_backend_async_context(request, benchmark_size): - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) scenario = request.param _, config = get_scenario(scenario) diff --git a/tests/providers/basic.py b/tests/providers/basic.py index 913ef60a..065398fa 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -395,7 +395,9 @@ async def get_dialect(self, scenario_name: str = "default"): return super().get_dialect(scenario_name) async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) await model_class.configure(config, AsyncPostgresBackend) backend_instance = model_class.__backend__ diff --git a/tests/providers/basic_connection.py b/tests/providers/basic_connection.py index c81dc941..9e4ab4b4 100644 --- a/tests/providers/basic_connection.py +++ b/tests/providers/basic_connection.py @@ -103,7 +103,9 @@ def setup_sync_pool_and_model(self, scenario_name: str) -> Tuple[BackendPool, Ty async def setup_async_pool_and_model(self, scenario_name: str) -> Tuple[AsyncBackendPool, Type[AsyncActiveRecord]]: """Setup async connection pool and model for context tests.""" - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) diff --git a/tests/providers/crud_benchmark.py b/tests/providers/crud_benchmark.py index 874b39c7..bfc77fdd 100644 --- a/tests/providers/crud_benchmark.py +++ b/tests/providers/crud_benchmark.py @@ -128,7 +128,9 @@ def _setup_model(self, model_class: Type[ActiveRecord], scenario: str) -> Type[A async def _setup_async_model( self, model_class: Type[ActiveRecord], scenario: str ) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario) await model_class.configure(config, AsyncPostgresBackend) diff --git a/tests/providers/events.py b/tests/providers/events.py index 8f570c27..f20be52f 100644 --- a/tests/providers/events.py +++ b/tests/providers/events.py @@ -159,7 +159,9 @@ def __init__(self): self._active_async_backends = [] async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) await model_class.configure(config, AsyncPostgresBackend) backend_instance = model_class.__backend__ diff --git a/tests/providers/fastapi_benchmark.py b/tests/providers/fastapi_benchmark.py index 9be27347..3ebdcba6 100644 --- a/tests/providers/fastapi_benchmark.py +++ b/tests/providers/fastapi_benchmark.py @@ -143,7 +143,9 @@ async def teardown_benchmark_async( async def _setup_async_model( self, model_class: Type[ActiveRecord], scenario: str ) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario) await model_class.configure(config, AsyncPostgresBackend) @@ -158,7 +160,9 @@ async def _initialize_schema_async(self, backend): await backend.execute(self._schema_sql(), options=options) def _create_async_context_factory(self, config): - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) @asynccontextmanager async def context_factory(): @@ -177,7 +181,9 @@ def _fastapi_runtime_config(self, connection_strategy): ) from exc async def _create_async_pool(self, config, runtime_config): - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) from rhosocial.activerecord.connection.pool import AsyncBackendPool, PoolConfig pool_config = PoolConfig( diff --git a/tests/providers/mixins.py b/tests/providers/mixins.py index 9c49f9c4..42f844c1 100644 --- a/tests/providers/mixins.py +++ b/tests/providers/mixins.py @@ -183,7 +183,9 @@ def __init__(self): self._active_async_backends = [] async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) await model_class.configure(config, AsyncPostgresBackend) backend_instance = model_class.__backend__ diff --git a/tests/providers/partition.py b/tests/providers/partition.py index dce8ed1d..571711e9 100644 --- a/tests/providers/partition.py +++ b/tests/providers/partition.py @@ -16,7 +16,7 @@ PartitionStrategy, TruncateExpression, ) -from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresAttachPartitionExpression, PostgresColumnDefinition, diff --git a/tests/providers/query.py b/tests/providers/query.py index e7d87b95..6eab9b81 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -468,7 +468,9 @@ def __init__(self): self._active_async_backends = [] async def _setup_async_model(self, model_class: Type[ActiveRecord], scenario_name: str, table_name: str, shared_backend=None) -> Type[ActiveRecord]: - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) if shared_backend is None: await model_class.configure(config, AsyncPostgresBackend) @@ -580,7 +582,9 @@ async def setup_profile_fixtures(self, scenario_name: str) -> Tuple[Type[ActiveR async def setup_order_item_model(self, scenario_name: str) -> Type[AsyncActiveRecord]: """Set up the composite-PK AsyncOrderItem model for the query feature tests.""" - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) from rhosocial.activerecord.backend.options import ExecutionOptions, StatementType from providers.fixtures._common import drop_table from providers.fixtures.basic import TABLE_EXPRESSIONS as BASIC_EXPRS @@ -609,10 +613,10 @@ async def setup_schema_fixtures(self, scenario_name: str): AsyncSchemaOrder, ) - from rhosocial.activerecord.backend.impl.postgres.backend import ( + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( AsyncPostgresBackend, - PostgresBackend, ) + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend _, config = get_scenario(scenario_name) @@ -665,7 +669,9 @@ async def cleanup_after_test(self, scenario_name: str): async def setup_mixed_schema_fixtures(self, scenario_name: str): """(AsyncUser, AsyncOrder, AsyncMixedSchemaOrder) with orders also in SCHEMA_A.""" - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) from rhosocial.activerecord.testsuite.feature.query.cross_schema.mixed_schema_models import ( AsyncMixedSchemaOrder, ) diff --git a/tests/providers/query_connection.py b/tests/providers/query_connection.py index dc86d651..f26e8a00 100644 --- a/tests/providers/query_connection.py +++ b/tests/providers/query_connection.py @@ -103,7 +103,9 @@ def setup_sync_pool_and_model(self, scenario_name: str) -> Tuple[BackendPool, Ty async def setup_async_pool_and_model(self, scenario_name: str) -> Tuple[AsyncBackendPool, Type[AsyncActiveRecord]]: """Setup async connection pool and model for query context tests.""" - from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend + from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import ( + AsyncPostgresBackend, + ) _, config = get_scenario(scenario_name) diff --git a/tests/providers/relation.py b/tests/providers/relation.py index 036e2d90..2ec56c93 100644 --- a/tests/providers/relation.py +++ b/tests/providers/relation.py @@ -2,7 +2,7 @@ from typing import Dict, List, Tuple, Type, Set from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend from rhosocial.activerecord.backend.impl.postgres.adapters.json import PostgresJSONBAdapter from rhosocial.activerecord.testsuite.feature.relation.interfaces import IRelationSyncProvider, IRelationAsyncProvider from rhosocial.activerecord.testsuite.feature.relation.fixtures.models import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py index 7b3bef06..3fdec40c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/conftest.py @@ -7,10 +7,8 @@ import os from typing import Dict, Any, Tuple, Type # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres.backend import ( - PostgresBackend, - AsyncPostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py index e0c6ea4a..e1082eaa 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_connection_config.py @@ -14,10 +14,8 @@ import pytest -from rhosocial.activerecord.backend.impl.postgres.backend import ( - AsyncPostgresBackend, - PostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py index 31178d0d..3e9cb636 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/postgres/test_pg19_copy_repack.py @@ -16,10 +16,8 @@ TableExpression, ) from rhosocial.activerecord.backend.expression.serialization import deserialize, serialize -from rhosocial.activerecord.backend.impl.postgres.backend import ( - AsyncPostgresBackend, - PostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.expression import ( PostgresCopyFromExpression, PostgresCopyLogVerbosity, diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py index 16cae9b8..8e1db137 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_backend_initialization.py @@ -1,9 +1,7 @@ import pytest -from rhosocial.activerecord.backend.impl.postgres.backend import ( - AsyncPostgresBackend, - PostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.backend.impl.postgres.dialect import PostgresDialect from rhosocial.activerecord.backend.impl.postgres.transaction import ( diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py index 87d126b3..33580f3b 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_connection_resilience.py @@ -23,10 +23,8 @@ import time import logging -from rhosocial.activerecord.backend.impl.postgres.backend import ( - PostgresBackend, - AsyncPostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend logger = logging.getLogger(__name__) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py index dfb87254..51adb382 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_version.py @@ -2,10 +2,8 @@ import pytest import pytest_asyncio # noqa: F401 -from rhosocial.activerecord.backend.impl.postgres.backend import ( - PostgresBackend, - AsyncPostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend class TestPostgresVersion: diff --git a/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py b/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py index 4ef41f93..91197815 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/connection/conftest.py @@ -14,10 +14,8 @@ import pytest_asyncio import yaml -from rhosocial.activerecord.backend.impl.postgres.backend import ( - PostgresBackend, - AsyncPostgresBackend, -) +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig from rhosocial.activerecord.connection.pool import ( PoolConfig, From f9688c2acca659263d6f2556f2815712414a8101 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 17:55:15 +0800 Subject: [PATCH 07/19] refactor(backend): re-export the async class from the backend package impl..backend already re-exported the sync class, so leaving the async one out made the two classes reachable by different rules. Export both. The eight backends whose async driver is a hard requirement import it eagerly. SQLite and SQL Server keep a PEP 562 __getattr__, because aiosqlite and aioodbc ship as the [async] extra and importing either eagerly would make the async class a requirement for sync-only users. --- .../backend/impl/postgres/backend/__init__.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py index cb97b759..09f72d7d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py @@ -1,17 +1,16 @@ # src/rhosocial/activerecord/backend/impl/postgres/backend/__init__.py """PostgreSQL backend implementations. -This module provides the synchronous PostgreSQL backend implementation. The -async counterpart lives in ``impl.postgres.async_backend``; every backend -follows that layout, so the sync class is always at ``impl..backend`` -and the async class at ``impl..async_backend``. +Every backend keeps both classes in this package: the sync class in +``backend.py`` and the async class in ``async_backend.py``. So the sync class +is at ``impl.postgres.backend.backend`` and the async class at +``impl.postgres.backend.async_backend``, and both are re-exported here. """ -from ..backend_base import PostgresBackendMixin -from .sync import PostgresBackend - +from .backend import PostgresBackend +from .async_backend import AsyncPostgresBackend __all__ = [ - "PostgresBackendMixin", "PostgresBackend", + "AsyncPostgresBackend", ] From 43c4d3984c49c512b7e3f7ee2e26f576e68bf992 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 18:29:15 +0800 Subject: [PATCH 08/19] fix(test): assert the storage-parameter enum on its owning module The test checked that impl.postgres re-exported PostgresStorageParameter, which is a property of the flat facade that has been removed. Assert it on impl.postgres.storage_parameters instead. --- .../feature/backend/dialect/test_materialized_view.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py index 957e192f..bfb5145c 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/dialect/test_materialized_view.py @@ -690,11 +690,11 @@ def test_min_version_metadata(self): assert PostgresStorageParameter.AUTOVACUUM_PARALLEL_WORKERS.min_version == (16, 0, 0) assert PostgresStorageParameter.FILLFACTOR.min_version is None - def test_enum_exported_from_package_root(self): - from rhosocial.activerecord.backend.impl import postgres as pg + def test_enum_exported_from_owning_package(self): + from rhosocial.activerecord.backend.impl.postgres import storage_parameters as pkg - assert pg.PostgresStorageParameter is PostgresStorageParameter - assert "PostgresStorageParameter" in pg.__all__ + assert pkg.PostgresStorageParameter is PostgresStorageParameter + assert "PostgresStorageParameter" in pkg.__all__ def test_expressions_render_enum_members(self): """Enum members and their string values render identically.""" From 1ac9220369bb8e58884fa4a934f9f994a5d8bd78 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 19:02:35 +0800 Subject: [PATCH 09/19] fix(providers): point backend_module at the backend package The worker tasks resolve the backend class by importing backend_module and reading the attribute off it. Pointing that at impl.postgres, now a bare namespace, made every worker task fail with "module has no attribute 'PostgresBackend'". The package that re-exports the class is impl.postgres.backend. --- tests/providers/basic.py | 2 +- tests/providers/query.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/providers/basic.py b/tests/providers/basic.py index 065398fa..7547b1f4 100644 --- a/tests/providers/basic.py +++ b/tests/providers/basic.py @@ -356,7 +356,7 @@ def get_worker_connection_params(self, scenario_name: str, fixture_type: str = N if pooled_db: config_dict = {**config_dict, "database": pooled_db} return { - 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', + 'backend_module': 'rhosocial.activerecord.backend.impl.postgres.backend', 'backend_class_name': backend_class_name, 'config_class_module': 'rhosocial.activerecord.backend.impl.postgres.config', 'config_class_name': 'PostgresConnectionConfig', diff --git a/tests/providers/query.py b/tests/providers/query.py index 6eab9b81..142790bc 100644 --- a/tests/providers/query.py +++ b/tests/providers/query.py @@ -417,7 +417,7 @@ def get_worker_connection_params(self, scenario_name: str, fixture_type: str = ' if pooled_db: config_dict = {**config_dict, "database": pooled_db} return { - 'backend_module': 'rhosocial.activerecord.backend.impl.postgres', + 'backend_module': 'rhosocial.activerecord.backend.impl.postgres.backend', 'backend_class_name': backend_class_name, 'config_class_module': 'rhosocial.activerecord.backend.impl.postgres.config', 'config_class_name': 'PostgresConnectionConfig', From bfc5a12922da4b32f342da74b204e084d736d220 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 19:20:34 +0800 Subject: [PATCH 10/19] fix(test): patch the async backend on the module the CLI reads The CLI resolves AsyncPostgresBackend inside backend_async_factory by importing impl.postgres.backend.async_backend, so patching the bare impl.postgres namespace left the real class in place. Point the patch at the defining module. --- .../activerecord_postgres_test/feature/backend/test_cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli.py index f4110306..4c685b42 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_cli.py @@ -428,7 +428,8 @@ def fake_handle(args, provider, **kwargs): 'resolve_connection_config_from_args', return_value='config', ), patch( - 'rhosocial.activerecord.backend.impl.postgres.AsyncPostgresBackend', + 'rhosocial.activerecord.backend.impl.postgres.backend.async_backend.' + 'AsyncPostgresBackend', return_value=async_backend, ) as backend_cls, patch( 'rhosocial.activerecord.backend.named_expression.cli_procedure_graph.' From 73a03390f7a544f4e0b43e2804e290122098e5fb Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 21:50:02 +0800 Subject: [PATCH 11/19] fix(postgres): drop the stale backend_base module and point at backend/base Constructing either backend raised ModuleNotFoundError: No module named 'rhosocial.activerecord.backend.impl.adapters'. The adapter registration in backend_base.py reached for ..adapters, but backend_base.py sits directly in impl/postgres, so the second dot climbed to impl and adapters resolved one level too high. The identical text in backend/base.py is correct only because that file sits one directory deeper. backend_base.py was a byte-for-byte copy of backend/base.py, kept alive only by two stale modules: backend/sync.py, itself a copy of backend/backend.py differing in nothing but that same import, and the pre-restructure top-level async_backend.py. Deleting backend_base.py without those would trade one breakage for another, so all three import sites now name backend/base.py. Both classes now construct and build their dialect, verified against PostgresConnectionConfig. Also corrects backend/base.py's file header, which still named the deleted path. --- .../backend/impl/postgres/async_backend.py | 2 +- .../backend/impl/postgres/backend/base.py | 2 +- .../backend/impl/postgres/backend/sync.py | 2 +- .../backend/impl/postgres/backend_base.py | 416 ------------------ 4 files changed, 3 insertions(+), 419 deletions(-) delete mode 100644 src/rhosocial/activerecord/backend/impl/postgres/backend_base.py diff --git a/src/rhosocial/activerecord/backend/impl/postgres/async_backend.py b/src/rhosocial/activerecord/backend/impl/postgres/async_backend.py index 795fe2cc..a5fb3f41 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/async_backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/async_backend.py @@ -53,7 +53,7 @@ from rhosocial.activerecord.backend.result import QueryResult from .config import PostgresConnectionConfig from .dialect import PostgresDialect -from .backend_base import PostgresBackendMixin, PostgresConcurrencyMixin +from .backend.base import PostgresBackendMixin, PostgresConcurrencyMixin from .protocols import PostgresExtensionInfo from .transaction import AsyncPostgresTransactionManager diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py index e916a345..7abb647e 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/base.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/backend_base.py +# src/rhosocial/activerecord/backend/impl/postgres/backend/base.py """PostgreSQL backend shared functionality. This module provides the base mixin class for PostgreSQL backend implementations, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py index 0d8134dd..25238c2b 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py @@ -42,7 +42,7 @@ from rhosocial.activerecord.backend.result import QueryResult from ..config import PostgresConnectionConfig from ..dialect import PostgresDialect -from ..backend_base import PostgresBackendMixin, PostgresConcurrencyMixin +from .base import PostgresBackendMixin, PostgresConcurrencyMixin from ..protocols import PostgresExtensionInfo from ..transaction import PostgresTransactionManager from ..introspection import SyncPostgreSQLIntrospector diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend_base.py b/src/rhosocial/activerecord/backend/impl/postgres/backend_base.py deleted file mode 100644 index e916a345..00000000 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend_base.py +++ /dev/null @@ -1,416 +0,0 @@ -# src/rhosocial/activerecord/backend/impl/postgres/backend_base.py -"""PostgreSQL backend shared functionality. - -This module provides the base mixin class for PostgreSQL backend implementations, -containing methods that are shared between sync and async backends. -""" - -from typing import Dict, Tuple, Type, TYPE_CHECKING -import logging - -if TYPE_CHECKING: - from rhosocial.activerecord.backend.type_adapter import SQLTypeAdapter - from rhosocial.activerecord.backend.protocols import ConcurrencyHint - - -class PostgresBackendMixin: - """PostgreSQL backend shared methods mixin. - - This mixin provides methods that are shared between sync and async - PostgreSQL backend implementations. These methods do not involve - I/O operations and have identical implementations in both backends. - - Classes using this mixin must provide: - - self._dialect: PostgresDialect instance - - self.adapter_registry: Type adapter registry - - self.config: Connection configuration - - self._logger: Logger instance - """ - - # PostgreSQL connection error SQLSTATE codes - # Reference: https://www.postgresql.org/docs/current/errcodes-appendix.html - CONNECTION_ERROR_SQLSTATES = { - "08000", # CONNECTION_EXCEPTION - "08001", # SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION - "08003", # CONNECTION_DOES_NOT_EXIST - "08004", # SQLSERVER_REJECTED_ESTABLISHMENT_OF_SQLCONNECTION - "08006", # CONNECTION_FAILURE - "08007", # TRANSACTION_RESOLUTION_UNKNOWN - "08P01", # PROTOCOL_VIOLATION - "57P01", # ADMIN_SHUTDOWN - "57P02", # CRASH_SHUTDOWN - "57P03", # CANNOT_CONNECT_NOW - "57P04", # DATABASE_DROPPED - } - - def log(self, level: int, message: str): - """Log a message with the specified level.""" - if hasattr(self, "_logger") and self._logger: - self._logger.log(level, message) - else: - # Fallback logging - print(f"[{logging.getLevelName(level)}] {message}") - - def get_default_schema(self) -> str: - """Get the default schema name for PostgreSQL. - - Returns the configured default_schema, or 'public' as the - PostgreSQL standard default if not explicitly set. - - Returns: - str: Default schema name - """ - return getattr(self.config, "default_schema", None) or "public" - - def create_expression(self, expression_str: str): - """Create an expression object for raw SQL expressions.""" - from rhosocial.activerecord.backend.expression.operators import RawSQLExpression - - return RawSQLExpression(self.dialect, expression_str) - - @property - def threadsafety(self) -> int: - """Return driver threadsafety level. - - PostgreSQL connections can be safely shared across threads when using - psycopg version 3, which supports thread-safe connection handling. - - Returns: - 2 (connections can be safely shared across threads) - """ - return 2 - - def requires_manual_commit(self) -> bool: - """Check if manual commit is required for this database.""" - return not self.config.autocommit - - def _register_postgres_adapters(self): - """Register PostgreSQL-specific type adapters. - - Range and Multirange adapter registration is controlled by configuration: - - range_adapter_mode: Controls how Range types are handled - - NATIVE (default): Use psycopg's native Range type (pass-through) - - CUSTOM: Use PostgresRange custom type - - BOTH: Register both adapters, with custom taking precedence - - multirange_adapter_mode: Same for Multirange types (PG 14+ only) - - Note: The following adapters are NOT registered by default due to str->str - type mapping conflicts with existing string handling: - - PostgresXMLAdapter: XML type - - PostgresBitStringAdapter: bit/varbit types - - PostgresJsonPathAdapter: jsonpath type - - Users should specify these explicitly when working with such columns. - """ - from ..adapters import ( - PostgresListAdapter, - PostgresJSONBAdapter, - PostgresNetworkAddressAdapter, - PostgresEnumAdapter, - PostgresRangeAdapter, - PostgresMultirangeAdapter, - ) - from ..adapters.geometric import PostgresGeometryAdapter - from ..adapters.monetary import PostgresMoneyAdapter - from ..adapters.network_address import PostgresMacaddrAdapter, PostgresMacaddr8Adapter - from ..adapters.object_identifier import PostgresOidAdapter, PostgresXidAdapter, PostgresTidAdapter - from ..adapters.pg_lsn import PostgresLsnAdapter - from ..adapters.text_search import PostgresTsVectorAdapter, PostgresTsQueryAdapter - from ..config import RangeAdapterMode - - pg_adapters = [ - PostgresListAdapter(), - PostgresJSONBAdapter(), - PostgresNetworkAddressAdapter(), - PostgresGeometryAdapter(), - PostgresEnumAdapter(), - PostgresMoneyAdapter(), - PostgresMacaddrAdapter(), - PostgresMacaddr8Adapter(), - PostgresTsVectorAdapter(), - PostgresTsQueryAdapter(), - PostgresLsnAdapter(), - PostgresOidAdapter(), - PostgresXidAdapter(), - PostgresTidAdapter(), - # PostgresJsonPathAdapter(), # Not registered: str->str conflict - # PostgresXMLAdapter is NOT registered by default - # due to str->str type pair conflict - ] - - for adapter in pg_adapters: - for py_type, db_types in adapter.supported_types.items(): - for db_type in db_types: - self.adapter_registry.register(adapter, py_type, db_type) - - # Register Range adapters based on configuration - range_mode = getattr(self.config, "range_adapter_mode", RangeAdapterMode.NATIVE) - if range_mode in (RangeAdapterMode.CUSTOM, RangeAdapterMode.BOTH): - range_adapter = PostgresRangeAdapter() - for py_type, db_types in range_adapter.supported_types.items(): - for db_type in db_types: - self.adapter_registry.register(range_adapter, py_type, db_type) - self.log(logging.DEBUG, "Registered PostgresRangeAdapter (custom range type)") - - # Register Multirange adapters based on configuration (PG 14+ only) - # Note: Version check happens in dialect, but at this point we haven't - # connected yet, so we register if configured. The adapter itself will - # handle version-specific logic if needed. - multirange_mode = getattr(self.config, "multirange_adapter_mode", RangeAdapterMode.NATIVE) - if multirange_mode in (RangeAdapterMode.CUSTOM, RangeAdapterMode.BOTH): - multirange_adapter = PostgresMultirangeAdapter() - for py_type, db_types in multirange_adapter.supported_types.items(): - for db_type in db_types: - self.adapter_registry.register(multirange_adapter, py_type, db_type) - self.log(logging.DEBUG, "Registered PostgresMultirangeAdapter (custom multirange type)") - - self.log(logging.DEBUG, "Registered PostgreSQL-specific type adapters") - - def _is_connection_error(self, error: Exception) -> bool: - """Check if an error indicates a connection loss. - - This method identifies errors that result from lost or invalid connections, - which should trigger automatic reconnection attempts. - - Args: - error: The exception to check - - Returns: - True if the error indicates a connection problem, False otherwise - """ - from psycopg.errors import OperationalError as PsycopgOperationalError - - if isinstance(error, PsycopgOperationalError): - # Check SQLSTATE code if available - sqlstate = getattr(error, 'sqlstate', None) - if sqlstate and sqlstate in self.CONNECTION_ERROR_SQLSTATES: - return True - - # Check for common connection error patterns in message - error_msg = str(error).lower() - connection_error_patterns = [ - 'connection', - 'connect', - 'closed', - 'terminated', - 'unexpectedly', - 'broken pipe', - 'reset by peer', - 'server closed', - 'server has gone away', - ] - for pattern in connection_error_patterns: - if pattern in error_msg: - return True - - return False - - def _is_connection_error_message(self, error_msg: str) -> bool: - """Check if an error message indicates a connection loss. - - This method identifies error messages that result from lost or invalid connections, - which should trigger automatic reconnection attempts. - - Args: - error_msg: The error message to check - - Returns: - True if the message indicates a connection problem, False otherwise - """ - error_msg_lower = error_msg.lower() - connection_error_patterns = [ - 'connection', - 'connect', - 'closed', - 'terminated', - 'terminating connection due to administrator command', - 'unexpectedly', - 'broken pipe', - 'reset by peer', - 'server closed', - 'server has gone away', - 'ssl connection has been closed', - 'consuming input failed', - ] - for pattern in connection_error_patterns: - if pattern in error_msg_lower: - return True - return False - - def _parse_explain_result(self, raw_rows, sql, duration): - """Return a typed :class:`PostgresExplainResult` for PostgreSQL EXPLAIN output.""" - from ..explain import PostgresExplainResult, PostgresExplainPlanLine - rows = [PostgresExplainPlanLine(line=r.get("QUERY PLAN", "")) for r in raw_rows] - return PostgresExplainResult(raw_rows=raw_rows, sql=sql, duration=duration, rows=rows) - - def get_default_adapter_suggestions(self) -> Dict[Type, Tuple["SQLTypeAdapter", Type]]: - """ - [Backend Implementation] Provides default type adapter suggestions for PostgreSQL. - - This method defines a curated set of type adapter suggestions for common Python - types, mapping them to their typical PostgreSQL-compatible representations as - demonstrated in test fixtures. It explicitly retrieves necessary `SQLTypeAdapter` - instances from the backend's `adapter_registry`. If an adapter for a specific - (Python type, DB driver type) pair is not registered, no suggestion will be - made for that Python type. - - Returns: - Dict[Type, Tuple[SQLTypeAdapter, Type]]: A dictionary where keys are - original Python types (`TypeRegistry`'s `py_type`), and values are - tuples containing a `SQLTypeAdapter` instance and the target - Python type (`TypeRegistry`'s `db_type`) expected by the driver. - - Note: - PostgreSQL XML and BitString types are NOT included here due to str->str - mapping conflicts with the base string handling. Users must explicitly - specify adapters for these types when needed. - """ - suggestions: Dict[Type, Tuple["SQLTypeAdapter", Type]] = {} - - # Define a list of desired Python type to DB driver type mappings. - # This list reflects types seen in test fixtures and common usage, - # along with their preferred database-compatible Python types for the driver. - # Types that are natively compatible with the DB driver (e.g., Python str, int, float) - # and for which no specific conversion logic is needed are omitted from this list. - # The consuming layer should assume pass-through behavior for any Python type - # that does not have an explicit adapter suggestion. - # - # Exception: If a user requires specific processing for a natively compatible type - # (e.g., custom serialization/deserialization for JSON strings beyond basic conversion), - # they would need to implement and register their own specialized adapter. - # This backend's default suggestions do not cater to such advanced processing needs. - from datetime import date, datetime, time - from decimal import Decimal - from uuid import UUID - from enum import Enum - - # Import PostgreSQL-specific types - from ..type_values.range import PostgresRange, PostgresMultirange - from ..type_values.geometric import Point, Line, LineSegment, Box, Path, Polygon, Circle - from ..type_values.text_search import PostgresTsVector, PostgresTsQuery - from ..type_values.monetary import PostgresMoney - from ..type_values.network_address import PostgresMacaddr, PostgresMacaddr8 - from ..type_values.pg_lsn import PostgresLsn - from ..type_values.json import PostgresJsonPath - from ..type_values.object_identifier import ( - OID, - RegClass, - RegType, - RegProc, - RegProcedure, - RegOper, - RegOperator, - RegConfig, - RegDictionary, - RegNamespace, - RegRole, - RegCollation, - XID, - XID8, - CID, - TID, - ) - - type_mappings = [ - # === Standard Python Types === - (bool, bool), # Python bool -> DB driver bool (PostgreSQL BOOLEAN) - # Why str for date/time? - # PostgreSQL has native DATE, TIME, TIMESTAMP types but accepts string representations - (datetime, str), # Python datetime -> DB driver str (PostgreSQL TIMESTAMP) - (date, str), # Python date -> DB driver str (PostgreSQL DATE) - (time, str), # Python time -> DB driver str (PostgreSQL TIME) - (Decimal, Decimal), # Python Decimal -> DB driver Decimal (PostgreSQL NUMERIC/DECIMAL) - (UUID, str), # Python UUID -> DB driver str (PostgreSQL UUID type) - (dict, str), # Python dict -> DB driver str (PostgreSQL JSON/JSONB) - (list, list), # Python list -> DB driver list (PostgreSQL arrays - psycopg handles natively) - (Enum, str), # Python Enum -> DB driver str (PostgreSQL TEXT) - # === PostgreSQL Range Types === - (PostgresRange, str), # PostgreSQL range types (int4range, daterange, etc.) - (PostgresMultirange, str), # PostgreSQL multirange types (PG 14+) - # === PostgreSQL Geometric Types === - # All geometric types map to str representation - (Point, str), # 2D point (x, y) - (Line, str), # Infinite line {A, B, C} - (LineSegment, str), # Line segment [(x1,y1),(x2,y2)] - (Box, str), # Rectangular box (x1,y1),(x2,y2) - (Path, str), # Sequence of points (open or closed) - (Polygon, str), # Closed path - (Circle, str), # Circle <(x,y),r> - # === PostgreSQL Text Search Types === - (PostgresTsVector, str), # Text search vector - (PostgresTsQuery, str), # Text search query - # === PostgreSQL Monetary Type === - (PostgresMoney, str), # MONEY type with locale-aware formatting - # === PostgreSQL Network Address Types === - (PostgresMacaddr, str), # 6-byte MAC address - (PostgresMacaddr8, str), # 8-byte MAC address - # === PostgreSQL pg_lsn Type === - (PostgresLsn, str), # Log Sequence Number - # === PostgreSQL JSON Path Type === - (PostgresJsonPath, str), # JSON path expression - # === PostgreSQL Object Identifier Types === - (OID, int), # Unsigned 4-byte integer for internal object identification - (RegClass, str), # Relation (table/view/sequence) name - (RegType, str), # Data type name - (RegProc, str), # Function name - (RegProcedure, str), # Function name with argument types - (RegOper, str), # Operator name - (RegOperator, str), # Operator name with argument types - (RegConfig, str), # Text search configuration name - (RegDictionary, str), # Text search dictionary name - (RegNamespace, str), # Namespace (schema) name - (RegRole, str), # Role (user/group) name - (RegCollation, str), # Collation name - # === PostgreSQL Transaction/Command Identifiers === - (XID, int), # 32-bit transaction identifier - (XID8, int), # 64-bit transaction identifier (PG 13+) - (CID, int), # Command identifier - # === PostgreSQL Tuple Identifier === - (TID, str), # Tuple identifier (block number, offset) - # Note: PostgresXML and PostgresBitString are NOT included here - # because they both map to str, which conflicts with base string handling. - # Users must explicitly specify adapters for these types. - ] - - # Iterate through the defined mappings and retrieve adapters from the registry. - for py_type, db_type in type_mappings: - adapter = self.adapter_registry.get_adapter(py_type, db_type) - if adapter: - suggestions[py_type] = (adapter, db_type) - else: - # Log a debug message if a specific adapter is expected but not found. - self.log( - logging.DEBUG, - f"No adapter found for ({py_type.__name__}, {db_type.__name__}). " - "Suggestion will not be provided for this type.", - ) - - return suggestions - - -class PostgresConcurrencyMixin: - """Mixin providing PostgreSQL-specific concurrency hint. - - Uses PostgreSQL connection pool for concurrency limit. - Returns pool.size as the concurrency limit. - """ - - def get_concurrency_hint(self) -> "ConcurrencyHint": - """Get concurrency hint based on connection pool size.""" - from rhosocial.activerecord.backend.protocols import ConcurrencyHint - - pool = getattr(self, "_pool", None) - if pool and hasattr(pool, "size"): - max_concurrency = pool.size - return ConcurrencyHint( - max_concurrency=max_concurrency, - reason=f"connection pool size={max_concurrency}", - ) - # No pool - return None to indicate no constraint - return ConcurrencyHint( - max_concurrency=None, - reason="no connection pool configured", - ) - - -__all__ = ["PostgresBackendMixin", "PostgresConcurrencyMixin"] From b461c79a2fc496d26f048e4f4f0533d956135040 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Mon, 28 Sep 2026 22:49:28 +0800 Subject: [PATCH 12/19] fix(postgres): remove the last stale top-level async_backend The remaining pre-restructure copy of async_backend.py, left over from moving both classes into the backend package. It differed from backend/async_backend.py only in its import depths, and those depths were wrong: ..expression and ..introspection climbed to impl.expression and impl.introspection, one level short of backend.expression and impl.postgres.introspection. They sit inside function bodies, so nothing failed until the code path ran. The canonical backend/async_backend.py resolves both correctly from its deeper position. Deleting the stale copy rather than correcting it also keeps a single definition per class; the two introspection docs now name the canonical path. Verified both classes still construct, and that resolving every relative import across all ten backend repositories and the core now finds no unresolvable module, down from 19. --- .../introspection.md | 2 +- .../introspection.md | 2 +- .../backend/impl/postgres/async_backend.py | 921 ------------------ 3 files changed, 2 insertions(+), 923 deletions(-) delete mode 100644 src/rhosocial/activerecord/backend/impl/postgres/async_backend.py diff --git a/docs/en_US/postgres_specific_features/introspection.md b/docs/en_US/postgres_specific_features/introspection.md index 11c63e57..78c5fee2 100644 --- a/docs/en_US/postgres_specific_features/introspection.md +++ b/docs/en_US/postgres_specific_features/introspection.md @@ -253,7 +253,7 @@ for idx in indexes: The async backend provides identical introspection methods with the same names as the sync version: ```python -rhosocial.activerecord.backend.impl.postgres.async_backend import AsyncPostgresBackend +rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend backend = AsyncPostgresBackend( host="localhost", port=5432, diff --git a/docs/zh_CN/postgres_specific_features/introspection.md b/docs/zh_CN/postgres_specific_features/introspection.md index 5ebf771b..53960aa8 100644 --- a/docs/zh_CN/postgres_specific_features/introspection.md +++ b/docs/zh_CN/postgres_specific_features/introspection.md @@ -253,7 +253,7 @@ for idx in indexes: 异步后端提供相同的内省方法,方法名与同步版本相同: ```python -rhosocial.activerecord.backend.impl.postgres.async_backend import AsyncPostgresBackend +rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend backend = AsyncPostgresBackend( host="localhost", port=5432, diff --git a/src/rhosocial/activerecord/backend/impl/postgres/async_backend.py b/src/rhosocial/activerecord/backend/impl/postgres/async_backend.py deleted file mode 100644 index a5fb3f41..00000000 --- a/src/rhosocial/activerecord/backend/impl/postgres/async_backend.py +++ /dev/null @@ -1,921 +0,0 @@ -# src/rhosocial/activerecord/backend/impl/postgres/async_backend.py -"""PostgreSQL asynchronous backend implementation. - -This module provides an asynchronous PostgreSQL implementation: -- PostgreSQL asynchronous backend with connection management and query execution -- PostgreSQL-specific connection configuration -- Type mapping and value conversion -- Transaction management with savepoint support -- PostgreSQL dialect and expression handling -- PostgreSQL-specific type definitions and mappings - -Architecture: -- AsyncPostgresBackend: Asynchronous implementation using psycopg -- Independent from ORM frameworks - uses only native drivers -""" - -import datetime -import logging -from typing import ( - Any, - AsyncIterable, - Dict, - Iterable, - List, - Optional, - Tuple, - TYPE_CHECKING, - Union, -) - -import psycopg -from psycopg import AsyncConnection -from psycopg.errors import Error as PsycopgError -from psycopg.errors import ( - IntegrityError as PsycopgIntegrityError, - OperationalError as PsycopgOperationalError, - ProgrammingError as PsycopgProgrammingError, - DeadlockDetected as PsycopgDeadlockError, -) - -from rhosocial.activerecord.backend.base import AsyncStorageBackend -from rhosocial.activerecord.backend.introspection.backend_mixin import IntrospectorBackendMixin -from rhosocial.activerecord.backend.explain import AsyncExplainBackendMixin -from rhosocial.activerecord.backend.errors import ( - ConnectionError, - DatabaseError, - DeadlockError, - IntegrityError, - OperationalError, - QueryError, -) -from rhosocial.activerecord.backend.options import ExecutionOptions -from rhosocial.activerecord.backend.result import QueryResult -from .config import PostgresConnectionConfig -from .dialect import PostgresDialect -from .backend.base import PostgresBackendMixin, PostgresConcurrencyMixin -from .protocols import PostgresExtensionInfo -from .transaction import AsyncPostgresTransactionManager - -if TYPE_CHECKING: - from ..expression import PostgresCopyFromExpression, PostgresCopyToExpression - - -CopyChunk = Union[str, bytes, bytearray, memoryview] -AsyncCopyInput = Union[ - CopyChunk, - Iterable[CopyChunk], - AsyncIterable[CopyChunk], -] - - -class AsyncPostgresBackend( - IntrospectorBackendMixin, - PostgresBackendMixin, - AsyncExplainBackendMixin, - PostgresConcurrencyMixin, - AsyncStorageBackend, -): - """Asynchronous PostgreSQL-specific backend implementation.""" - - def __init__(self, **kwargs: Any) -> None: - """Initialize async PostgreSQL backend with connection configuration. - - Note: - The dialect is initialized with a default version (9.6.0) and no plugins - are enabled. Call introspect_and_adapt() after connecting to detect - the actual server version and installed extensions. - """ - graph_feature_overrides = kwargs.pop("graph_feature_overrides", None) - - # Ensure we have proper PostgreSQL configuration - connection_config = kwargs.get("connection_config") - - if connection_config is None: - # Extract PostgreSQL-specific parameters from kwargs - config_params = {} - pg_specific_params = [ - "host", - "port", - "database", - "username", - "password", - "charset", - "collation", - "timezone", - "version", - "pool_size", - "pool_timeout", - "pool_name", - "pool_reset_session", - "pool_pre_ping", - "ssl_ca", - "ssl_cert", - "ssl_key", - "ssl_verify_cert", - "ssl_verify_identity", - "log_queries", - "log_level", - "application_name", - "fallback_application_name", - "connect_timeout", - "options", - "service", - "target_session_attrs", - "gssencmode", - "channel_binding", - "replication", - "assume_role", - "role", - "search_path", - "row_security", - "datestyle", - "intervalstyle", - "timezone_setting", - "extra_float_digits", - "client_encoding", - "tcp_user_timeout", - "tcp_keepalives_idle", - "tcp_keepalives_interval", - "tcp_keepalives_count", - "load_balance_hosts", - "replication", - "keepalives", - "keepalives_idle", - "keepalives_interval", - "keepalives_count", - ] - - for param in pg_specific_params: - if param in kwargs: - config_params[param] = kwargs[param] - - # Set defaults if not provided - if "host" not in config_params: - config_params["host"] = "localhost" - if "port" not in config_params: - config_params["port"] = 5432 - - kwargs["connection_config"] = PostgresConnectionConfig(**config_params) - - # Initialize the base class first to ensure all base properties are set - super().__init__(**kwargs) - - # Initialize PostgreSQL-specific components - # Version is None until introspect_and_adapt() is called - self._dialect = PostgresDialect( - graph_feature_overrides=graph_feature_overrides, - ) - - # Register PostgreSQL-specific type adapters (same as sync backend) - # Note: XML adapter is NOT registered by default due to potential conflicts. - # See PostgresXMLAdapter documentation for details. - self._register_postgres_adapters() - - # Initialize transaction manager (will use backend.execute()) - self._transaction_manager = AsyncPostgresTransactionManager(self, self.logger) - - self.log(logging.INFO, "AsyncPostgresBackend initialized") - - def _create_introspector(self) -> Any: - """Create and return an AsyncPostgreSQLIntrospector with an async executor.""" - from rhosocial.activerecord.backend.introspection.executor import AsyncIntrospectorExecutor - from ..introspection import AsyncPostgreSQLIntrospector - - return AsyncPostgreSQLIntrospector(self, AsyncIntrospectorExecutor(self)) - - @property - def dialect(self) -> PostgresDialect: - """Get the PostgreSQL dialect instance.""" - return self._dialect - - @property - def transaction_manager(self) -> AsyncPostgresTransactionManager: - """Get the async PostgreSQL transaction manager.""" - return self._transaction_manager - - - async def connect(self) -> None: - """Establish async connection to PostgreSQL database.""" - try: - # Prepare connection parameters from config - conn_params = { - "host": self.config.host, - "port": self.config.port, - "dbname": self.config.database, - "user": self.config.username, - "password": self.config.password, - } - - # Add additional parameters if they exist in config - additional_params = [ - "application_name", - "fallback_application_name", - "connect_timeout", - "options", - "service", - "target_session_attrs", - "gssencmode", - "channel_binding", - "replication", - "assume_role", - "role", - "search_path", - "row_security", - "datestyle", - "intervalstyle", - "timezone", - "extra_float_digits", - "client_encoding", - "tcp_user_timeout", - "tcp_keepalives_idle", - "tcp_keepalives_interval", - "tcp_keepalives_count", - "load_balance_hosts", - "keepalives", - "keepalives_idle", - "keepalives_interval", - "keepalives_count", - ] - - for param in additional_params: - if hasattr(self.config, param): - value = getattr(self.config, param) - if value is not None: # Only add the parameter if it's not None - conn_params[param] = value - - # Handle 'options' parameter specially as it should be a string, not a dict - if hasattr(self.config, "options") and self.config.options is not None: - options_value = self.config.options - if isinstance(options_value, dict): - options_str = " ".join([f"-c {k}={v}" for k, v in options_value.items()]) - conn_params["options"] = options_str - else: - conn_params["options"] = options_value - - # Add SSL parameters if provided - ssl_params = {} - if hasattr(self.config, "ssl_ca"): - ssl_params["sslcert"] = self.config.ssl_ca - if hasattr(self.config, "ssl_cert"): - ssl_params["sslcert"] = self.config.ssl_cert - if hasattr(self.config, "ssl_key"): - ssl_params["sslkey"] = self.config.ssl_key - if hasattr(self.config, "ssl_mode"): - ssl_params["sslmode"] = self.config.ssl_mode - - if ssl_params: - conn_params.update(ssl_params) - - self._connection = await AsyncConnection.connect(**conn_params) - await self._connection.set_autocommit(True) # Disable psycopg auto-transaction management - - self.log( - logging.INFO, - f"Connected to PostgreSQL database: {self.config.host}:{self.config.port}/{self.config.database}", - ) - except PsycopgError as e: - self.log(logging.ERROR, f"Failed to connect to PostgreSQL database: {str(e)}") - raise ConnectionError(f"Failed to connect to PostgreSQL: {str(e)}") from e - - async def disconnect(self) -> None: - """Close async connection to PostgreSQL database.""" - if self._connection: - try: - # Rollback any active transaction - if self.in_transaction: - await self.transaction_manager.rollback() - - await self._connection.close() - self._connection = None - self.log(logging.INFO, "Disconnected from PostgreSQL database") - except PsycopgError as e: - self.log(logging.ERROR, f"Error during disconnection: {str(e)}") - raise OperationalError(f"Error during PostgreSQL disconnection: {str(e)}") from e - - async def _get_cursor(self) -> Any: - """Get a database cursor, ensuring connection is active. - - This method implements automatic connection health checking (Plan A): - - Checks if connection object exists - - Checks if connection is still valid using closed/broken attributes - - Automatically reconnects if connection was lost - """ - if not self._connection: - self.log(logging.DEBUG, "No connection, connecting...") - await self.connect() - elif self._connection.closed or self._connection.broken: - self.log(logging.DEBUG, "Connection lost, reconnecting...") - self._connection = None # Clear the closed/broken connection - await self.connect() - - return self._connection.cursor() - - @staticmethod - def _copy_output_chunk(chunk: Any) -> bytes: - if isinstance(chunk, bytes): - return chunk - if isinstance(chunk, str): - return chunk.encode("utf-8") - if isinstance(chunk, (bytearray, memoryview)): - return bytes(chunk) - raise TypeError("PostgreSQL COPY stream yielded a non-buffer chunk") - - @staticmethod - def _validate_copy_input_chunk( - chunk: Any, - binary: bool, - encoding: Optional[str] = None, - ) -> CopyChunk: - if not isinstance(chunk, (str, bytes, bytearray, memoryview)): - raise TypeError("COPY input chunks must be str or bytes-like objects") - if binary and isinstance(chunk, str): - raise TypeError("COPY FROM BINARY requires bytes-like input chunks") - if encoding is not None and isinstance(chunk, str): - raise TypeError("COPY FROM with ENCODING requires bytes-like input chunks") - return chunk - - async def copy_to(self, expression: "PostgresCopyToExpression") -> bytes: - """Execute COPY TO STDOUT and return the complete byte stream.""" - from ..expression import PostgresCopyToExpression - - if not isinstance(expression, PostgresCopyToExpression): - raise TypeError("expression must be a PostgresCopyToExpression") - sql, params = expression.to_sql() - chunks = [] - try: - async with await self._get_cursor() as cursor: - async with cursor.copy(sql, params) as copy: - async for chunk in copy: - chunks.append(self._copy_output_chunk(chunk)) - except BaseException as error: - if isinstance(error, PsycopgError): - await self._handle_error(error) - else: - await self._try_rollback_transaction() - raise - return b"".join(chunks) - - async def copy_from( - self, - expression: "PostgresCopyFromExpression", - data: AsyncCopyInput, - ) -> int: - """Stream COPY FROM STDIN data and return the server-reported row count.""" - from ..expression import PostgresCopyFromExpression - - if not isinstance(expression, PostgresCopyFromExpression): - raise TypeError("expression must be a PostgresCopyFromExpression") - sql, params = expression.to_sql() - binary = str(expression.format).lower() == "binary" - try: - async with await self._get_cursor() as cursor: - async with cursor.copy(sql, params) as copy: - if isinstance(data, (str, bytes, bytearray, memoryview)): - await copy.write( - self._validate_copy_input_chunk(data, binary, expression.encoding) - ) - elif hasattr(data, "__aiter__"): - async for chunk in data: - await copy.write( - self._validate_copy_input_chunk(chunk, binary, expression.encoding) - ) - else: - try: - chunks = iter(data) - except TypeError as error: - raise TypeError( - "COPY input must be bytes, str, or a sync/async iterable of chunks" - ) from error - for chunk in chunks: - await copy.write( - self._validate_copy_input_chunk(chunk, binary, expression.encoding) - ) - rowcount = int(cursor.rowcount) - if getattr(self, "_transaction_manager", None) is not None: - await self._handle_auto_commit_if_needed() - return rowcount - except BaseException as error: - if isinstance(error, PsycopgError): - await self._handle_error(error) - else: - await self._try_rollback_transaction() - raise - - async def execute_many(self, sql: str, params_list: List[Tuple]) -> QueryResult: - """Execute the same SQL statement multiple times with different parameters asynchronously.""" - if not self._connection: - await self.connect() - - cursor = None - start_time = datetime.datetime.now() - - try: - cursor = await self._get_cursor() - - # Log the batch operation if logging is enabled - if getattr(self.config, "log_queries", False): - self.log(logging.DEBUG, f"Executing batch operation: {sql}") - self.log(logging.DEBUG, f"With {len(params_list)} parameter sets") - - # Execute multiple statements - affected_rows = 0 - for params in params_list: - await cursor.execute(sql, params) - affected_rows += cursor.rowcount - - duration = (datetime.datetime.now() - start_time).total_seconds() - - result = QueryResult(affected_rows=affected_rows, data=None, duration=duration) - - self.log( - logging.INFO, - f"Batch operation completed, affected {affected_rows} rows, duration={duration:.3f}s", - ) - return result - - except PsycopgIntegrityError as e: - self.log(logging.ERROR, f"Integrity error in batch: {str(e)}") - raise IntegrityError(str(e)) from e - except PsycopgError as e: - self.log(logging.ERROR, f"PostgreSQL error in batch: {str(e)}") - raise DatabaseError(str(e)) from e - except Exception as e: - self.log(logging.ERROR, f"Unexpected error during batch execution: {str(e)}") - raise QueryError(str(e)) from e - finally: - if cursor: - await cursor.close() - - async def get_server_version(self) -> Optional[Tuple[int, int, int]]: - """Get PostgreSQL server version asynchronously. - - Returns: - Tuple of (major, minor, patch) version numbers, or None if version - cannot be determined. - """ - if not self._connection: - await self.connect() - - cursor = None - try: - cursor = await self._get_cursor() - await cursor.execute("SELECT version()") - version_row = await cursor.fetchone() - if not version_row or not version_row[0]: - self.log(logging.WARNING, "PostgreSQL version query returned no result") - return None - - version_str = version_row[0] - - # Extract version from string like "PostgreSQL 13.2..." - import re - - match = re.search(r"PostgreSQL (\d+)(?:\.(\d+))?(?:[^\d]*(\d+))?", version_str) - if match: - major = int(match.group(1)) - minor = int(match.group(2)) if match.group(2) else 0 - patch = int(match.group(3)) if match.group(3) else 0 - return (major, minor, patch) - else: - self.log(logging.WARNING, f"Could not parse PostgreSQL version: {version_str}") - return None - except Exception as e: - self.log(logging.WARNING, f"Could not determine PostgreSQL version: {str(e)}") - return None - finally: - if cursor: - await cursor.close() - - async def introspect_and_adapt(self) -> None: - """Introspect backend and adapt backend instance to actual server capabilities. - - This method ensures a connection exists, queries the actual PostgreSQL server version - and installed extensions, and updates the backend's internal state. - - Introspection includes: - 1. Server version: affects version-dependent feature support checks - 2. Installed extensions: queries pg_extension system table to determine plugin availability - - After detection, the dialect instance caches extension information, - which can be queried via is_extension_installed(). - - Note: If version cannot be determined, the default version (9.6.0) is retained. - """ - # Ensure connection exists - if not self._connection: - await self.connect() - - # Get server version - actual_version = await self.get_server_version() - - # If version cannot be determined, keep the default version - if actual_version is None: - self.log(logging.WARNING, "Could not determine server version, retaining default (9.6.0)") - actual_version = (9, 6, 0) - - cached_version = getattr(self, "_server_version_cache", None) - version_changed = cached_version != actual_version - - if version_changed: - self._server_version_cache = actual_version - self._dialect = PostgresDialect( - actual_version, - graph_feature_overrides=getattr( - self._dialect, "_graph_feature_overrides", None - ), - ) - - # Detect installed extensions - extensions = await self._detect_extensions() - self._dialect._extensions = extensions - - # Log detected extensions - installed_exts = [f"{k}={v.version}" for k, v in extensions.items() if v.installed] - ext_info = ", ".join(installed_exts) if installed_exts else "none" - - if version_changed: - self.log(logging.INFO, f"Adapted to PostgreSQL {actual_version}, extensions: {ext_info}") - else: - self.log(logging.DEBUG, f"Extensions detected: {ext_info}") - - async def _detect_extensions(self) -> Dict[str, PostgresExtensionInfo]: - """Detect installed and available extensions.""" - async with self._connection.cursor() as cursor: - await cursor.execute(PostgresDialect.SQL_INSTALLED_EXTENSIONS) - installed_rows = await cursor.fetchall() - - await cursor.execute(PostgresDialect.SQL_AVAILABLE_EXTENSIONS) - available_rows = await cursor.fetchall() - - return PostgresDialect.build_extension_map(installed_rows, available_rows) - - async def ping(self, reconnect: bool = True) -> bool: - """ - Ping the PostgreSQL server to check if the async connection is alive. - - Args: - reconnect: If True, attempt to reconnect if the connection is dead. - If False, just return the current connection status. - - Returns: - True if the connection is alive (or was successfully reconnected), - False if the connection is dead and reconnect is False or reconnection failed. - """ - try: - if not self._connection: - if reconnect: - await self.connect() - return True - else: - return False - - # Check connection status without triggering auto-reconnect - if self._connection.closed or self._connection.broken: - if reconnect: - self._connection = None - await self.connect() - return True - else: - return False - - # Execute a simple query to verify connection is actually working - cursor = await self._get_cursor() - await cursor.execute("SELECT 1") - await cursor.fetchone() - await cursor.close() - - return True - except psycopg.Error as e: - self.log(logging.WARNING, f"PostgreSQL async connection ping failed: {str(e)}") - if reconnect: - try: - self._connection = None - await self.connect() - return True - except Exception as connect_error: - self.log(logging.ERROR, f"Failed to reconnect after ping failure: {str(connect_error)}") - return False - return False - - async def _reconnect(self) -> bool: - """ - Attempt to reconnect to the PostgreSQL server. - - Returns: - True if reconnection was successful, False otherwise - """ - try: - self._connection = None - await self.connect() - return True - except Exception as e: - self.log(logging.ERROR, f"Failed to reconnect: {str(e)}") - return False - - async def execute( - self, - sql: str, - params: Optional[Tuple] = None, - *, - options: Optional[ExecutionOptions] = None, - max_retries: int = 2, - **kwargs: Any, - ) -> QueryResult: - """ - Execute a SQL statement with optional parameters asynchronously. - - This method wraps the base execute method with automatic reconnection - logic for connection errors (Plan B: Error Retry Mechanism). - - Args: - sql: SQL statement to execute - params: Optional parameters for the SQL statement - options: Execution options - max_retries: Maximum number of retry attempts for connection errors - **kwargs: Additional keyword arguments - - Returns: - QueryResult containing the query results - """ - from rhosocial.activerecord.backend.options import StatementType - - # If no options provided, create default options from kwargs - if options is None: - # Determine statement type based on SQL - sql_upper = sql.strip().upper() - if sql_upper.startswith(("SELECT", "WITH", "SHOW", "DESCRIBE", "EXPLAIN")): - stmt_type = StatementType.DQL - elif sql_upper.startswith(("INSERT", "UPDATE", "DELETE", "UPSERT")): - stmt_type = StatementType.DML - else: - stmt_type = StatementType.DDL - - # Extract column_mapping and column_adapters from kwargs if present - column_mapping = kwargs.get("column_mapping") - column_adapters = kwargs.get("column_adapters") - - options = ExecutionOptions( - stmt_type=stmt_type, - process_result_set=None, - column_adapters=column_adapters, - column_mapping=column_mapping, - ) - else: - # If options is provided but column_mapping or column_adapters are explicitly passed in kwargs, - # update the options with these values - if "column_mapping" in kwargs: - options.column_mapping = kwargs["column_mapping"] - if "column_adapters" in kwargs: - options.column_adapters = kwargs["column_adapters"] - - # Retry loop for connection errors (Plan B) - # We catch the framework's OperationalError because the original PsycopgOperationalError - # has already been converted by _handle_error() in the base class. - for attempt in range(max_retries + 1): - try: - return await super().execute(sql, params, options=options) - except OperationalError as e: - error_msg = str(e) - if self._is_connection_error_message(error_msg) and attempt < max_retries: - self.log(logging.WARNING, - f"Connection error on attempt {attempt + 1}, retrying... Error: {error_msg}") - if await self._reconnect(): - continue - # If reconnection fails, break and raise the error - self.log(logging.ERROR, "Failed to reconnect after connection error") - break - raise - - async def _handle_error(self, error: Exception) -> None: - """Handle PostgreSQL-specific errors asynchronously.""" - error_msg = str(error) - - # Try to rollback transaction to recover from aborted state - # PostgreSQL requires explicit ROLLBACK after errors in a transaction - await self._try_rollback_transaction() - - if isinstance(error, PsycopgIntegrityError): - if "duplicate key value violates unique constraint" in error_msg.lower(): - self.log(logging.ERROR, f"Unique constraint violation: {error_msg}") - raise IntegrityError(f"Unique constraint violation: {error_msg}") from error - elif "violates foreign key constraint" in error_msg.lower(): - self.log(logging.ERROR, f"Foreign key constraint violation: {error_msg}") - raise IntegrityError(f"Foreign key constraint violation: {error_msg}") from error - self.log(logging.ERROR, f"Integrity error: {error_msg}") - raise IntegrityError(error_msg) from error - elif isinstance(error, PsycopgOperationalError): - if "deadlock detected" in error_msg.lower(): - self.log(logging.ERROR, f"Deadlock error: {error_msg}") - raise DeadlockError(error_msg) from error - self.log(logging.ERROR, f"Operational error: {error_msg}") - raise OperationalError(error_msg) from error - elif isinstance(error, PsycopgProgrammingError): - self.log(logging.ERROR, f"Programming error: {error_msg}") - raise QueryError(error_msg) from error - elif isinstance(error, PsycopgDeadlockError): - self.log(logging.ERROR, f"Deadlock error: {error_msg}") - raise DeadlockError(error_msg) from error - elif isinstance(error, PsycopgError): - self.log(logging.ERROR, f"PostgreSQL error: {error_msg}") - raise DatabaseError(error_msg) from error - else: - self.log(logging.ERROR, f"Unexpected error: {error_msg}") - raise error - - async def _try_rollback_transaction(self) -> None: - """Attempt to rollback transaction to recover from aborted state asynchronously. - - PostgreSQL requires explicit ROLLBACK after query errors in a transaction. - This method attempts to recover by rolling back the transaction, regardless - of the transaction manager's state. - """ - try: - # First, try the transaction manager if active - if self.in_transaction: - self.log(logging.INFO, "Attempting to rollback transaction after error") - await self._transaction_manager.rollback() - - # Also try direct connection rollback to handle edge cases - # This ensures we recover from aborted transaction state - if self._connection and not self._connection.closed: - try: - await self._connection.rollback() - self.log(logging.INFO, "Direct connection rollback completed") - except Exception as direct_error: - self.log(logging.DEBUG, f"Direct rollback not needed or failed: {direct_error}") - except Exception as rollback_error: - self.log(logging.WARNING, f"Failed to rollback transaction: {rollback_error}") - - async def _handle_auto_commit(self) -> None: - """Handle auto commit based on PostgreSQL connection and transaction state asynchronously. - - This method will commit the current connection if: - 1. The connection exists and is open - 2. There is no active transaction managed by transaction_manager - - It's used by insert/update/delete operations to ensure changes are - persisted immediately when auto_commit=True is specified. - """ - try: - # Check if connection exists - if not self._connection: - return - - # Check if we're not in an active transaction - if not self.in_transaction: - # For PostgreSQL, if autocommit is disabled, we need to commit explicitly - if not self.config.autocommit: - await self._connection.commit() - self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") - except Exception as e: - # Just log the error but don't raise - this is a convenience feature - self.log(logging.WARNING, f"Failed to auto-commit: {str(e)}") - - async def _handle_auto_commit_if_needed(self) -> None: - """ - Handle auto-commit for PostgreSQL asynchronously. - - PostgreSQL respects the autocommit setting, but we also need to handle explicit commits. - """ - if not self.in_transaction and self._connection: - if not self.config.autocommit: - await self._connection.commit() - self.log(logging.DEBUG, "Auto-committed operation (not in active transaction)") - - # region Advisory Lock Methods - - async def execute_advisory_lock( - self, - key, - shared: bool = False, - session: bool = True - ) -> None: - """ - Acquire an advisory lock asynchronously. - - This method blocks until the lock is acquired. - - Args: - key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers - shared: If True, acquire shared lock; otherwise exclusive lock - session: If True (default), session-level lock; otherwise transaction-level - - Raises: - DatabaseError: If the lock operation fails - """ - from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryLockExpression - - expr = PostgresAdvisoryLockExpression(self.dialect, key=key, shared=shared, session=session) - sql, params = expr.to_sql() - await self.execute(sql, params) - - async def execute_advisory_unlock(self, key, shared: bool = False) -> bool: - """ - Release an advisory lock asynchronously. - - Args: - key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers - shared: If True, release shared lock; otherwise exclusive lock - - Returns: - True if the lock was released, False if it was not held - """ - from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryUnlockExpression - - expr = PostgresAdvisoryUnlockExpression(self.dialect, key=key, shared=shared) - sql, params = expr.to_sql() - result = await self.execute(sql, params) - # pg_advisory_unlock returns boolean in first column - if result.data and len(result.data) > 0: - row = result.data[0] - # Result is a dict with column name as key - return bool(list(row.values())[0]) - return False - - async def execute_advisory_unlock_all(self) -> None: - """ - Release all advisory locks held by the current session asynchronously. - """ - from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresAdvisoryUnlockAllExpression - - expr = PostgresAdvisoryUnlockAllExpression(self.dialect) - sql, params = expr.to_sql() - await self.execute(sql, params) - - async def try_advisory_lock( - self, - key, - shared: bool = False, - session: bool = True - ) -> bool: - """ - Non-blocking advisory lock acquisition asynchronously. - - Args: - key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers - shared: If True, acquire shared lock; otherwise exclusive lock - session: If True (default), session-level lock; otherwise transaction-level - - Returns: - True if the lock was acquired, False if it was not available - """ - from rhosocial.activerecord.backend.impl.postgres.expression.advisory import PostgresTryAdvisoryLockExpression - - expr = PostgresTryAdvisoryLockExpression(self.dialect, key=key, shared=shared, session=session) - sql, params = expr.to_sql() - result = await self.execute(sql, params) - # pg_try_advisory_lock returns boolean in first column - if result.data and len(result.data) > 0: - row = result.data[0] - # Result is a dict with column name as key - return bool(list(row.values())[0]) - return False - - def advisory_lock( - self, - key, - shared: bool = False, - session: bool = True - ): - """ - Async context manager for advisory locks. - - Automatically acquires the lock on entry and releases it on exit. - - Args: - key: Lock key - either a single 64-bit integer or tuple of two 32-bit integers - shared: If True, acquire shared lock; otherwise exclusive lock - session: If True (default), session-level lock; otherwise transaction-level - - Returns: - Async context manager that holds the lock - - Example: - async with backend.advisory_lock(key=12345): - # Critical section - lock is held - await process_data() - # Lock automatically released - - # Shared lock for read-only access - async with backend.advisory_lock(key=12345, shared=True): - await read_data() - """ - from contextlib import asynccontextmanager - - @asynccontextmanager - async def _advisory_lock_context(): - await self.execute_advisory_lock(key=key, shared=shared, session=session) - try: - yield - finally: - # Only release session-level locks; transaction-level are auto-released - if session: - await self.execute_advisory_unlock(key=key) - - return _advisory_lock_context() - - # endregion - - -__all__ = ["AsyncPostgresBackend"] From 979088a6e9c9551fb87b93447575552f70272e68 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 29 Sep 2026 00:26:05 +0800 Subject: [PATCH 13/19] chore: correct the path comment at the top of the file The convention is that the first line names the module's own path, so a stale name sends the next reader to a file that is not there. These were left behind by earlier renames, when table.py became ddl_table.py and show_types.py became show/types.py. Comment only; no behaviour changes. --- .../activerecord/backend/impl/postgres/backend/async_backend.py | 2 +- .../activerecord/backend/impl/postgres/backend/backend.py | 2 +- .../backend/impl/postgres/expression/advisory/__init__.py | 2 +- .../activerecord/backend/impl/postgres/mixins/ddl_table.py | 2 +- .../activerecord/backend/impl/postgres/reserved_words.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py index eac2ee4e..d4b84fae 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/async_backend.py +# src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py """PostgreSQL asynchronous backend implementation. This module provides an asynchronous PostgreSQL implementation: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py index 25238c2b..9a42eba4 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py +# src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py """PostgreSQL synchronous backend implementation. This module provides a synchronous PostgreSQL implementation: diff --git a/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/__init__.py b/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/__init__.py index 6c374b85..add86c57 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/__init__.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/__init__.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/expression/advisory/__init__.py +# src/rhosocial/activerecord/backend/impl/postgres/expression/advisory/__init__.py """ Advisory lock expression classes. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py index 2950ba46..9d4bf39c 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgres/mixins/table.py +# src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py import re from typing import Any, List, Tuple diff --git a/src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py b/src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py index 35686517..dee395f0 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py @@ -1,4 +1,4 @@ -# src/rhosocial/activerecord/backend/impl/postgresql/reserved_words.py +# src/rhosocial/activerecord/backend/impl/postgres/reserved_words.py """ PostgreSQL reserved words list. From 6da699fb250662c811be4ed6b2c7dd14d322d330 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 29 Sep 2026 12:18:27 +0800 Subject: [PATCH 14/19] docs: rewrite the README and correct the .claude/ knowledge base The README's 15-row version table described PostgreSQL's own feature history rather than what this backend gates on, and several thresholds were off by a minor release (RETURNING 8.2 vs 8.4, JSONB 9.4 vs 9.4, JSON_TABLE 12 vs 13, MERGE 15 vs 16). Replaced with a table generated from the dialect's supports_* switches. The core dependency was documented as >=1.0.0, which cannot resolve against the 1.0.0.devN pre-release line; pyproject had the correct value. In the agent docs: the architecture file imported from a nonexistent impl.postgresql package and named the class PostgreSQLBackend (it is PostgresBackend), listed a CTEQueryMixin that does not exist in the ActiveQuery composition, showed __init__.py for packages that are PEP 420 namespace packages, inverted the IActiveRecord relationship to BaseActiveRecord, and mislabelled ValidationError's base. Also corrected the core dependency and extras listings, and the PYTHONPATH guidance, which omitted tests -- without it the shared testsuite cannot resolve its provider registry. --- .claude/architecture.md | 105 +++++----- .claude/backend_development.md | 54 ++--- .claude/code_style.md | 27 ++- .claude/testing.md | 10 +- .claude/version_control.md | 140 ++++++------- README.md | 358 +++++++++++++-------------------- 6 files changed, 301 insertions(+), 393 deletions(-) diff --git a/.claude/architecture.md b/.claude/architecture.md index bb797c4c..8e563d02 100644 --- a/.claude/architecture.md +++ b/.claude/architecture.md @@ -104,26 +104,25 @@ Clear distinction between Backend and ActiveRecord, avoiding tight coupling betw **Implementation**: The `SQLDialectBase` (and its concrete implementations like `SQLiteDialect`) provides dedicated methods for this: - `format_identifier(self, identifier: str) -> str`: Formats and quotes identifiers. -- `format_string_literal(self, value: str) -> str`: Formats and quotes string literals. +- `format_literal(self, value: Any) -> str`: Formats and quotes string literals. All query building components (e.g., mixins, `SQLExpression` subclasses) are designed to route identifier and literal formatting through these dialect methods, ensuring a single, centralized point of control for SQL syntax generation. ```python -# In backend/dialect.py +# In backend/dialect/base.py class SQLDialectBase(ABC): @abstractmethod def format_identifier(self, identifier: str) -> str: """Format identifier (table name, column name).""" pass - @abstractmethod - def format_string_literal(self, value: str) -> str: - """Format string literal.""" + def format_literal(self, value: Any) -> str: + """Format a Python value as a SQL literal.""" pass # Usage in query builder (conceptual) # formatted_column = self.model_class.backend().dialect.format_identifier(column_name) -# formatted_string = self.model_class.backend().dialect.format_string_literal(string_value) +# formatted_string = self.model_class.backend().dialect.format_literal(string_value) ``` ## Package Architecture @@ -290,7 +289,10 @@ class ActiveRecord( ```python # backend/base.py class StorageBackend( - # ...composed from LoggingMixin, CapabilityMixin, TypeAdaptionMixin, SQLBuildingMixin, etc. + # ...composed from LoggingMixin, TypeAdaptionMixin, SQLBuildingMixin, + # ReturningClauseMixin, ResultProcessingMixin, SQLOperationsMixin, ExecutionMixin, + # BatchExecutionMixin, ExecutionHooksMixin, ConnectionMixin, + # TransactionManagementMixin. There is no `CapabilityMixin`. ABC ): """ @@ -404,14 +406,14 @@ class Article(TimestampMixin, SoftDeleteMixin, ActiveRecord): ```python # The project's query builder is ActiveQuery, composed from specialized mixins. -from rhosocial.activerecord.query import ActiveQuery, BaseQueryMixin, AggregateQueryMixin, CTEQueryMixin, JoinQueryMixin, RangeQueryMixin, RelationalQueryMixin +from rhosocial.activerecord.query import ActiveQuery, BaseQueryMixin, AggregateQueryMixin, CTEQuery, JoinQueryMixin, RangeQueryMixin, RelationalQueryMixin class ActiveQuery( - CTEQueryMixin, + CTEQuery, JoinQueryMixin, RelationalQueryMixin, RangeQueryMixin, - # BaseQueryMixin and AggregateQueryMixin are inherited through CTEQueryMixin + # BaseQueryMixin and AggregateQueryMixin are inherited through CTEQuery ): """ Complete ActiveQuery implementation, combining all query mixins. @@ -495,7 +497,7 @@ def create_backend(backend_type: str, **config) -> StorageBackend: backends = { 'sqlite': SQLiteBackend, 'mysql': MySQLBackend, - 'postgresql': PostgreSQLBackend, + 'postgres': PostgresBackend, } backend_class = backends.get(backend_type) @@ -511,8 +513,10 @@ def create_backend(backend_type: str, **config) -> StorageBackend: ``` BaseModel (Pydantic) - └── IActiveRecord (Interface - implemented by BaseActiveRecord) - └── BaseActiveRecord (Core implementation, implements IActiveRecord) + └── BaseActiveRecord (Core CRUD implementation, a BaseModel subclass) + # NOTE: BaseActiveRecord does NOT inherit from IActiveRecord. + # IActiveRecord is a Protocol the QUERY mixins implement + # (e.g. `class QueryMixin(IActiveRecord)` in interface/query.py). # ActiveRecord is composed from these mixins and BaseActiveRecord └── QueryMixin └── RelationManagementMixin @@ -530,7 +534,7 @@ StorageBackendBase (ABC) └── StorageBackend (ABC) └── SQLiteBackend # Concrete implementation # └── MySQLBackend (if implemented) - # └── PostgreSQLBackend (if implemented) + # └── PostgresBackend (if implemented) StorageBackendBase (ABC) # Composed from mixins for async operations @@ -589,8 +593,10 @@ The project maintains **minimal core dependencies** by design: ```python # Minimal core dependencies - Pydantic only dependencies = [ - "pydantic>=2.0.0", # Data validation and model definition - "typing_extensions>=4.0.0", # Backported typing features for Python 3.8 + "pydantic==2.10.6; python_version < '3.9'", # Data validation and model definition + "pydantic>=2.12.0; python_version >= '3.9'", + "typing-extensions>=4.12.0; python_version < '3.9'", # Backported typing for 3.8 + "backports.zoneinfo; python_version < '3.9'", ] ``` @@ -599,9 +605,11 @@ dependencies = [ ### Optional Dependencies ```python +# The core ships NO driver extras. Each extra points at the separate backend +# distribution, which is what pulls the driver in transitively. extras_require = { - "mysql": ["mysql-connector-python>=8.0.0"], # MySQL backend - "postgresql": ["psycopg[binary]>=3.2.13"], # PostgreSQL backend + "mysql": ["rhosocial-activerecord-mysql>=1.0.0,<2.0.0"], + "postgres": ["rhosocial-activerecord-postgres>=1.0.0,<2.0.0"], "dev": ["pytest", "black", "mypy"], # Development tools } ``` @@ -622,8 +630,8 @@ def discover_backends(): pass try: - from rhosocial.activerecord.backend.impl.postgresql import PostgreSQLBackend - backends['postgresql'] = PostgreSQLBackend # Uses psycopg 3 directly + from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend + backends['postgres'] = PostgresBackend # Uses psycopg 3 directly except ImportError: pass @@ -635,16 +643,18 @@ def discover_backends(): ### 1. Custom Fields ```python -class EncryptedField(Field): - """Custom encrypted field type.""" - - def __set__(self, instance, value): - encrypted = encrypt(value) - super().__set__(instance, encrypted) - - def __get__(self, instance, owner): - value = super().__get__(instance, owner) - return decrypt(value) if value else None +# There is no `Field` base class to subclass. `base/fields.py` holds only +# annotation markers (DDLAnnotation, UseColumn, UseAdapter, UseSqlType, +# UseIndex, UseConstraint, UseColumnAttributes, UseComment, UseGeneratedColumn, +# DerivedField). For transparent encryption use a pydantic field_validator, or +# a SQLTypeAdapter for the column type. +class EncryptedModel(ActiveRecord): + settings: str + + @field_validator("settings", mode="before") + @classmethod + def _decrypt(cls, v): + return decrypt(v) if v else None ``` ### 2. Custom Validators @@ -681,14 +691,16 @@ class User(UserQueryMixin, ActiveRecord): ### 4. Event Hooks ```python -class User(ActiveRecord): - def before_save(self): - """Called before saving.""" - self.updated_at = datetime.now() +# `before_save` / `after_save` are NEVER called -- no such hook exists and +# nothing dispatches to them, so code written this way is silently dead. +# Register against the ModelEvent enum instead. +from rhosocial.activerecord.interface.base import ModelEvent + +user = User(name="Alice") +user.on(ModelEvent.AFTER_INSERT, lambda rec: cache.invalidate(f"user:{rec.id}")) - def after_save(self): - """Called after saving.""" - cache.invalidate(f"user:{self.id}") +# Events: BEFORE_VALIDATE, AFTER_VALIDATE, BEFORE_INSERT, AFTER_INSERT, +# BEFORE_UPDATE, AFTER_UPDATE, BEFORE_DELETE, AFTER_DELETE. ``` ## Performance Optimization @@ -791,17 +803,18 @@ class ThreadSafeDict(Dict[K, V]): ### Exception Hierarchy ```python -class ActiveRecordError(Exception): +# backend/errors.py -- there is NO `ActiveRecordError` class. `DatabaseError` +# is the actual root, and everything below derives from it. +class DatabaseError(Exception): """Base exception for all ActiveRecord errors.""" -class DatabaseError(ActiveRecordError): - """Database operation errors.""" - -class ValidationError(ActiveRecordError): - """Data validation errors.""" - -class RecordNotFound(DatabaseError): - """Record not found in database.""" +class ConnectionError(DatabaseError): ... +class TransactionError(DatabaseError): ... +class QueryError(DatabaseError): ... +class ValidationError(DatabaseError): ... +class IntegrityError(DatabaseError): ... +class LockError(DatabaseError): ... +class RecordNotFound(DatabaseError): ... ``` ### Error Propagation diff --git a/.claude/backend_development.md b/.claude/backend_development.md index aa452352..9eb5e353 100644 --- a/.claude/backend_development.md +++ b/.claude/backend_development.md @@ -32,7 +32,7 @@ rhosocial-activerecord-{backend}/ │ └── impl/ │ └── {backend}/ │ ├── __init__.py -│ ├── backend.py # Main backend implementation +│ ├── backend/ # sync + async backend classes │ ├── adapters.py # Backend-specific type adapters │ ├── config.py # Connection configuration │ ├── dialect.py # SQL dialect handling @@ -114,7 +114,7 @@ Expression.to_sql() -> Dialect.format_*() -> SQL string and parameters - `operators.py`: SQL operations (binary, unary, arithmetic expressions) - `predicates.py`: SQL predicate expressions (WHERE clause conditions) - `query_parts.py`: SQL query clauses (WHERE, GROUP BY, HAVING, ORDER BY, etc.) -- `statements.py`: DML/DQL/DDL statements (SELECT, INSERT, UPDATE, DELETE, etc.) +- `statements/`: DML/DQL/DDL statements (a package, not a module) (SELECT, INSERT, UPDATE, DELETE, etc.) - `functions.py`: Standalone factory functions for creating SQL expressions - `aggregates.py`: SQL aggregation expressions and functions - `advanced_functions.py`: Advanced SQL functions (CASE, CAST, EXISTS, window functions) @@ -149,8 +149,9 @@ Every backend must implement the `StorageBackend` abstract base class. This is t ```python # backend.py from typing import Any, Dict, List, Optional, Tuple, Type -from ...base import StorageBackend, ConnectionConfig -from ...type_adapter import SQLTypeAdapter +from rhosocial.activerecord.backend.base import StorageBackend +from rhosocial.activerecord.backend.config import ConnectionConfig +from rhosocial.activerecord.backend.type_adapter import SQLTypeAdapter class MyBackend(StorageBackend): """Implementation of MyDatabase backend using native driver.""" @@ -175,7 +176,8 @@ class MyBackend(StorageBackend): """Check if connection is valid.""" pass - def execute(self, sql: str, params: Optional[Tuple] = None, returning: Optional[Union[bool, List[str], ReturningOptions]] = None, column_adapters: Optional[Dict[str, Tuple[SQLTypeAdapter, Type]]] = None) -> QueryResult: + def execute(self, sql: str, params: Optional[Tuple] = None, *, + options: Optional[ExecutionOptions] = None) -> QueryResult: """Executes a SQL query.""" pass @@ -197,10 +199,6 @@ class MyBackend(StorageBackend): """ pass - def _initialize_capabilities(self) -> DatabaseCapabilities: - """Declare all features supported by this backend.""" - pass - def _handle_error(self, error: Exception) -> None: """Catch driver-specific exceptions and raise standard errors.""" pass @@ -241,10 +239,10 @@ Define an immutable configuration class for your backend. # config.py from dataclasses import dataclass, field from typing import Optional, Dict, Any -from rhosocial.activerecord.backend.config import BaseConfig +from rhosocial.activerecord.backend.config import ConnectionConfig -@dataclass(frozen=True) -class MyDatabaseConfig(BaseConfig): +@dataclass +class MyDatabaseConfig(ConnectionConfig): host: str = "localhost" port: int = 5432 database: str @@ -366,34 +364,20 @@ Every dialect inherits from: ```python # dialect.py -from rhosocial.activerecord.backend.dialect import ( - SQLDialect, ReturningClauseHandler, TypeMapping -) -from typing import Dict, List, Optional +from rhosocial.activerecord.backend.dialect.base import SQLDialectBase +from typing import Optional, Tuple -class MyDatabaseDialect(SQLDialect): +class MyDatabaseDialect(SQLDialectBase): def __init__(self): super().__init__() self.returning_handler = MyDatabaseReturningHandler() - def quote_identifier(self, identifier: str) -> str: return f'"{identifier}"' - def get_placeholder(self, name: str = None) -> str: return "?" - - def format_limit_offset(self, sql: str, limit: Optional[int], offset: Optional[int]) -> str: - if limit is not None: sql += f" LIMIT {limit}" - if offset is not None: sql += f" OFFSET {offset}" - return sql - - def get_type_mappings(self) -> Dict[str, TypeMapping]: - return {"INTEGER": TypeMapping("INTEGER"), "TEXT": TypeMapping("TEXT"), ...} + def get_parameter_placeholder(self, position: int = 0) -> str: + return "?" -class MyDatabaseReturningHandler(ReturningClauseHandler): - @property - def is_supported(self) -> bool: return True - - def format_clause(self, columns: Optional[List[str]] = None) -> str: - cols = "*" if not columns else ', '.join(self.dialect.quote_identifier(c) for c in columns) - return f"RETURNING {cols}" +# Identifier quoting is inherited from SQLDialectBase.format_identifier; +# LIMIT/OFFSET come from the DQL mixins; type mapping is not a dialect +# method but a set of SQLTypeAdapter registrations on the backend. ``` ### 4. The Type Adaptation System @@ -565,7 +549,7 @@ This checklist summarizes all the required and recommended steps for creating a - [ ] Implement `get_default_adapter_suggestions` to define the conversion strategy. - [ ] Implement `Transaction Management`. - [ ] Implement `Error Handling` to map driver exceptions. -- [ ] Implement `Feature Detection` to declare capabilities. +- [ ] Implement the `supports_*` capability switches your dialect needs. - [ ] If providing an `AsyncStorageBackend`, ensure it has **Functional Equivalence** with the sync version. ### Required Tests diff --git a/.claude/code_style.md b/.claude/code_style.md index 048b351d..8dc35fcd 100644 --- a/.claude/code_style.md +++ b/.claude/code_style.md @@ -56,7 +56,7 @@ from pydantic import BaseModel, Field # Local application imports from rhosocial.activerecord.model import ActiveRecord -from rhosocial.activerecord.backend import StorageBackend +from rhosocial.activerecord.backend.base import StorageBackend # Relative imports (within same package) from .base import BaseActiveRecord @@ -65,7 +65,7 @@ from ..interface import IActiveRecord ### Line Length and Wrapping -- Maximum line length: 100 characters +- Maximum line length: 120 characters (ruff `line-length = 120`; black formats to 100, but 100 is not the limit) - Wrap long lines using parentheses: ```python @@ -291,7 +291,7 @@ from typing import annotations import third_party -from rhosocial.activerecord import local +from rhosocial.activerecord.model import ActiveRecord from . import relative # Constants @@ -414,12 +414,12 @@ raise DatabaseError( """Test module for specific feature.""" import pytest -from rhosocial.activerecord import Model +from rhosocial.activerecord.model import ActiveRecord # Fixtures at top @pytest.fixture def sample_model(): - return Model(name="test") + return ActiveRecord(name="test") # Test classes for organization class TestCRUDOperations: @@ -603,13 +603,12 @@ def transaction(backend: StorageBackend): ### Mixin Classes ```python -class TimestampMixin: - """Add timestamp fields to models.""" - created_at: datetime = Field(default_factory=datetime.now) - updated_at: Optional[datetime] = None - - def before_save(self): - self.updated_at = datetime.now() +# TimestampMixin declares NO fields: it hooks +# ModelEvent.BEFORE_INSERT / BEFORE_UPDATE and requires the model to +# declare the fields itself, named per __created_at_field__ / +# __updated_at_field__. Use DefaultTimestampMixin to get them declared +# for you. There is no before_save() hook -- lifecycle is registered +# with instance.on(ModelEvent.X, handler). ``` ## Expression-Dialect System Coding Standards @@ -687,7 +686,7 @@ class MyExpression(mixins.ArithmeticMixin, mixins.ComparisonMixin, bases.SQLValu def to_sql(self) -> 'bases.SQLQueryAndParams': # Delegate formatting to dialect - formatted_value = self.dialect.format_string_literal(self.value) + formatted_value = self.dialect.format_literal(self.value) return formatted_value, (self.value,) ``` @@ -705,7 +704,7 @@ def format_identifier(self, identifier: str) -> str: - [ ] Path comment at file start matches actual location - [ ] All functions/classes have docstrings - [ ] Type hints on all function signatures -- [ ] No line exceeds 100 characters +- [ ] No line exceeds 120 characters - [ ] Imports sorted and grouped correctly - [ ] No commented-out code - [ ] Exception messages are informative diff --git a/.claude/testing.md b/.claude/testing.md index 3f16238b..7360363f 100644 --- a/.claude/testing.md +++ b/.claude/testing.md @@ -21,6 +21,8 @@ This backend CANNOT be used with free-threaded Python builds. Free-threaded test ```toml dependencies = [ "psycopg[binary]>=3.2.13", + "rhosocial-activerecord>=1.0.0.dev0,<2.0.0", + "rhosocial-activerecord>=1.0.0.dev0,<2.0.0", "rhosocial-activerecord" ] ``` @@ -30,8 +32,8 @@ dependencies = [ ```bash # Activate virtual environment and set PYTHONPATH cd /mnt/i/GitHubRepositories/rhosocial/python-activerecord-postgres -source .venv/bin/activate -export PYTHONPATH=src +source .venv3.14-ubuntu26.04/bin/activate +export PYTHONPATH=src:tests # Run tests pytest @@ -57,5 +59,5 @@ markers = [ ## Reference -- [Core testing guide](../python-activerecord/.claude/testing.md) -- [PostgreSQL backend development](../python-activerecord/.claude/backend_development.md) \ No newline at end of file +- [Core testing guide](../../python-activerecord/.claude/testing.md) +- [PostgreSQL backend development](../../../python-activerecord/.claude/backend_development.md) \ No newline at end of file diff --git a/.claude/version_control.md b/.claude/version_control.md index b162bb9f..77488742 100644 --- a/.claude/version_control.md +++ b/.claude/version_control.md @@ -901,9 +901,9 @@ git push origin --delete release/v1.2.0 # Optional - Require pull request reviews (minimum 1 approval) - **Require status checks to pass**: - - `test-with-coverage` (Python 3.14 with coverage ≥90%) - - `test-other-versions` (Python 3.8-3.13 compatibility) - - `test-free-threaded` (Python 3.13t, 3.14t) + - `test` (Python 3.14 with coverage ≥90%) + - `test` (Python 3.8-3.13 compatibility) + - `test` (Python 3.13t, 3.14t) - **Require branches to be up to date before merge** - **Require linear history** (no merge commits) - Include administrators in restrictions @@ -2743,55 +2743,44 @@ git push origin maint/1.2.x v1.2.6 ### Capability Declaration System -Backends MUST declare their supported capabilities using the `DatabaseCapabilities` system: +There is **no** `rhosocial.activerecord.backend.capabilities` module, and no +`DatabaseCapabilities` / `CapabilityCategory` / `CTECapability` classes, and no +`_initialize_capabilities()` hook. Capabilities are declared the ordinary way: +the dialect implements a Protocol from +`rhosocial.activerecord.backend.dialect.protocols` and answers a +`supports_*` switch. ```python -# Backend capability declaration example -from rhosocial.activerecord.backend.capabilities import ( - DatabaseCapabilities, - CapabilityCategory, - CTECapability, - WindowFunctionCapability, -) - -class MySQLBackend(StorageBackend): - def _initialize_capabilities(self): - """Declare backend capabilities based on server version.""" - capabilities = DatabaseCapabilities() - version = self.get_server_version() - - # CTEs supported from MySQL 8.0+ - if version >= (8, 0, 0): - capabilities.add_cte([ - CTECapability.BASIC_CTE, - CTECapability.RECURSIVE_CTE, - ]) - - # Window functions from MySQL 8.0+ - if version >= (8, 0, 0): - capabilities.add_window_function(ALL_WINDOW_FUNCTIONS) - - return capabilities +# mixins/cte.py on the MySQL backend -- the real mechanism +from rhosocial.activerecord.backend.dialect.protocols import CTESupport + +class MySQLCTEMixin(CTESupport): + def supports_basic_cte(self) -> bool: + return self.version >= (8, 0, 0) + + def supports_recursive_cte(self) -> bool: + return self.version >= (8, 0, 0) ``` +`SQLDialectBase` is adapted to the live server on connect +(`backend.introspect_and_adapt()`), which is what populates `self.version` the +gates compare against. A `version` of `None` means "not adapted", and every +gate then raises `DialectNotAdaptedException`. + ### Capability-Driven Test Execution -Tests automatically skip when required capabilities are unavailable: +Tests branch on the same `supports_*` switches rather than on a decorator: ```python -from rhosocial.activerecord.backend.capabilities import ( - CapabilityCategory, - CTECapability, -) -from rhosocial.activerecord.testsuite.utils import requires_capability - -@requires_capability(CapabilityCategory.CTE, CTECapability.RECURSIVE_CTE) -def test_recursive_cte(tree_fixtures): - """Test requires recursive CTE support.""" - Node = tree_fixtures[0] - # Test implementation +dialect = backend.dialect +if dialect.supports_recursive_cte(): + ... # exercise the recursive-CTE path ``` +Note that `supports_*` names are not uniform: some take an argument (for +example `supports_collation_name(name)`, `supports_json_table()`), and many +take none. + **Capability Version Tracking**: - New capabilities added in MINOR versions @@ -3021,7 +3010,7 @@ rhosocial-activerecord-{backend}/ **Backend Class**: `{Backend}Backend` -- Examples: `MySQLBackend`, `PostgreSQLBackend` +- Examples: `MySQLBackend`, `PostgresBackend` (note: not `PostgreSQLBackend`) #### Interface Compliance @@ -3030,7 +3019,7 @@ All backends MUST implement: 1. **StorageBackend Interface**: ```python - from rhosocial.activerecord.backend import StorageBackend + from rhosocial.activerecord.backend.base import StorageBackend class MyBackend(StorageBackend): def connect(self) -> None: ... @@ -3045,20 +3034,24 @@ All backends MUST implement: 2. **Capability Declaration**: + There is no `_initialize_capabilities()` and no `DatabaseCapabilities` + object. Declare capabilities by implementing the `supports_*` switches on + the dialect: + ```python - def _initialize_capabilities(self) -> DatabaseCapabilities: - """Declare backend capabilities.""" - capabilities = DatabaseCapabilities() - # Add supported capabilities based on version/config - return capabilities + def supports_basic_cte(self) -> bool: + return self.version >= (8, 0, 0) ``` 3. **Test Provider Implementation**: ```python - from rhosocial.activerecord.testsuite.core import IProvider + # There is no `IProvider`. The testsuite defines per-category interfaces: + # IBasicProvider, IQueryProvider, IRelationProvider, IEventsProvider, + # IMixinsProvider (see tests/providers/registry.py). + from rhosocial.activerecord.testsuite.core import IBasicProvider - class MyBackendProvider(IProvider): + class MyBackendProvider(IBasicProvider): def setup_fixtures(self, scenario: str) -> Tuple[Type[ActiveRecord], ...]: # Setup models and schemas pass @@ -3070,36 +3063,31 @@ All backends MUST implement: #### Capability Declaration Requirements -Backends MUST accurately declare capabilities: +Backends MUST answer the `supports_*` switches truthfully, against the real +`self.version` populated by `introspect_and_adapt()`: ```python -def _initialize_capabilities(self): - capabilities = DatabaseCapabilities() - version = self.get_server_version() - - # Example: MySQL 8.0+ features - if version >= (8, 0, 0): - capabilities.add_cte([ - CTECapability.BASIC_CTE, - CTECapability.RECURSIVE_CTE, - ]) - capabilities.add_window_function(ALL_WINDOW_FUNCTIONS) - - # JSON operations - if version >= (5, 7, 0): - capabilities.add_json([ - JSONCapability.JSON_EXTRACT, - JSONCapability.JSON_SET, - ]) - - return capabilities +# mixins/cte.py +def supports_basic_cte(self) -> bool: + return self.version >= (8, 0, 0) + +# mixins/window.py +def supports_window_functions(self) -> bool: + return self.version >= (8, 0, 0) + +# mixins/json.py -- note the patch level: 5.7.0 is NOT enough +def supports_json_type(self) -> bool: + return self.version >= (5, 7, 8) ``` **Capability Testing**: -- Backend must pass all tests for declared capabilities -- Tests automatically skip for unsupported capabilities -- False capability declarations fail during CI +- Tests branch on the same switches the renderer consults +- A gate that claims a feature the server lacks fails at DDL time, not + silently +- The version ladder used to exercise gates is a *lower bound*: probe below + the oldest real release too, or a capability that is really "available + everywhere" gets misreported as gated at the floor #### Version Management for Extensions @@ -3177,7 +3165,7 @@ Forks creating independent implementations have full autonomy but should: ```python # Still use rhosocial.activerecord namespace - from rhosocial.activerecord.backend import StorageBackend + from rhosocial.activerecord.backend.base import StorageBackend ``` 2. **Document Compatibility**: diff --git a/README.md b/README.md index 5b66df6e..96365207 100644 --- a/README.md +++ b/README.md @@ -5,289 +5,211 @@ [![Tests](https://github.com/rhosocial/python-activerecord-postgres/actions/workflows/test.yml/badge.svg)](https://github.com/rhosocial/python-activerecord-postgres/actions) [![Coverage Status](https://codecov.io/gh/rhosocial/python-activerecord-postgres/branch/main/graph/badge.svg)](https://app.codecov.io/gh/rhosocial/python-activerecord-postgres/tree/main) [![Apache 2.0 License](https://img.shields.io/github/license/rhosocial/python-activerecord-postgres.svg)](https://github.com/rhosocial/python-activerecord-postgres/blob/main/LICENSE) -[![Powered by vistart](https://img.shields.io/badge/Powered_by-vistart-blue.svg)](https://github.com/vistart)
rhosocial ActiveRecord Logo

PostgreSQL Backend for rhosocial-activerecord

-

Native Array & JSONB Support · Advanced Types · Sync & Async

+

Native arrays & JSONB · Range types · DDL expressions · Sync & async

-> **Note**: This is a backend implementation for [rhosocial-activerecord](https://github.com/rhosocial/python-activerecord). It cannot be used standalone. +This is a backend implementation for [rhosocial-activerecord](https://github.com/rhosocial/python-activerecord). +It cannot be used standalone. -## Why This Backend? - -### 1. PostgreSQL's Unique Type System - -| Feature | PostgreSQL Backend | MySQL Backend | SQLite Backend | -|---------|-------------------|---------------|----------------| -| **Native Arrays** | ✅ `INTEGER[]`, `TEXT[]` | ❌ Serialize to string | ❌ Serialize to string | -| **JSONB** | ✅ Binary JSON, indexed | ✅ JSON (text-based) | ⚠️ JSON1 extension | -| **UUID** | ✅ Native type | ⚠️ CHAR(36) | ⚠️ TEXT | -| **Range Types** | ✅ `DATERANGE`, `INT4RANGE` | ❌ | ❌ | -| **RETURNING** | ✅ All operations | ❌ | ✅ | - -### 2. No Adapter Overhead for Arrays - -Unlike databases without native arrays, PostgreSQL stores and queries arrays directly: - -```python -# PostgreSQL: Native array - no serialization needed -class Article(ActiveRecord): - tags: list[str] # Maps directly to TEXT[] - -# MySQL/SQLite: Requires adapter -class Article(ActiveRecord): - tags: Annotated[list[str], UseAdapter(ListToStringAdapter(), str)] -``` - -### 3. Powerful Array Operators - -Query arrays with native PostgreSQL operators: - -```python -# Contains -Article.query().where("tags @> ARRAY[?]", ('python',)).all() - -# Overlaps (any element matches) -Article.query().where("tags && ARRAY[?, ?]", ('python', 'database')).all() +## Requirements -# Any element equals -Article.query().where("? = ANY(tags)", ('python',)).all() -``` + -> 💡 **AI Prompt**: "Show me all PostgreSQL array operators with examples" +_979088a_ -## Quick Start +- **Python**: `>=3.8` +- **Core**: `rhosocial-activerecord>=1.0.0.dev0,<2.0.0` +- **Driver**: `psycopg[binary]>=3.2.13` +- **CI-tested server versions**: 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19beta4 +- **Published**: PyPI `1.0.0.dev16` -### Installation +## Installation ```bash pip install rhosocial-activerecord-postgres ``` -### Basic Usage +## Quick Start ```python -from rhosocial.activerecord.model import ActiveRecord +from typing import ClassVar, Optional +from uuid import UUID, uuid4 + +from rhosocial.activerecord.base import FieldProxy from rhosocial.activerecord.backend.impl.postgres.backend import PostgresBackend from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig -from typing import Optional -from uuid import UUID - -class User(ActiveRecord): - __table_name__ = "users" - id: Optional[UUID] = None # Native UUID type - name: str - tags: list[str] # Native array type - metadata: dict # JSONB type - -# Configure -config = PostgresConnectionConfig( - host="localhost", - port=5432, - database="myapp", - username="user", - password="password" +from rhosocial.activerecord.backend.expression import CreateTableExpression +from rhosocial.activerecord.backend.expression.statements import ( + ColumnConstraint, + ColumnConstraintType, + ColumnDefinition, ) -User.configure(config, PostgresBackend) - -# Use -user = User(name="Alice", tags=["python", "postgres"]) -user.save() - -# Query with array operators -python_users = User.query().where("tags @> ARRAY[?]", ('python',)).all() -``` - -> 💡 **AI Prompt**: "How do I configure connection pooling for PostgreSQL?" - -## PostgreSQL-Specific Features - -### Array Types +from rhosocial.activerecord.backend.expression.types import ( + ArrayType, + JsonBType, + TextType, + UUIDType, + VarCharType, +) +from rhosocial.activerecord.model import ActiveRecord -Native array support without adapters: -```python class Article(ActiveRecord): __table_name__ = "articles" + + id: Optional[UUID] = None title: str - tags: list[str] # TEXT[] - scores: list[int] # INTEGER[] + tags: list[str] # maps to text[] / TEXT[] + metadata: dict # maps to JSONB -# Query with array operators -Article.query().where("tags @> ARRAY[?]", ('python',)).all() -Article.query().where("? = ANY(tags)", ('database',)).all() -Article.query().where("array_length(tags, 1) > ?", (3,)).all() -``` + c: ClassVar[FieldProxy] = FieldProxy() -> See [Array Type Comparison](docs/en_US/type_adapters/array_comparison.md) for differences with other databases. -### JSONB Operations +Article.configure( + PostgresConnectionConfig( + host="localhost", + port=5432, + database="myapp", + username="user", + password="password", + ), + PostgresBackend, +) -Binary JSON with indexing support: +dialect = Article.__backend__.dialect +Article.__backend__.execute(*CreateTableExpression( + dialect, + "articles", + columns=[ + ColumnDefinition( + dialect, "id", UUIDType(dialect), + constraints=[ColumnConstraint(dialect, ColumnConstraintType.PRIMARY_KEY)], + ), + ColumnDefinition(dialect, "title", VarCharType(dialect, 200)), + ColumnDefinition(dialect, "tags", ArrayType(dialect, TextType(dialect))), + ColumnDefinition(dialect, "metadata", JsonBType(dialect)), + ], +).to_sql()) +# CREATE TABLE "articles" ("id" UUID PRIMARY KEY, "title" VARCHAR(200), +# "tags" TEXT[], "metadata" JSONB) +``` -```python -class Product(ActiveRecord): - __table_name__ = "products" - name: str - attributes: dict # JSONB +> Type mapping is not automatic for every field. `list[str]` and `dict` need +> either an explicit `DataType` in the DDL above or a type adapter; check +> [type adapters](docs/en_US/type_adapters/) before assuming the column +> matches your annotation. -# Query JSONB -Product.query().where("attributes->>'brand' = ?", ("Dell",)).all() +### Async -# JSONB contains (@>) -Product.query().where("attributes @> ?", ('{"brand": "Dell"}',)).all() -``` +`AsyncPostgresBackend` exposes the same API. Method names are identical; add +`await`. -### UUID Type +```python +from rhosocial.activerecord.backend.impl.postgres.backend import AsyncPostgresBackend +from rhosocial.activerecord.model import AsyncActiveRecord -Native UUID storage and querying: -```python -from uuid import UUID, uuid4 +class Article(AsyncActiveRecord): + ... -class User(ActiveRecord): - __table_name__ = "users" - id: UUID - name: str -user = User(id=uuid4(), name="Alice") -user.save() +Article.configure(config, AsyncPostgresBackend) +rows = await Article.query().where(Article.c.title == "hello").all() ``` -### Range Types - -Built-in support for range types: +## PostgreSQL-Specific Features -```python -from datetime import date +### Native arrays -class Booking(ActiveRecord): - __table_name__ = "bookings" - room_id: int - date_range: tuple # DATERANGE +No serialization adapter is needed for `text[]`; operators map straight +through. -# Query with range operators -Booking.query().where("date_range @> ?", (date(2024, 1, 15),)).all() +```python +Article.query().where("tags @> ARRAY[?]", ("python",)).all() +Article.query().where("? = ANY(tags)", ("python",)).all() +Article.query().where("tags && ARRAY[?]", ("python",)).all() ``` -### RETURNING Clause - -Retrieve data after INSERT/UPDATE/DELETE: +### JSONB ```python -# INSERT with RETURNING -user = User(name="Alice") -user.save() -print(user.id) # Populated automatically via RETURNING - -# Works for all operations (unlike MySQL) +Product.query().where("metadata->>'brand' = ?", ("Dell",)).all() +Product.query().where("metadata @> ?", ('{"brand": "Dell"}',)).all() ``` -## Requirements - -- **Python**: 3.8+ (including 3.13t/3.14t free-threaded builds) -- **Core**: `rhosocial-activerecord>=1.0.0` -- **Driver**: `psycopg[binary]>=3.2.13` - -## PostgreSQL Version Compatibility - -| Feature | Min Version | Notes | -|---------|-------------|-------| -| Basic operations | 8.0+ | Core functionality | -| RETURNING | 8.2+ | INSERT/UPDATE/DELETE RETURNING | -| CTEs | 8.4+ | WITH clauses | -| Window functions | 8.4+ | ROW_NUMBER, RANK, etc. | -| TRUNCATE RESTART IDENTITY | 8.4+ | Reset sequences on truncate | -| JSON | 9.2+ | Basic JSON support | -| LATERAL joins | 9.3+ | LATERAL keyword | -| JSONB | 9.4+ | Binary JSON, indexed | -| FILTER clause | 9.4+ | Aggregate FILTER | -| Ordered-set aggregates | 9.4+ | PERCENTILE_CONT, etc. | -| UPSERT | 9.5+ | INSERT ... ON CONFLICT | -| Advanced grouping | 9.5+ | ROLLUP, CUBE, GROUPING SETS | -| SKIP LOCKED | 9.5+ | FOR UPDATE SKIP LOCKED | -| MATERIALIZED CTE hints | 12.0+ | MATERIALIZED/NOT MATERIALIZED | -| JSON_TABLE | 12.0+ | JSON table function | -| MERGE | 15.0+ | MERGE statement | - -**Supported SQL Protocols:** -- ✅ SetOperationSupport (UNION, INTERSECT, EXCEPT) — All versions -- ✅ TruncateSupport (TRUNCATE TABLE) — All versions; RESTART IDENTITY ≥ 8.4 +## Version Gates + +Gates below are the versions at which this backend's own `supports_*` +switch returns `True`, probed at 8.2, 8.4, 9, 9.2, 9.4, 9.5, 10, 12, 13, 14, 15, 16, 17, 18. +A version is the first probe at or above the real gate, so each is an +upper bound within one ladder step. Switches already `True` at the +oldest release carry no gate and are omitted. + +| Min version | Capabilities that turn on | +|---|---| +| 8.4 | `array_fill`, `full_text_search`, `truncate_restart_identity`, `xmlagg`, `xmlattributes`, `xmlcomment`, `xmlconcat`, `xmlelement`, `xmlexists`, `xmlforest`, `xmlparse`, `xmlpi`, `xmlroot`, `xmlserialize` | +| 9 | `for_no_key_update`, `for_share`, `ts_rank_cd` | +| 9.2 | `json_type`, `range_constructors`, `range_functions`, `range_operators`, `range_type` | +| 9.4 | `alter_materialized_view`, `filter_clause`, `for_key_share`, `jsonb`, `lateral_join`, `materialized_view`, `materialized_view_concurrent_refresh`, `materialized_view_if_not_exists`, `materialized_view_storage_options`, `materialized_view_tablespace`, `ordered_set_aggregation`, `refresh_materialized_view` | +| 9.5 | `alter_policy_rename`, `alter_policy_replace`, `array_position`, `create_policy`, `cube`, `drop_policy_if_exists`, `for_update_skip_locked`, `grouping_sets`, `if_not_exists_table`, `index_if_not_exists`, `rls_enable_always`, `rls_enable_disable`, `rls_force`, `rollup`, `trigger_if_not_exists`, `unlogged_table`, `upsert` | +| 10 | `add_partition`, `aggregate_ddl`, `alter_domain`, `alter_type`, `attach_partition`, `cluster`, `collation_ddl`, `commit_timestamp`, `copy_from`, `copy_to`, `create_domain`, `create_statistics`, `create_type`, `detach_partition`, `foreign_table_ddl`, `function_ddl`, `parallel_append`, `parallel_gather_merge`, `parallel_index_scan`, `parallel_query`, `partitioned_table_creation`, `partition_metadata_introspection`, `phrase_search`, `publication`, `safe_hash_index`, `statistics_dependencies`, `statistics_ndistinct`, `subscription`, `table_logging_switch`, `table_partitioning`, `trigger_referencing`, `type_objects`, `xmltable` | +| 12 | `call_statement`, `composite_domains`, `concurrent_index`, `default_partition`, `domain_arrays`, `generated_columns`, `gist_include`, `hash_partitioning`, `index_include`, `jit`, `json_path`, `json_table`, `jsonb_subscript`, `materialized_cte`, `nondeterministic_collation`, `parallel_create_index`, `parallel_hash_join`, `parallel_index_only_scan`, `partition_key_update`, `partitionwise_aggregate`, `partitionwise_join`, `reindex_concurrently`, `statistics_mcv`, `stored_generated_columns`, `websearch_tsquery` | +| 13 | `btree_deduplication`, `database_if_exists`, `database_if_not_exists`, `incremental_sort`, `parallel_vacuum`, `xid8_type` | +| 14 | `async_foreign_scan`, `binary_replication`, `brin_bloom`, `brin_multivalue`, `concurrent_detach`, `cte_search_cycle`, `multirange`, `multirange_agg`, `multirange_constructor`, `multirange_type`, `numeric_infinity`, `spgist_include`, `streaming_transactions`, `two_phase_decoding`, `vacuum_process_toast` | +| 15 | `copy_header_match`, `copy_text_header`, `current_role_keyword`, `merge_statement`, `nulls_not_distinct_unique`, `table_set_access_method` | +| 16 | `concurrent_unique_nulls_not_distinct`, `random_normal`, `regexp_like` | +| 17 | `copy_force_all`, `copy_log_verbosity`, `copy_on_error`, `json_table_nested_path`, `merge_with_cte`, `returning_old_new`, `update_returning_old` | +| 18 | `constraint_enforced`, `copy_reject_limit`, `copy_silent_log`, `streaming_btree_index_build` | + +PostgreSQL 12 or newer is the practical baseline; anything older misses +JSON_TABLE, materialized CTEs, generated columns and concurrent index +operations. + +## Known Limitations + +- **No connection pooling by default.** This backend connects on use and + keeps a single connection. Optional pooling is a separate extra: + `pip install rhosocial-activerecord-postgres[pooling]`, configured with + `pool_min_size` / `pool_max_size` / `pool_timeout`. See + [connection management](docs/en_US/installation_and_configuration/pool.md). +- **`RETURNING` is version-gated.** The basic clause works broadly, but + `returning_old_new` and `update_returning_old` need PostgreSQL 18. +- Field annotations do not drive DDL. `list[str]`, `dict` and `UUID` columns + are described explicitly in the `CreateTableExpression` above. -**Recommended**: PostgreSQL 12+ for optimal feature support. - -See [PROTOCOL_SUPPORT.md](docs/PROTOCOL_SUPPORT.md) for complete protocol support matrix. - -## Get Started with AI Code Agents +## Testing -This project supports AI-assisted development: +Tests require a reachable PostgreSQL server and must run serially. ```bash -git clone https://github.com/rhosocial/python-activerecord-postgres.git -cd python-activerecord-postgres +export PYTHONPATH=src:tests +.venv3.14-ubuntu26.04/bin/pytest tests/ ``` -### Example AI Prompts - -- "How do I use array operators to query tags?" -- "What's the difference between JSON and JSONB in PostgreSQL?" -- "Show me how to create a GIN index on an array column" -- "How do I use range types for scheduling?" - -### For Any LLM - -Feed the documentation files in `docs/` for context-aware assistance. - -## Testing - -> ⚠️ **CRITICAL**: Tests MUST run serially. Do NOT use `pytest -n auto` or parallel execution. +The shared contract suite lives in +[python-activerecord-testsuite](https://github.com/rhosocial/python-activerecord-testsuite): ```bash -# Run all tests -PYTHONPATH=src pytest tests/ - -# Run specific feature tests -PYTHONPATH=src pytest tests/rhosocial/activerecord_postgres_test/feature/basic/ -PYTHONPATH=src pytest tests/rhosocial/activerecord_postgres_test/feature/query/ +PYTHONPATH=tests .venv3.14-ubuntu26.04/bin/pytest \ + ../python-activerecord-testsuite/src/rhosocial/activerecord/testsuite/feature/ ``` -See the [Testing Documentation](https://github.com/rhosocial/python-activerecord/blob/main/.claude/testing.md) for details. - ## Documentation -- **[Getting Started](docs/en_US/getting_started/)** — Installation and configuration -- **[PostgreSQL Features](docs/en_US/postgres_specific_features/)** — PostgreSQL-specific capabilities -- **[Type Adapters](docs/en_US/type_adapters/)** — Data type handling -- **[Array Comparison](docs/en_US/type_adapters/array_comparison.md)** — Array support across databases -- **[Transaction Support](docs/en_US/transaction_support/)** — Transaction management - -## Comparison with Other Backends - -| Feature | PostgreSQL | MySQL | SQLite | -|---------|------------|-------|--------| -| **Native Arrays** | ✅ | ❌ | ❌ | -| **JSONB (indexed)** | ✅ | ⚠️ JSON only | ⚠️ Extension | -| **UUID Type** | ✅ Native | ⚠️ CHAR(36) | ⚠️ TEXT | -| **Range Types** | ✅ | ❌ | ❌ | -| **RETURNING** | ✅ | ❌ | ✅ | -| **CTEs** | ✅ 8.4+ | ✅ 8.0+ | ✅ 3.8.3+ | -| **Full-Text Search** | ✅ | ✅ | ⚠️ FTS5 | - -> 💡 **AI Prompt**: "When should I choose PostgreSQL over MySQL for my project?" +- **[Getting started](docs/en_US/installation_and_configuration/)** — installation, connection configuration, SSL, encoding +- **[PostgreSQL features](docs/en_US/postgres_specific_features/)** — PostgreSQL-specific capabilities +- **[Type adapters](docs/en_US/type_adapters/)** — type conversion rules, array comparison across databases +- **[Transactions](docs/en_US/transaction_support/)** — isolation levels and savepoints +- **[Troubleshooting](docs/en_US/troubleshooting/)** — connection, performance and encoding issues ## Contributing -We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. +See [CONTRIBUTING.md](https://github.com/rhosocial/python-activerecord/blob/main/CONTRIBUTING.md). ## License [Apache License 2.0](LICENSE) — Copyright © 2026 [vistart](https://github.com/vistart) - ---- - -
-

Built with ❤️ by the rhosocial team

-

GitHub · Documentation · PyPI

-
From 3708a1039fe61983bab8f0dc553f1e29e137d0a1 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 29 Sep 2026 12:56:37 +0800 Subject: [PATCH 15/19] fix(postgres): split the psycopg floor by Python version psycopg 3.3.6 declares requires-python >=3.10 while 3.2.13 supports >=3.8, so a single floor of >=3.2.13 leaves 3.10 and above on the older line. The backend is tested against 3.8 through 3.15, and the driver gate belongs with the other version-dependent behaviour rather than being left to resolve to whatever is newest. Both pyproject.toml and requirements.txt carry the same two markers, since CI installs the backend with ".[test]" and the requirements file is used for developer setup. Verified the markers evaluate as intended: 3.8 and 3.9 select >=3.2.13, 3.10 through 3.15 select >=3.3.6, and both bounds match the published metadata. --- pyproject.toml | 4 +++- requirements.txt | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c96b3de0..82e58ffd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,9 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "psycopg[binary]>=3.2.13", + # psycopg 3.3.6 requires Python 3.10+; 3.8 and 3.9 stay on the 3.2 line. + "psycopg[binary]>=3.3.6; python_version >= '3.10'", + "psycopg[binary]>=3.2.13; python_version < '3.10'", "rhosocial-activerecord>=1.0.0.dev0,<2.0.0" ] diff --git a/requirements.txt b/requirements.txt index 6f427b7e..e47bf885 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,6 @@ # Core dependency (rhosocial-activerecord) is installed via CI workflow from git # PostgreSQL driver -psycopg[binary]>=3.2.13 \ No newline at end of file +# psycopg 3.3.6 requires Python 3.10+; 3.8 and 3.9 stay on the 3.2 line. +psycopg[binary]>=3.3.6; python_version >= '3.10' +psycopg[binary]>=3.2.13; python_version < '3.10' \ No newline at end of file From 57ed25c70936a7f410e036cfa726f1a8acb4eab7 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 29 Sep 2026 15:48:32 +0800 Subject: [PATCH 16/19] fix(postgres): forward SSL/TLS config to psycopg PostgresConnectionConfig declares the libpq-named PostgresSSLMixin (sslmode/sslcert/sslkey/sslrootcert/sslcrl/sslcompression) and does not inherit the generic SSLMixin, so it has no ssl_ca/ssl_cert/ssl_key/ssl_mode attributes at all. Both connect() implementations nevertheless guarded on those four names with hasattr(), so every check was False and no TLS setting ever reached psycopg.connect(). A connection configured with sslmode="verify-full" ran in plaintext, and an unreadable sslrootcert was silently ignored instead of failing the handshake. Collect the configured libpq keywords once in PostgresConnectionConfig.get_ssl_connection_params() and call it from the sync and async connect paths, so the two cannot drift apart again. The kwargs extraction lists in both backends now accept the libpq names. Verified against a PostgreSQL 19beta4 server: pg_stat_ssl.ssl is now False for sslmode=disable and True for sslmode=require, and verify-full with a missing root CA is rejected at connect time. --- changelog.d/59.fixed.md | 1 + .../impl/postgres/backend/async_backend.py | 28 ++-- .../backend/impl/postgres/backend/backend.py | 28 ++-- .../backend/impl/postgres/backend/sync.py | 28 ++-- .../backend/impl/postgres/config.py | 35 ++++- .../backend/test_ssl_connection_params.py | 133 ++++++++++++++++++ 6 files changed, 198 insertions(+), 55 deletions(-) create mode 100644 changelog.d/59.fixed.md create mode 100644 tests/rhosocial/activerecord_postgres_test/feature/backend/test_ssl_connection_params.py diff --git a/changelog.d/59.fixed.md b/changelog.d/59.fixed.md new file mode 100644 index 00000000..da73f2de --- /dev/null +++ b/changelog.d/59.fixed.md @@ -0,0 +1 @@ +Forward SSL/TLS connection parameters to the driver. `PostgresBackend.connect()` and `AsyncPostgresBackend.connect()` read `ssl_ca`/`ssl_cert`/`ssl_key`/`ssl_mode`, but `PostgresConnectionConfig` inherits only the libpq-named `PostgresSSLMixin` (`sslmode`/`sslcert`/`sslkey`/`sslrootcert`/`sslcrl`/`sslcompression`) and not the generic `SSLMixin`, so every `hasattr()` check was `False` and no TLS setting ever reached `psycopg.connect()`. Connections therefore ran unencrypted even with `sslmode="verify-full"`, and an unreadable `sslrootcert` was silently ignored. The shared `PostgresConnectionConfig.get_ssl_connection_params()` now collects the configured libpq keywords once for both the sync and async paths, and the kwargs extraction lists in both backends accept the libpq names instead of the generic ones. diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py index d4b84fae..1244990f 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/async_backend.py @@ -109,11 +109,12 @@ def __init__(self, **kwargs: Any) -> None: "pool_name", "pool_reset_session", "pool_pre_ping", - "ssl_ca", - "ssl_cert", - "ssl_key", - "ssl_verify_cert", - "ssl_verify_identity", + "sslmode", + "sslcert", + "sslkey", + "sslrootcert", + "sslcrl", + "sslcompression", "log_queries", "log_level", "application_name", @@ -253,19 +254,10 @@ async def connect(self) -> None: else: conn_params["options"] = options_value - # Add SSL parameters if provided - ssl_params = {} - if hasattr(self.config, "ssl_ca"): - ssl_params["sslcert"] = self.config.ssl_ca - if hasattr(self.config, "ssl_cert"): - ssl_params["sslcert"] = self.config.ssl_cert - if hasattr(self.config, "ssl_key"): - ssl_params["sslkey"] = self.config.ssl_key - if hasattr(self.config, "ssl_mode"): - ssl_params["sslmode"] = self.config.ssl_mode - - if ssl_params: - conn_params.update(ssl_params) + # Add SSL/TLS parameters. The config exposes libpq keyword names + # (sslmode/sslcert/sslkey/sslrootcert/sslcrl/sslcompression) so they + # can be forwarded to psycopg verbatim. + conn_params.update(self.config.get_ssl_connection_params()) self._connection = await AsyncConnection.connect(**conn_params) await self._connection.set_autocommit(True) # Disable psycopg auto-transaction management diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py index 9a42eba4..b9929d77 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/backend.py @@ -95,11 +95,12 @@ def __init__(self, **kwargs: Any) -> None: "pool_name", "pool_reset_session", "pool_pre_ping", - "ssl_ca", - "ssl_cert", - "ssl_key", - "ssl_verify_cert", - "ssl_verify_identity", + "sslmode", + "sslcert", + "sslkey", + "sslrootcert", + "sslcrl", + "sslcompression", "log_queries", "log_level", "application_name", @@ -240,19 +241,10 @@ def connect(self) -> None: else: conn_params["options"] = options_value - # Add SSL parameters if provided - ssl_params = {} - if hasattr(self.config, "ssl_ca"): - ssl_params["sslcert"] = self.config.ssl_ca - if hasattr(self.config, "ssl_cert"): - ssl_params["sslcert"] = self.config.ssl_cert - if hasattr(self.config, "ssl_key"): - ssl_params["sslkey"] = self.config.ssl_key - if hasattr(self.config, "ssl_mode"): - ssl_params["sslmode"] = self.config.ssl_mode - - if ssl_params: - conn_params.update(ssl_params) + # Add SSL/TLS parameters. The config exposes libpq keyword names + # (sslmode/sslcert/sslkey/sslrootcert/sslcrl/sslcompression) so they + # can be forwarded to psycopg verbatim. + conn_params.update(self.config.get_ssl_connection_params()) self._connection = psycopg.connect(**conn_params) self._connection.autocommit = True # Disable psycopg auto-transaction management diff --git a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py index 25238c2b..09ce8a3a 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/backend/sync.py @@ -95,11 +95,12 @@ def __init__(self, **kwargs: Any) -> None: "pool_name", "pool_reset_session", "pool_pre_ping", - "ssl_ca", - "ssl_cert", - "ssl_key", - "ssl_verify_cert", - "ssl_verify_identity", + "sslmode", + "sslcert", + "sslkey", + "sslrootcert", + "sslcrl", + "sslcompression", "log_queries", "log_level", "application_name", @@ -240,19 +241,10 @@ def connect(self) -> None: else: conn_params["options"] = options_value - # Add SSL parameters if provided - ssl_params = {} - if hasattr(self.config, "ssl_ca"): - ssl_params["sslcert"] = self.config.ssl_ca - if hasattr(self.config, "ssl_cert"): - ssl_params["sslcert"] = self.config.ssl_cert - if hasattr(self.config, "ssl_key"): - ssl_params["sslkey"] = self.config.ssl_key - if hasattr(self.config, "ssl_mode"): - ssl_params["sslmode"] = self.config.ssl_mode - - if ssl_params: - conn_params.update(ssl_params) + # Add SSL/TLS parameters. The config exposes libpq keyword names + # (sslmode/sslcert/sslkey/sslrootcert/sslcrl/sslcompression) so they + # can be forwarded to psycopg verbatim. + conn_params.update(self.config.get_ssl_connection_params()) self._connection = psycopg.connect(**conn_params) self._connection.autocommit = True # Disable psycopg auto-transaction management diff --git a/src/rhosocial/activerecord/backend/impl/postgres/config.py b/src/rhosocial/activerecord/backend/impl/postgres/config.py index 8d7340b7..b1b18c7d 100644 --- a/src/rhosocial/activerecord/backend/impl/postgres/config.py +++ b/src/rhosocial/activerecord/backend/impl/postgres/config.py @@ -24,9 +24,26 @@ class RangeAdapterMode(Enum): BOTH = "both" +SSL_CONNECTION_PARAMS = ( + "sslmode", + "sslcert", + "sslkey", + "sslrootcert", + "sslcrl", + "sslcompression", +) + + @dataclass class PostgresSSLMixin: - """Mixin implementing postgres-specific SSL/TLS options.""" + """Mixin implementing postgres-specific SSL/TLS options. + + The field names mirror libpq connection keywords so they can be handed to + ``psycopg.connect()`` unchanged. They are deliberately distinct from the + generic :class:`~rhosocial.activerecord.backend.config.SSLMixin` names + (``ssl_ca``/``ssl_cert``/``ssl_key``/``ssl_mode``), which libpq does not + understand. + """ sslmode: Optional[str] = None # 'disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full' sslcert: Optional[str] = None @@ -96,6 +113,22 @@ class PostgresConnectionConfig( enable_type_compatibility_warnings: Enable type conversion warnings (default: True) """ + def get_ssl_connection_params(self) -> Dict[str, Any]: + """Get the libpq SSL/TLS keywords that should be forwarded to the driver. + + Both the sync and async backends call this when building their + ``psycopg.connect()`` keyword arguments, so the two paths cannot drift + apart. ``None`` values are dropped so that psycopg (which also drops + ``None``) falls back to libpq/PGSERVICE defaults for anything the user + did not set. + """ + params: Dict[str, Any] = {} + for name in SSL_CONNECTION_PARAMS: + value = getattr(self, name, None) + if value is not None: + params[name] = value + return params + def to_dict(self) -> Dict[str, Any]: """Convert config to dictionary, including postgres-specific parameters.""" # Get base config diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/test_ssl_connection_params.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_ssl_connection_params.py new file mode 100644 index 00000000..106db47e --- /dev/null +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/test_ssl_connection_params.py @@ -0,0 +1,133 @@ +# tests/rhosocial/activerecord_postgres_test/feature/backend/test_ssl_connection_params.py +"""Regression tests: SSL/TLS config must actually reach ``psycopg.connect()``. + +The config exposes libpq keyword names (``sslmode``/``sslcert``/``sslkey``/ +``sslrootcert``/``sslcrl``/``sslcompression``). These tests pin that the driver +call receives exactly those names, so the generic ``ssl_ca``/``ssl_cert``/ +``ssl_key``/``ssl_mode`` naming can never creep back in and silently drop the +settings. +""" + +from unittest.mock import patch + +import pytest + +from rhosocial.activerecord.backend.impl.postgres.backend.async_backend import AsyncPostgresBackend +from rhosocial.activerecord.backend.impl.postgres.backend.backend import PostgresBackend +from rhosocial.activerecord.backend.impl.postgres.config import PostgresConnectionConfig + + +@pytest.fixture +def ssl_config(): + return PostgresConnectionConfig( + host="db.example.com", + port=5432, + database="test_db", + username="tester", + password="secret", + sslmode="verify-full", + sslcert="/certs/client.crt", + sslkey="/certs/client.key", + sslrootcert="/certs/ca.crt", + sslcrl="/certs/ca.crl", + sslcompression=True, + ) + + +def test_config_exposes_libpq_ssl_field_names(ssl_config): + """The generic SSL mixin names must not be present on the postgres config. + + ``ConnectionConfig`` does not inherit ``SSLMixin``, so a config carrying + ``ssl_ca``/``ssl_mode`` would make the driver look for attributes that do + not exist and quietly forward nothing. + """ + for missing in ("ssl_ca", "ssl_cert", "ssl_key", "ssl_mode"): + assert not hasattr(ssl_config, missing), ( + f"PostgresConnectionConfig must not expose generic SSL field {missing!r}" + ) + + +def test_get_ssl_connection_params_returns_all_set_values(ssl_config): + assert ssl_config.get_ssl_connection_params() == { + "sslmode": "verify-full", + "sslcert": "/certs/client.crt", + "sslkey": "/certs/client.key", + "sslrootcert": "/certs/ca.crt", + "sslcrl": "/certs/ca.crl", + "sslcompression": True, + } + + +def test_get_ssl_connection_params_omits_unset_values(): + config = PostgresConnectionConfig(host="localhost", database="test_db", sslmode="require") + assert config.get_ssl_connection_params() == {"sslmode": "require"} + + +def test_get_ssl_connection_params_empty_by_default(): + config = PostgresConnectionConfig(host="localhost", database="test_db") + assert config.get_ssl_connection_params() == {} + + +def test_sync_connect_forwards_ssl_params(ssl_config): + """Sync connect() must hand the libpq SSL keywords to psycopg verbatim.""" + backend = PostgresBackend(connection_config=ssl_config) + with patch("rhosocial.activerecord.backend.impl.postgres.backend.backend.psycopg.connect") as connect: + backend.connect() + + params = connect.call_args.kwargs + for key, value in ssl_config.get_ssl_connection_params().items(): + assert params[key] == value + assert params["host"] == "db.example.com" + assert params["dbname"] == "test_db" + + +@pytest.mark.asyncio +async def test_async_connect_forwards_ssl_params(ssl_config): + """Async connect() must hand the same libpq SSL keywords to psycopg.""" + backend = AsyncPostgresBackend(connection_config=ssl_config) + connect = patch( + "rhosocial.activerecord.backend.impl.postgres.backend.async_backend.AsyncConnection.connect" + ) + with connect as mock_connect: + mock_connect.return_value.set_autocommit = _noop_coroutine + await backend.connect() + + params = mock_connect.call_args.kwargs + for key, value in ssl_config.get_ssl_connection_params().items(): + assert params[key] == value + assert params["host"] == "db.example.com" + assert params["dbname"] == "test_db" + + +async def _noop_coroutine(*args, **kwargs): + return None + + +def test_sync_connect_sends_no_ssl_keys_when_unset(): + """No stray ``ssl*`` key may be emitted when the user configured nothing. + + psycopg drops ``None`` kwargs, but an explicit ``sslmode=None`` would hide + the difference between "unset" and "dropped by accident"; keeping the key + out entirely is what lets libpq/PGSERVICE defaults apply. + """ + config = PostgresConnectionConfig(host="localhost", port=5432, database="test_db") + backend = PostgresBackend(connection_config=config) + with patch("rhosocial.activerecord.backend.impl.postgres.backend.backend.psycopg.connect") as connect: + backend.connect() + + assert not [key for key in connect.call_args.kwargs if key.startswith("ssl")] + + +def test_sslmode_disable_is_distinguishable_from_require(): + """A stored sslmode must not be normalised away. + + ``disable`` and ``require`` are the two values that visibly change the + resulting session, so a config that collapses them is a broken config. + """ + def build(mode): + return PostgresConnectionConfig( + host="localhost", database="test_db", sslmode=mode + ).get_ssl_connection_params() + + assert build("disable") == {"sslmode": "disable"} + assert build("require") == {"sslmode": "require"} From 8e4804be99292a3fd1f50ea9b087c475824149f7 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 29 Sep 2026 16:38:16 +0800 Subject: [PATCH 17/19] chore: rename changelog fragment to PR #60 --- changelog.d/{59.fixed.md => 60.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{59.fixed.md => 60.fixed.md} (100%) diff --git a/changelog.d/59.fixed.md b/changelog.d/60.fixed.md similarity index 100% rename from changelog.d/59.fixed.md rename to changelog.d/60.fixed.md From b359b533bbf20938932bde14c87d3faf3f6cd652 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 29 Sep 2026 17:07:52 +0800 Subject: [PATCH 18/19] ci: unify the changelog fragment rules Replace the six divergent changelog-check variants with one implementation that encodes the documented merge rules. feature/*|fix/* -> release/*|maint/*|main: - the PR must add at least one changelog.d/..md - every added fragment's must equal the PR number - fragments already present from previously merged branches are ignored, since only files ADDED by this PR are considered - escape hatches ([trivial] title, skip-changelog label, docs: update CHANGELOG) waive only the "at least one" requirement; the number-must-match rule is always enforced on fragments that were added release/* -> main: - CHANGELOG.md must have changed (content is not inspected) - changelog.d/ must hold no fragments at the merge commit - no escape hatches apply to this path The number-must-match rule was the one documented requirement that none of the previous variants implemented. Two latent bugs are also fixed: the paths-ignore of "**.md" meant a CHANGELOG.md-only push never ran the check at all, and three backends skipped release merges outright instead of applying the release rules. --- .github/workflows/changelog-check.yml | 240 ++++++++++++++++++-------- 1 file changed, 168 insertions(+), 72 deletions(-) diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml index b0b920fc..60ee0087 100644 --- a/.github/workflows/changelog-check.yml +++ b/.github/workflows/changelog-check.yml @@ -1,4 +1,21 @@ # .github/workflows/changelog-check.yml +# +# Enforces the rhosocial changelog fragment rules. +# +# feature/* or fix/* -> release/* | maint/*| main +# - the PR must add at least one changelog.d/..md fragment +# - every added fragment's must equal the PR number +# - fragments already present in changelog.d/ from previously merged +# branches are ignored; only files ADDED by this PR are considered +# +# release/* -> main +# - CHANGELOG.md must have changed (content is not inspected) +# - changelog.d/ must contain no fragments at the merge commit +# +# Escape hatches (all three retained): a [trivial] marker in the PR title, +# the skip-changelog label, or a "docs: update CHANGELOG" title. An escape +# hatch waives the "at least one fragment" requirement only -- the +# number-must-match-PR rule is always enforced on fragments that were added. name: Changelog Fragment Check on: @@ -9,12 +26,9 @@ on: - 'maint/**' push: branches: + - main - 'release/v**' - 'maint/**' - paths-ignore: - - 'docs/**' - - '**.md' - - '.github/workflows/**' permissions: contents: read @@ -28,95 +42,177 @@ jobs: with: fetch-depth: 0 - - name: Check for changelog fragment + - name: Validate changelog fragments env: - PR_TITLE: ${{ github.event.pull_request.title || '' }} EVENT_NAME: ${{ github.event_name }} + HEAD_REF: ${{ github.head_ref || '' }} + BASE_REF: ${{ github.base_ref || '' }} + PR_NUMBER: ${{ github.event.pull_request.number || '' }} + PR_TITLE: ${{ github.event.pull_request.title || '' }} + PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} run: | - # For push events, check if there are uncommitted changelog fragments - if [[ "$EVENT_NAME" == "push" ]]; then - echo "Push event detected - checking for changelog fragments in last commit" + set -uo pipefail + + # Towncrier fragment types. Advisory only: a project may legitimately + # configure a different set in pyproject.toml [tool.towncrier.type]. + KNOWN_TYPES="added changed deprecated removed fixed performance security docs internal" + + # Files that live in changelog.d/ but are not fragments. + is_boilerplate() { + case "${1##*/}" in + README.md|template.md|.gitkeep) return 0 ;; + *) return 1 ;; + esac + } + + # Validate against (empty = don't care). + # Prints the fragment type on success. + validate_fragment() { + local path="$1" expected="$2" + local base num type + + base="${path##*/}" + + if [[ ! "$base" =~ ^([0-9]+)\.([a-zA-Z]+)\.md$ ]]; then + echo "::error file=$path::fragment must be named ..md, got '$base'" + return 1 + fi + num="${BASH_REMATCH[1]}" + type="${BASH_REMATCH[2]}" - # Check if last commit already contains a changelog fragment - LAST_COMMIT_FILES=$(git diff-tree --no-commit-id --name-only -r HEAD) + if [[ -n "$expected" && "$num" != "$expected" ]]; then + echo "::error file=$path::fragment number '$num' does not match PR #$expected" + echo " Rename it to changelog.d/$expected.$type.md, or open a PR for #$num." + return 1 + fi - if echo "$LAST_COMMIT_FILES" | grep -q "^changelog.d/[0-9].*\.md$"; then - echo "✓ Changelog fragment found in last commit" - exit 0 + if [[ " $KNOWN_TYPES " != *" $type "* ]]; then + echo "::warning file=$path::'$type' is not a standard towncrier type" >&2 fi - # Check if last commit is a version bump or changelog build - LAST_MSG=$(git log -1 --pretty=%B) - if [[ "$LAST_MSG" =~ ^chore:\ bump\ version ]] || [[ "$LAST_MSG" =~ ^docs:\ update\ CHANGELOG ]]; then - echo "✓ Version bump or changelog update commit, fragment not required" - exit 0 + echo "$type" + return 0 + } + + is_exempt() { + [[ "$PR_TITLE" == *"[trivial]"* ]] && return 0 + [[ ",$PR_LABELS," == *",skip-changelog,"* ]] && return 0 + [[ "$PR_TITLE" == "docs: update CHANGELOG"* ]] && return 0 + return 1 + } + + # ------------------------------------------------------------------ + # release/* -> main : CHANGELOG must change, changelog.d/ must be empty + # ------------------------------------------------------------------ + check_release_merge() { + local scope="$1" changed remaining failed=0 + + changed=$(git diff --name-only "origin/main...HEAD" -- CHANGELOG.md) + if [[ -z "$changed" ]]; then + echo "::error::CHANGELOG.md was not updated by this $scope." + echo " A release merge must regenerate the changelog (e.g. \`towncrier build --yes --keep\`)" + echo " before the fragments under changelog.d/ are discarded." + failed=1 + else + echo "OK: CHANGELOG.md updated" fi - # Check if there are any source code changes (excluding tests/docs) - if echo "$LAST_COMMIT_FILES" | grep -qE "^src/.*\.py$"; then - echo "⚠ Warning: Source code changes without changelog fragment" - echo "Consider adding a changelog fragment for traceability" - echo "Changed files: $LAST_COMMIT_FILES" - # Warning only, don't fail the build - exit 0 + # Checked against the merge result, not the diff: leftover fragments + # from earlier in the release cycle must be gone by this point. + remaining=$(git ls-tree --name-only HEAD changelog.d/ \ + | while read -r f; do + if ! is_boilerplate "$f" \ + && [[ "${f##*/}" =~ ^[0-9]+\.[a-zA-Z]+\.md$ ]]; then + echo "$f" + fi + done) + if [[ -n "$remaining" ]]; then + echo "::error::changelog.d/ still contains fragments after the release merge:" + echo "$remaining" | while read -r f; do echo " - $f"; done + echo " Delete them; their content is already folded into CHANGELOG.md." + failed=1 + else + echo "OK: changelog.d/ is clear of fragments" fi - echo "✓ No source code changes requiring fragment" + return $failed + } + + # ------------------------------------------------------------------ + # push event: no PR number available, so only structural checks + # ------------------------------------------------------------------ + if [[ "$EVENT_NAME" == "push" ]]; then + if [[ "$GITHUB_REF_NAME" == "main" ]]; then + touched=$(git diff-tree --no-commit-id --name-only -r HEAD) + if ! echo "$touched" | grep -qE '^(CHANGELOG\.md|changelog\.d/|src/)'; then + echo "OK: no changelog-relevant changes in this push" + exit 0 + fi + check_release_merge "push to main" || exit 1 + exit $? + fi + echo "OK: push to $GITHUB_REF_NAME (fragment numbering is validated on the PR)" exit 0 fi - # For pull_request events, use the original logic - - # Fetch the base branch explicitly to ensure it's available for comparison - git fetch origin ${{ github.base_ref }} - - # Compare the head of the PR (current HEAD) with the base branch's remote tracking ref - # This compares the changes introduced by the PR branch relative to its base. - # Use --diff-filter=A to only consider added files for fragment check - CHANGED_FILES=$(git diff --name-only --diff-filter=A origin/${{ github.base_ref }}...HEAD) + # ------------------------------------------------------------------ + # pull_request + # ------------------------------------------------------------------ + echo "head=$HEAD_REF base=$BASE_REF pr=#$PR_NUMBER" - # Debugging: Print the diff command and its output - echo "Executing: git diff --name-only --diff-filter=A origin/${{ github.base_ref }}...HEAD" - echo "Changed files (added only): $CHANGED_FILES" - - # --- NEW EXEMPTION FOR CHANGELOG BUILD PRS --- - # Check if PR title indicates a changelog update (e.g., "docs: update CHANGELOG") - # This exempts PRs that are specifically for building the changelog (which remove fragments) - if [[ "$PR_TITLE" =~ ^docs:\ update\ CHANGELOG ]]; then - echo "✓ PR title indicates changelog update, fragment check skipped" + if [[ "$HEAD_REF" == release/* && "$BASE_REF" == "main" ]]; then + git fetch origin main --quiet + echo "== release merge into main ==" + check_release_merge "release merge" || exit 1 exit 0 fi - # --- END NEW EXEMPTION --- - # Check if any changelog fragment was added - if echo "$CHANGED_FILES" | grep -q "^changelog.d/[0-9].*\.md$"; then - echo "✓ Changelog fragment found" - exit 0 + git fetch origin "$BASE_REF" --quiet + + # Fragments ADDED by this PR. Fragments that were already in + # changelog.d/ (carried in by previously merged branches) are not in + # this list and are therefore ignored. + added=$(git diff --name-only --diff-filter=A "origin/$BASE_REF...HEAD" -- 'changelog.d/*.md') + + failed=0 + types="" + if [[ -n "$added" ]]; then + while read -r f; do + [[ -z "$f" ]] && continue + is_boilerplate "$f" && continue + t=$(validate_fragment "$f" "$PR_NUMBER") || failed=1 + [[ -n "$t" ]] && types="$types $t" + done <<< "$added" fi - # Check if this is an internal change (exempt from fragment requirement) - if echo "$CHANGED_FILES" | grep -qE "^(tests/|docs/|\.github/)"; then - echo "✓ Internal change, fragment not required" - exit 0 + # The number-must-match rule is enforced even for exempt PRs; only + # the presence requirement is waived. + if [[ "$failed" -ne 0 ]]; then + echo "" + echo "✗ Fragment numbering does not match PR #$PR_NUMBER" + exit 1 fi - # Check if PR is marked as trivial - if [[ "$PR_TITLE" =~ \[trivial\] ]]; then - echo "✓ PR marked as trivial, fragment not required" - exit 0 + if [[ -z "$added" ]]; then + if is_exempt; then + echo "OK: no fragment required (exempt)" + exit 0 + fi + if git diff --name-only "origin/$BASE_REF...HEAD" \ + | grep -qE '^(tests/|docs/|\.github/)' \ + && ! git diff --name-only "origin/$BASE_REF...HEAD" | grep -qE '^src/'; then + echo "OK: internal-only change, no fragment required" + exit 0 + fi + echo "::error::No changelog fragment added by this PR." + echo " Add at least one changelog.d/$PR_NUMBER..md" + echo "" + echo "Types: added | changed | deprecated | removed | fixed |" + echo " performance | security | docs | internal" + echo "" + echo "Or waive it: add [trivial] to the PR title, apply the" + echo "skip-changelog label, or title the PR 'docs: update CHANGELOG'." + exit 1 fi - echo "✗ Missing changelog fragment" - echo "Please add a changelog fragment in changelog.d/" - echo "Example: changelog.d/${{ github.event.number }}.fixed.md" - echo "" - echo "Fragment types:" - echo " - added.md - New features" - echo " - fixed.md - Bug fixes" - echo " - changed.md - Behavior changes" - echo " - deprecated.md - Deprecations" - echo " - removed.md - Removed features" - echo " - security.md - Security fixes" - echo "" - echo "If this PR does not need a changelog entry, add [trivial] to the title." - exit 1 + echo "OK: fragment(s) added for PR #$PR_NUMBER ->$types" From 3dda125ec47a114ac0a2b29d371e2f104f4929e4 Mon Sep 17 00:00:00 2001 From: zhao vistart Date: Tue, 29 Sep 2026 17:31:20 +0800 Subject: [PATCH 19/19] ci: reject feature/fix branches that target main directly A feature/fix branch merges into a release or maintenance branch; only release/* merges into main. Opening a second PR at main from the same branch is a duplicate, and it also breaks the changelog fragment, which is numbered for exactly one PR. This pattern already cost a wasted PR in six repositories (a ->release PR merged alongside a ->main PR that was then closed), and the fragment numbering check only exposed it indirectly, by failing the leftover PR. hotfix/* -> main stays allowed; the version control policy calls for hotfixes to be merged back to main and the active release branch. --- .github/workflows/changelog-check.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/changelog-check.yml b/.github/workflows/changelog-check.yml index 60ee0087..abc4ec9f 100644 --- a/.github/workflows/changelog-check.yml +++ b/.github/workflows/changelog-check.yml @@ -3,6 +3,7 @@ # Enforces the rhosocial changelog fragment rules. # # feature/* or fix/* -> release/* | maint/*| main +# - a feature/fix branch must not target main directly # - the PR must add at least one changelog.d/..md fragment # - every added fragment's must equal the PR number # - fragments already present in changelog.d/ from previously merged @@ -167,6 +168,28 @@ jobs: exit 0 fi + # A feature/fix branch never merges straight into main; it targets a + # release or maintenance branch. Opening a second PR at main from the + # same branch is a duplicate, and it also invalidates the fragment + # numbering, which is bound to a single PR number. + if [[ ("$HEAD_REF" == feature/* || "$HEAD_REF" == fix/* || "$HEAD_REF" == bugfix/* \ + || "$HEAD_REF" == test/* || "$HEAD_REF" == docs/* || "$HEAD_REF" == chore/*) \ + && "$BASE_REF" == "main" ]]; then + echo "::error::'$HEAD_REF' must not target \`main\` directly." + echo "" + echo "Per the rhosocial merge rules:" + echo " feature/* fix/* bugfix/* test/* docs/* chore/* -> release/* | maint/*" + echo " release/* -> main" + echo "" + echo "Retarget this PR at the active release branch, or close it if" + echo "another PR from '$HEAD_REF' already targets the release branch." + echo "The changelog fragment is numbered for one PR only, so a duplicate" + echo "PR at main would also fail the fragment numbering check." + echo "" + git branch -r --list "origin/release/*" | sed 's#.*origin/##' | sed 's/^/ /' + exit 1 + fi + git fetch origin "$BASE_REF" --quiet # Fragments ADDED by this PR. Fragments that were already in